Linux安全程式設計:避免競爭條件
【賽迪網-IT技術報道】瞭解什麼是競爭條件,以及它們為什麼會引發安全問題。本文向您展示瞭如何在類 UNIX® (Unix-like)系統中處理常見的競爭條件,包括如何正確地建立鎖檔案、鎖檔案的替代者,如何處理檔案系統,以及如何處理共享目錄(特別是如何在 /tmp 目錄下正確建立臨時目錄)。需要您對訊號處理稍有了解。
通過一個偷竊而來的口令,Mallory 成功地登入到一臺執行 Linux 的重要伺服器。其帳號是一個非常受限的帳號,但是 Mallory 知道如何使用它來製造麻煩。Mallory 安裝並運行了一個行為非常奇怪的小程式,該程式使用多個程序在 /tmp 目錄下快速地建立和刪除很多不同的符號連結檔案。(符號連結檔案也稱為 symlink,是一種簡單的檔案,當被訪問時,它會將請求重定向到另一個檔案。)Mallory 的程式不停地建立和刪除很多指向同一特殊檔案(/etc/passwd,口令檔案)的不同符號連結檔案。
這臺重要的伺服器的安全措施之一是,它每天都執行 Tripwire —— 具體地說,是較老的 2.3.0 版本。Tripwire 是一個檢測重要檔案是否被篡改的安全程式。與很多程式一樣,Tripwire 啟動時會嘗試著建立一個臨時檔案。Tripwire 會檢視並斷定不存在名為“/tmp/twtempa19212”的檔案,所以看起來這是一個合適的臨時檔名稱。但是在 Tripwire 完成檢查後,Mallory 的程式就會使用該名稱建立一個符號連結檔案。這不是偶然的;Mallory 程式的設計目標就是建立最有可能為 Tripwire 所使用的檔名。然後 Tripwire 就會開啟該檔案,開始寫入臨時資訊,但不用建立新的空檔案,Tripwire 現在正在重寫口令檔案!從那時起,任何人 —— 甚至是管理員 —— 都不能登入到該系統,因為口令檔案已經被破壞了。更糟的是,Mallory 的攻擊完全可以覆蓋所有檔案,包括伺服器上儲存的重要資料。
競爭條件簡介
這是個假想的故事;“Mallory”是攻擊者的一個慣用名。但是這類攻擊,以及它所利用的缺陷,都極其常見。問題是很多程式都容易受到名為“競爭條件”的安全問題的影響。
當由於事件次序異常而造成對同一資源的競爭,從而導致程式無法正常執行時,就會出現“競爭條件”。注意,競爭條件無需介入同一程式的兩個部分之間的競爭;如果一個外部的攻擊者可以通過意想不到的方式干擾程式,那麼就會出現很多安全問題。例如,如果 Tripwire 2.3.0 確定某個檔案不存在,它就會嘗試著建立該檔案,而不去考慮在進行這兩個步驟期間,該檔案是否已經被攻擊者建立。幾十年前,競爭條件還不是什麼問題;那時,計算機系統通常在同一時刻只能執行一個單獨的程式,什麼都不能打斷它或者與它競爭。但是,當今的計算機通常需要同時執行大量的程序和執行緒,經常還會有多個處理器確實在同時執行不同的程式。這樣做更靈活,但是有一個危險:如果這些程序和執行緒共享了所有的資源,那麼它們都可能互相影響。實際上,競爭條件缺陷是軟體的更常見缺陷之一,此外,在類 Unix 系統上,/tmp 和 /var/tmp 目錄經常會被錯誤地使用,從而導致競爭條件。
不過,我們首先需要了解一些術語。所有 類-Unix 系統都支援使用者程序;每個程序都有自己的記憶體空間(其他程序通常無法訪問)。底層的核心會盡量使程序看起來像是在同時執行;在多處理器的系統中,它們確實可以同時執行。從理論上講,一個程序可以擁有一個或多個執行緒;這些執行緒可以共享記憶體。執行緒也可以同時執行。由於執行緒可以共享記憶體,所以,相對於程序,執行緒之間更有可能產生競爭條件;正是由於這個原因,多執行緒程式的除錯要困難得多。Linux 核心有一個非常好的基本設計:只有執行緒,並且一些執行緒可以與其他執行緒共享記憶體(這樣實現了傳統的執行緒),而另外一些執行緒則不能(這樣就實現了獨立程序)。
為了理解競爭條件,讓我們首先來看一個非常普通的 C 宣告:
清單 1. 普通的 C 宣告
b = b + 1; |
看起來非常簡單,不是嗎?但是,讓我們假定有兩個執行緒在執行這一行程式碼,在這裡,“b”是一個由兩個執行緒共享的變數,“b”的初始值為“5”。以下是一個似是而非的執行次序:
清單 2. 使用共享的“b”的可能執行次序
(thread1) load b into some register in thread 1. (thread2) load b into some register in thread 2. (thread1) add 1 to thread 1's register, computing 6. (thread2) add 1 to thread 2's register, computing 6. (thread1) store the register value (6) to b. (thread2) store the register value (6) to b. |
初始值為 5,然後兩個執行緒分別加 1,但是最終的結果是 6... 而不是應該得到的 7。問題在於,兩個執行緒互相干擾,從而導致產生錯誤的最終答案。
通常,執行緒不是以原子的方式執行的;另一個執行緒可以在任何兩個指令期間打斷它,而且還可以使用一些共享的資源。如果一個安全程式的執行緒沒有預防這些中斷,那麼另一個執行緒就可以干擾該安全程式的執行緒。在安全程式中,不管在任何一對指令中間運行了多少其他執行緒的程式碼,程式都必須正確地執行。關鍵是,當您的程式訪問任意資源時,要確定其他某個執行緒是否可能因為使用該資源對您的程式造成干擾。
解決競爭條件
競爭條件的典型解決方案是,確保程式在使用某個資源(比如檔案、裝置、物件或者變數)時,擁有自己的專有權。獲得某個資源的專有權的過程稱為加鎖。鎖不太容易處理。死鎖(“抱死,deadly embrace”)是常見的問題,在這種情形下,程式會因等待對方釋放被加鎖的資源而無法繼續執行。要求所有執行緒都必須按照相同的順序(比如,按字母排序,或者從“largest grain”到“smallest grain”的順序)獲得鎖,這樣可以避免大部分死鎖。另一個常見問題是活鎖(livelock),在這種情況下,程式至少成功地獲得和釋放了一個鎖,但是以這種方式無法將程式再繼續執行下去。如果一個鎖被掛起,順利地釋放它會很難。簡言之,編譯在任何情況下都可以按需要正確地加鎖和釋放的程式通常很困難。
有時,可以一次執行一個單獨操作來完成一些特殊的操作,從而使您不需要顯式地對某個資源進行加鎖而後再解鎖。這類操作稱為“原子”操作,只要能夠使用這類操作,它們通常是最好的解決方案。
有一些錯誤是如此常見,所以,為了避免犯這些錯誤,您需要了解它們。一個問題是,以不總是鎖定某資源的方式建立鎖檔案;您應該學習如何正確建立它們,或者轉而採取不同的加鎖機制。您還需要正確地處理檔案系統中的競爭,其中包括如何處理永遠危險的共享目錄 /tmp 和 /var/tmp,以及如何安全地使用訊號。下一章中將描述如何安全使用它們。
鎖檔案
通常,類 Unix 系統是通過建立表示一個鎖的檔案來實現不同程序間共享的鎖。使用單獨的檔案來表示鎖,是“勸告式(advisory)”鎖而不是“強制(mandatory)”鎖的一個例子。換句話說,作業系統不會強制您通過鎖來共享資源,所以,所有需要該資源的程序都必須協同使用該鎖。這看起來好像很簡單,但並不是所有簡單的主意都不是好主意;建立單獨的檔案,就可以方便地獲得系統的狀態,其中包括哪些資源被加鎖了。如果您使用這種方法,有一些標準的技巧可以簡化這些鎖的清除,具體地說,是刪除那些掛起的鎖。例如,一個父程序可以設定一個鎖,然後呼叫一個子程序來執行工作(確保父程序可以有效地呼叫子程序),當子程序返回時,父程序釋放該鎖。或者,可以使用 cron 作業來檢視那些鎖(其中包括程序的 id);如果程序沒有處於活動狀態,那麼該作業就會清除那些鎖,並重新啟動相應的程序。最後,鎖檔案的清除可以作為系統啟動的一部分(從而使您的鎖在系統突然崩潰之後不再處於掛起狀態)。
如果您正在建立單獨的檔案來表示鎖,那麼要注意一個常見的錯誤:對 creat() 或者與之相當的 open() 的呼叫(模式為 O_WRONLY | O_CREAT | O_TRUNC)。問題是,root 總是 可以這樣建立檔案,即便鎖檔案已經存在,這意味著該鎖不能為 root 正常工作。簡單的解決方案是在使用 open() 時指定標記 O_WRONLY | O_CREAT | O_EXCL(將許可權設定為 0,使同一使用者的其他程序無法獲得該鎖)。注意 O_EXCL 的使用,這是建立“專用”檔案的正式途徑;甚至在本地檔案系統上,root 也可以這樣做。這個簡單的方法對 NFS 版本 1 或者版本 2 不適用;如果必須在使用這些老的 NFS 版本連線的遠端系統上使用鎖檔案,那麼可以使用 Linux 文件中給出的方案:“在相同的檔案系統上建立一個惟一的檔案(例如,結合主機名和 pid),使用 link(2) 來建立一個指向鎖檔案的連結,使用 stat(2) 來檢查該惟一檔案的連結計數器是否增加到了 2。不要使用 link(2) 呼叫的返回值。”
如果您使用檔案來表示鎖,那麼要確保這些鎖檔案放置在攻擊者無法利用(例如,不能刪除它們或者新增干擾它們的檔案)的位置。典型的解決方案是使用一個目錄,使該目錄的許可權根本不允許未經授權的程式新增或者刪除檔案。確保只有您可以信任的程式才能新增或者刪除鎖檔案!
檔案系統層次結構標準(Filesystem Hierarchy Standard,FHS)得到了 Linux 系統的廣泛使用,同時還引入了這類鎖檔案的標準約定。如果您只是希望確保您的伺服器在一臺給定的機器上執行不超過一次,那麼您通常應該建立一個名為 /var/run/NAME.pid 的程序識別符號,以程序 id 作為檔案內容。根據同樣的思路,您應該將裝置鎖檔案之類的鎖檔案放置在 /var/lock 中。
鎖檔案的代替者
使用單獨的檔案來表示鎖是一個非常古老的方法。另一個方法是使用 POSIX 記錄鎖(record locks),它通過 fcntl(2) 實現為一個任意的鎖。採用 POSIX 記錄鎖的理由有很多:POSIX 記錄鎖在幾乎所有的類 Unix 平臺上都獲得了支援(它得到了 POSIX.1 的授權),它可以鎖定檔案的一部分(而不是隻會鎖定整個檔案),而且它可以區別處理讀鎖和寫鎖的不同之處。此外,如果一個程序死掉,那麼它的 POSIX 記錄鎖就會自動被刪除。
只有所有程式都共同合作的時候,使用單獨的檔案或者 fcntl(2) 任意鎖才能生效。如果您不喜歡該思想,那麼可以轉而使用 System V 風格的強制鎖。強制鎖允許您鎖定一個檔案(或者它的一部分),使每一次 read(2) 和 write(2) 都檢查鎖,任何沒有持有該鎖的操作都將被掛起,直到該鎖被釋放為止。這樣做可能稍微方便一些,但也有其缺點;擁有 root 特權的程序也可能被強制鎖掛起,這樣通常容易造成拒絕服務(denial-of-service)攻擊。實際上,拒絕服務問題是非常嚴重的,因此通常要避免使用強制鎖。強制鎖是可用範圍很廣,但它不是通用的;Linux 和基於 System V 的系統支援這種鎖,但其他的類 Unix 系統不支援它。在 Linux 上,為了啟用強制檔案鎖,必須用特定的方式裝配檔案系統,因此很多配置在預設情況下不支援強制檔案鎖。
在一個程序內部,執行緒可能也同樣需要鎖;有很多書都非常詳細地討論了這些問題。在這裡,我們要討論的主要問題是確保您小心地涵蓋了所有情況;很容易忘記某個特定情形,或者沒有正確處理。事實上,正確使用鎖是很難的,攻擊者可能利用這些鎖處理中的錯誤。如果您需要在一個程序內部對執行緒使用很多鎖,那麼可以考慮使用自動完成鎖的維護的語言或者語言結構。有很多語言,比如 Java 和 Ada95,都有內建的可以自動處理鎖維護(並使結果有可能更正確)的語言結構。
只要有可能,在開發程式時最好根本不使用鎖。一個單獨的伺服器程序每次只接受一個客戶機請求,然後處理該請求,直到完成該請求為止,而後再獲得下一個請求,從某種意義上講,程序內部的所有物件是被自動鎖定的;這種簡單的設計可以避免很多危險的加鎖問題。如果您需要一個鎖,那麼保持其簡單性(比如為幾乎所有內容都使用惟一的鎖)是有好處的。這並不總是實用的,因為這樣設計有時會損害效能。具體地說,單伺服器系統需要確保無論哪個操作都無法佔用過長的時間。但是這個建議是值得考慮的;使用很多鎖的系統會更可能有缺陷,而且維護這些鎖也會影響效能。
處理檔案系統
安全程式的編寫必須確保攻擊者無法以導致問題的方式利用共享的資源,有時這並不像看起來那樣容易辦到。檔案系統是最常見的共享資源之一。所有的程式都可以共享檔案系統,所以,有時需要額外的努力,以確保攻擊者不能以引發問題的方式利用檔案系統。
有很多確定為安全的程式都存在稱為“time of check - time of use”(TOCTOU)的競爭條件缺陷。這隻說明了程式檢查某種情形是否可行,然後稍後使用那一資訊,但是攻擊者可能會在這兩個步驟之間改變該情形。對檔案系統來說,以下問題尤為突出;在這兩個步驟之間,攻擊者通常可以建立一個普通的檔案或者一個符號連結。例如,如果某個已授予特權的程式檢查是否不存在給定名稱的檔案,然後開啟該檔案寫入資訊,那麼在那兩個步驟之間,攻擊者可以建立一個使用該名稱的符號連結檔案... 比如 /etc/passwd 或者其他一些敏感檔案。
遵守一些簡單的規則,可以避免這些問題:
•不要使用 access(2) 來判定您是否可以做某件事情;通常攻擊者會在呼叫 access(2) 後改變該情形,所以,通過呼叫 access(2) 獲得的任何資料都可能不再是可信任的。換一種方式,將您的程式的特權設定得恰好是您想要的特權(例如,設定它的有效 id、檔案系統 id 或者有效 gid,並通過 setgroups 來清除所有不需要的組);然後呼叫 open(2) 直接開啟或建立您需要的檔案。在類 Unix 系統上, open(2) 呼叫是原子的(與以前的 NFS 系統版本 1 和版本 2 不同)。
•當建立一個新檔案時,使用 O_CREAT | O_EXCL 模式開啟它(確保只有在建立一個新檔案時呼叫 O_EXCL 才會成功)。最初只授與非常有限的許可權;至少禁止任意的使用者修改它!通常,這表示您需要使用 umask 和/或開啟引數,將初始的訪問許可權侷限於使用者,也可以侷限於使用者所在的組。不要嘗試在建立完檔案後再去減少許可權,因為這樣做會導致競爭條件。在大部分類 Unix 系統上,只在開啟檔案時才檢查許可權,所以,攻擊者可以在許可權位(permission bit)允許時開啟檔案,並使該檔案一直處於開啟狀態,不管許可權如何改變。如果您願意,還可以在以後將許可權修改得更為開放。您還需要為開啟失敗做好準備。如果您絕對需要能開啟某個新檔案,那麼應該建立一個迴圈:(1)建立一個“隨機”的檔名,(2)使用 O_CREAT | O_EXCL 選項開啟檔案,(3)成功開啟檔案後停止迴圈。
•當對檔案的元資訊進行操作時(比如修改它的所有者、對檔案進行統計,或者修改它的許可權位),首先要開啟該檔案,然後對開啟的檔案進行操作。只要有可能,應儘量避免使用獲取檔名的操作,而是使用獲取檔案描述符的操作。這意味著要使用 fchown( )、 fstat( ) 或 fchmod( ) 系統呼叫,而不使用取得檔名的函式,比如 chown()、 chgrp() 和 chmod()。這樣做將避免檔案在您的程式執行時被替換(一種可能的競爭條件)。例如,如果您關閉一個檔案,然後使用 chmod() 來修改其許可權,那麼攻擊者很可能在這兩個步驟之間移動或刪除該檔案,並建立指向另一個檔案(比如 /etc/passwd)的符號連結。
•如果您的程式需要遍歷檔案系統(遞迴地遍歷子目錄),那麼要提防攻擊者可能會利用您正在遍歷的目錄結構。這種情形的一個常見的例子是,執行您的程式的管理員、系統程式或者有特權的伺服器正在遍歷的是由普通使用者控制的檔案系統部分。GNU 檔案實用程式(fileutils)可以完成遞迴目錄刪除和目錄移動,但是在版本 4.1 之前,當遍歷目錄結構時,它只是簡單的遵循“..”這個特殊條目。當檔案被刪除時,攻擊者可以將一個低層級的目錄移動到更高的層級;fileutils 將會遵循“..”目錄向上到更高層級,可能會一直到檔案系統的根。通過在適當的時間刪除目錄,攻擊者可以刪除計算機中的任何檔案。您不應該信任“..”或“.”,如果它們是由攻擊者控制的。
如果可以,不要將檔案放置在可以由不信任使用者共享的目錄中。如果不是那樣,那麼應該儘量不使用在使用者間共享的目錄。不要介意建立只能由受信任的特定程序訪問的目錄。
考慮避免使用傳統的共享目錄 /tmp 和 /var/tmp。如果您可以只使用一個管道,將資料從一個位置傳送到另一個位置,那麼您就可以簡化程式,並排除潛在的安全問題。如果您確實需要建立一個臨時檔案,那麼可以考慮將臨時檔案儲存到其他地方。如果您不是在編寫一個有特權的程式,那麼這點尤其需要考慮;如果您的程式沒有特權,那麼將臨時檔案放置在使用者目錄內部會更安全一些(處理 root 使用者時要當心,它以“/”作為其主目錄)。這樣,即使您沒有“正確地”建立臨時檔案,攻擊者通常也無法引發問題(因為攻擊者不能利用使用者主目錄的內容)。
但是,無法總是能夠避免使用共享目錄,所以我們需要理解如何處理 /tmp 等共享目錄。這一點非常複雜,所以它應該自己佔用一節!
共享目錄(比如 /tmp)
共享目錄基本概念
如果您可信任的程式將要與潛在的非信任使用者共享一個目錄,那麼要特別小心。在類 Unix 系統中,最常見的共享目錄是 /tmp 和 /var/tmp,對這些目錄的錯誤使用滋生了很多安全缺陷。最初建立 /tmp 目錄,是將它作為一個建立臨時檔案的方便位置,通常不應該與任何其他人共享臨時檔案。不過,該目錄很快它就有了第二個用途 —— 建立使用者間共享物件的標準位置。由於這些標準目錄有多種用途,使得作業系統難以加強訪問控制來防止攻擊;因此,您必須正確地使用它們,以避免受到攻擊。
當您使用共享目錄時,確保目錄和檔案有適當的許可權。顯然,您需要限制哪些人可以對共享目錄中建立的檔案進行讀寫操作。但是,在類 Unix 系統中,如果多個使用者都可以向同一目錄新增檔案,而且您計劃通過一個有特權的程式向該目錄新增檔案,那麼要確保為該目錄設定“sticky”位。在一個普通的目錄中(沒有 sticky 位),任何人對它都有寫許可權 —— 包括攻擊者 —— 可以刪除或者重新命名檔案,導致各種各樣的問題。例如,一個可信任的程式可以在這樣一個目錄下建立一個檔案,而一個不受信任的使用者可以刪除或者重新命名它。在類 Unix 系統上,需要設定共享目錄的 “sticky”位;在 sticky 目錄中,檔案只能由 root 或者檔案的所有者解除連結或者重新命名。/tmp 和 /var/tmp 目錄通常實現為“sticky”目錄,以排除一些問題。
程式有時會留下一些沒用的臨時檔案,所以,大部分類 Unix 系統會自動刪除特定目錄 /tmp 和 /var/tmp 下的原有臨時檔案(“tmpwatch”程式可以完成這項任務),一些程式會“自動”刪除它們所使用的特定臨時目錄下的檔案。這聽起來很方便...只可惜攻擊者可能會讓系統特別繁忙,使 活動檔案成為舊檔案。結果:系統可能會自動刪除正被使用的檔名稱。然後會發生什麼?攻擊者可能會嘗試建立他們自己的相同名稱的檔案,或者至少讓系統建立另一個程序,並重新使用相同的檔名稱。結果:混亂。這類問題叫做“tmpwatch”問題。解決這種問題的方法是,一旦自動建立了一個臨時檔案,就必須始終使用開啟該檔案時得到的檔案描述符或檔案流。永遠不要重新開啟檔案或者使用任何以檔案為引數的操作 —— 始終使用檔案描述符或者相關的流,否則,tmpwatch 競爭將引發一些問題。您甚至無法先建立檔案、然後關閉它、然後再重新開啟它,即使許可權已經限制了誰可以開啟該檔案。
攻擊 sticky 目錄以及您建立的檔案的受限許可權只是第一步。在執行安全程式期間,攻擊者可能會嘗試進行插入操作。常見的一種攻擊是,當您的程式正在執行時,在共享目錄中建立和反建立指向其他一些檔案的符號連結 —— /etc/passwd 或者 /dev/zero 是常見的目標。攻擊者的目標是,創造這樣一種情形,即讓安全程式判定某個給定的檔名並不存在,然後,攻擊者就可以建立指向另一個檔案的符號連結,而後安全程式繼續執行某些操作(但是現在,它開啟的實際上是一個意料之外的檔案)。重要的檔案經常會被這樣破壞或更改。這種做法的另一個變種是,建立和反建立攻擊者可以進行寫操作的普通檔案,這樣,攻擊者有時就可以控制有特權的程式建立的“內部”檔案。
在這些共享目錄中建立檔案時,常遇見的一個問題是,您必須確保您計劃使用的檔名在建立時並不存在,然後自動建立該檔案。在建立該檔案“之前”進行檢查沒有用,因為在已經進行了檢查但還沒有建立該檔案之前,另一個程序可以使用該檔名創建出這個檔案。使用“不可預知的”或者“惟一的”檔名也沒有用,因為攻擊者可以反覆猜測該檔名,直到成功為止。所以,您需要執行一個或者建立一個新檔案或者失敗的操作 —— 不做其他任何事情。類 Unix 系統可以這樣做,但是您需要知道如何要求系統去做。
共享目錄的解決方案
不幸的是,有很多並不是解決方案。有一些程式只是直接呼叫 mktemp(3) 或 tmpnam(3) 來建立臨時檔案,然後基於這樣做會成功的假定去簡單地開啟它。錯誤的計劃!實際上,執行緒使用 tmpnam(3) 並不可靠,也不能可靠地處理迴圈,所以永遠不要使用它。1997 年的“Single Unix Specification”推薦使用 tmpfile(3),但不幸的是,在一些老系統上實現它很不安全。
在 C 中,為了在共享(sticky)目錄中安全地建立臨時檔案,通常的解決方案是將 umask() 值設定為一個非常受限制的值,然後反覆進行以下操作:(1)建立一個“隨機的”檔名,(2)使用 O_CREAT | O_EXCL 選項 open(2) 它(這將自動建立檔案,如果沒有建立該檔案,則操作失敗),(3)當成功開啟檔案時停止重複步驟。
C 程式設計師實際上不需要直接這樣去做;只需要呼叫庫函式 mkstemp(3),就可以開啟檔案。 mkstemp(3) 的一些實現並沒有將 umask(2) 設定為一個受限的值,所以聰明的做法是先呼叫 umask(2) 來強制將檔案設定為受限的值。有一個小麻煩, mkstemp(3) 不直接支援 TMP 或 TMPDIR 環境變數,所以,如果這對您來說是重要的,那麼您必須做更多的工作。
當為了安全地開啟臨時檔案而在共享(臨時)目錄中建立檔案系統物件時,GNOME 程式設計嚮導推薦您使用下面的 C 程式碼:
清單 3. 推薦使用的建立臨時檔案的 C 程式碼
char *filename; int fd; do { filename = tempnam (NULL, "foo"); fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600); free (filename); } while (fd == -1); |
注意,儘管使用了不安全的 tempnam(3) 函式,但是它被包裝在迴圈中,並使用了 O_CREAT 和 O_EXCL 選項,從而抵消了它的安全弱點,所以這樣做是可以的。一個附帶好處是, tempnam(3) 通常使用 TMPDIR,這使得使用者可以重定向其臨時檔案,如果需要的話。注意,您需要 free() 檔名稱。完成使用之後,您應該 close() 並 unlink() 檔案。這種方法有一個小的缺點,由於可能無法安全使用 tempnam,所以各種編譯器和安全掃描器可能都會向您發出使用不合邏輯的警告。使用 mkstemp(3) 就不存在這個問題。
整個這個開啟檔案的方法展示出了標準的 C IO 庫的一個奇特之處:沒有標準的方法來指定 O_EXCL 選項使用 fopen(),所以您不能以“普通的” C 方式來開啟檔案,並安全地建立臨時檔案。如果您想使用標準 C IO 庫,然後使用 open(),那麼您可以使用指定“w+b” 模式的 fdopen(),將檔案描述符轉換為一個 FILE *。
Perl 程式設計師應該使用 File::Temp,它嘗試提供一個安全建立臨時檔案的跨平臺方法。不過,首先要仔細閱讀如何正確使用它的文件;它同樣有不安全的函式介面。我建議您顯式地將 safe_level 設定為 HIGH;這樣就會呼叫附加的安全檢查。對大部分程式設計庫來說都是如此;大部分庫都既有安全介面,又有不安全介面,所以您需要查閱文件,並確保選擇了安全的版本。
注意,在舊版的 NFS(版本 1 或者版本 2)的目錄上使用 O_EXCL 是沒有用的,因為 NFS 的這些舊版本沒有正確地支援 O_EXCL。如果您自己使用了 O_EXCL,而且共享目錄是使用這些舊版 NFS 實現的,那麼您的程式將是不安全的。實際上,關於舊版 NFS 中 link(2) 和 stat(2) 的使用有一個複雜的解決方法;如果您的程式一定要在這樣的環境中工作,那麼您可以在 Linux 的 open(2)手冊頁或者其他地方閱讀關於該方法的內容。在這裡我不準備對它進行討論,因為即使 您的程式可以與舊版的 NFS 一起使用,您所使用的很多其他程式也不會使用該解決方法。無論如何您都不可能獲得使用 NFS 版本 1 或者版本 2 的臨時目錄的安全系統,因為其他程式沒有使用該解決方法,所以,如果使用遠端掛載的臨時目錄,更明智的做法是要求使用 NFS 版本 3 或更高版本。
您可以嘗試使用 mkdtemp(3),但這通常不是一個好主意,因為臨時檔案清除器(temp cleaners)可能會決定清除它們。
如果您正在編寫 shell 指令碼,那麼可以使用管道,或者在使用者的主目錄存入臨時檔案。根本不要使用 /tmp 或 /var/tmp 目錄;普通的 shell 通常無法支援檔案描述符,所以臨時檔案清除器(tmpfile cleaners)最終將使它們失敗。如果沒有臨時檔案清除器,而且您只是必須在 /tmp 中建立臨時檔案,那麼至少要使用 mktemp(1) 來防止更明顯的攻擊,因為 mktemp(1)(不是 mktemp(3))將使用 O_EXCL 來防止典型的競爭條件攻擊。您可能做的最糟糕事情通常也是最令人不安的事:假定“$$”沒有被攻擊者猜出來,並且只是將資訊重定向到這類檔案;那麼在建立時就不會按要求使用 O_EXCL 模式。攻擊者可以簡單地預建立類似的檔案,或者反覆建立和刪除它們,最終接管程式。這樣,類似的 shell 指令碼幾乎肯定有一個嚴重的缺陷:
清單 4. 有缺陷的 shell 指令碼
echo "This is a test" > /tmp/test$$ # DON'T DO THIS. |
不要再次使用臨時檔名稱(即不要刪除和重新建立檔案),不管您最初是如何獲得“安全的”臨時檔名稱。攻擊者都有可能觀察到原始的檔名稱,並在您第二次重新建立它時非法控制它。當然,要始終使用合適的檔案許可權。
做好自己的清理工作,或者通過使用退出處理器,或者利用 UNIX 檔案系統語義,在建立後立即 unlink() 該檔案,以便在清除目錄條目的同時仍然可以訪問該檔案,直到指向它的最後一個檔案描述符被關閉。於是,您就可以在自己的程式中通過傳送檔案描述符來訪問該檔案。對程式碼維護來說,解除檔案的連結有很多好處:不管您的程式是怎樣崩潰的,檔案都會被自動刪除。它還降低了維護者不安全地使用檔名的可能性(改為使用檔案描述符)。立即解除連結也有一個小問題,即這使得管理員檢視磁碟空間使用情況的難度稍有增加。
使這些對策成為作業系統的一部分已經取得了一些成功,儘管它們當前還沒有被廣為使用。要獲得更多資料,請參閱參考資料列表中關於來自 OpenWall 專案的 Solar Designer 的 Linux 核心補丁、RaceGuard,以及 Eugene Tsyrklevich 和 Bennet Yee 的工作連結。
訊號處理
在訊號中也會發生競爭條件。程式可以註冊處理各種型別的訊號,但是訊號可能會在最不合適的時候出現,包括您正在處理另一個訊號的時候。在一個訊號處理器內部,您通常應該做的一件事是,設定一個將在以後處理的全域性標記。還有幾個操作可以在訊號中安全地完成,但不是很多,而且在處理訊號之前,您必須對它有深入的理解。那是因為只有一些系統呼叫可以安全地呼叫內部訊號:只有可重入的(re-entrant)或者不被訊號中斷的呼叫才可以被安全地呼叫。您可以呼叫庫函式,但是隻有極少數函式是被安全呼叫的;在一個訊號處理器中呼叫大部分函式是出問題的主要原因,比如 free() 或者 syslog()。要獲得更多資料,請閱讀 Michal Zalewski 的名為 “Delivering Signals for Fun and Profit”的一篇文章。但是您最好只在一個訊號處理器中設定標記(別的什麼都不做),這勝過嘗試建立複雜的處理器。
結束語
本文討論了什麼是競爭條件,以及它為什麼會導致安全問題。我們已經分析瞭如何正確地建立鎖檔案及其替代者。還研究瞭如何處理檔案系統,並重點討論瞭如何處理共享目錄,以完成在 /tmp 目錄下建立臨時檔案等一些常見任務。我們還簡要地研究了訊號處理,至少充分了解了使用它們的一種安全方法。
當然,重要的是保護您的程式不受競爭條件的破壞。但是,當前的大部分程式都不能自己完成所有的事情;它們必須向其他程式庫和程式(比如命令解釋程式和 SQL 伺服器)發出請求。攻擊程式使用的最常見方式之一就是利用這些程式向其他程式發出請求的方式。所以,接下來我們將分析如何在不暴露缺陷的同時去呼叫其他程式。
相關推薦
Linux安全程式設計:避免競爭條件
【賽迪網-IT技術報道】瞭解什麼是競爭條件,以及它們為什麼會引發安全問題。本文向您展示瞭如何在類 UNIX® (Unix-like)系統中處理常見的競爭條件,包括如何正確地建立鎖檔案、鎖檔案的替代者,如何處理檔案系統,以及如何處理共享目錄(特別是如何在 /tmp 目錄下正
Linux網路程式設計:TCP客戶/伺服器模型及基本socket函式
TCP客戶/伺服器模型 TCP連線的分組交換 在使用socket API的時候應該清楚應用程式和TCP協議棧是如何互動的: 呼叫connect()會發出SYN段(SYN是TCP報文段頭部的一個標誌位,置為1) 阻塞的read()函式返回0就表明收到了FIN段 客戶端呼叫c
Linux網路程式設計:socket程式設計簡介、網路位元組序及相關函式
Socket(套接字) socket可以看成是使用者程序與核心網路協議棧的程式設計介面(API函式)。 socket不僅可以用於本機的程序間通訊,還可以用於網路上不同主機的程序間通訊。 IPv4套接字地址結構 IPv4套接字地址結構通常也稱為“網際套接字地址結構”,它以
linux c程式設計:Posix訊息佇列
Posix訊息佇列可以認為是一個訊息連結串列. 有足夠寫許可權的執行緒可以往佇列中放置訊息, 有足夠讀許可權的執行緒可以從佇列中取走訊息 在某個程序往一個佇列寫入訊息前, 並不需要另外某個程序在該佇列上等待訊息的到達. 這跟管道和FIFO是相反的, 因為
PHP安全程式設計:session劫持的防禦session 資料暴露
GET / HTTP/1.1 Host: example.org User-Agent: Firefox/1.0 Accept: text/html, image/png, image/jpeg, image/gif, */* Cookie: PHPSESSID=1234你應該意識到請求的一致性,並把不一致的
Linux shell程式設計中的判斷條件
-b file 若檔案存在且是一個塊特殊檔案,則為真 -c file 若檔案存在且是一個字元特殊檔案,則為真 -d file 若檔案存在且是一個目錄,則為真 -e file 若檔案存在,
linux c程式設計:System V訊息佇列一
訊息佇列可以認為是一個訊息連結串列,System V 訊息佇列使用訊息佇列識別符號標識。具有足 夠特權的任何程序都可以往一個佇列放置一個訊息,具有足夠特權的任何程序都可以從一個給定佇列讀出一個訊息。在某個程序往一個佇列寫入訊息之前,並不需要另外某個程序在該佇列上等待訊息的到達。System V 訊
Linux核心程式設計:從hello world 開始-(1)_C檔案編寫
/* *File : test.c *Author : DavidLin *Date : 2014-12-07pm *Email : [email
PHP安全程式設計:防止SQL注入
SQL 注入是PHP應用中最常見的漏洞之一。事實上令人驚奇的是,開發者要同時犯兩個錯誤才會引發一個SQL注入漏洞,一個是沒有對輸入的資料進行過濾(過濾輸入),還有一個是沒有對傳送到資料庫的資料進行轉義(轉義輸出)。這兩個重要的步驟缺一不可,需要同時加以特別關注以減少程式錯誤
PHP安全程式設計:session劫持的防禦
session 資料暴露 會話資料常會包含一些個人資訊和其它敏感資料。基於這個原因,會話資料的暴露是被普遍關心的問題。一般來說,暴露的範圍不會很大,因為會話資料是儲存在伺服器環境中的,而不是在資料庫或檔案系統中。因此,會話資料自然不會公開暴露。使用SSL是一種特別有效的手段
linux 網路程式設計:epoll 的例項
在前面已經經過了PPC、TPC、select之類( TPC就是使用程序處理data,TPC就是使用執行緒處理 ),前面兩個的缺點大家應該都是知道的是吧,對於select( 其實poll和他差不多 ),缺點是能同時連線的fd是在是不多,在linux中一般是102
Linux系統程式設計:標準IO和檔案IO的區別
首先了解下什麼是標準IO以及檔案IO。 標準IO:標準I/O是ANSI C建立的一個標準I/O模型,是一個標準函式包和stdio.h標頭檔案中的定義,具有一定的可移植性。標準IO庫處理很多細節。例如快取分配,以優化長度執行IO等。標準的IO提供了三種類型的快取。
linux系統程式設計:程序原語
程序原語 1. 程序(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。 2. 程序環境 在libc中定義的全域性變數environ指
Linux網路程式設計:原始套接字的魔力【上】
原文:http://blog.chinaunix.net/uid-23069658-id-3280895.html 基於原始套接字程式設計 在開發面向連線的TCP和麵向無連線的UDP程式時,我們所關心的核心問題在於資料收發層面,資料的傳輸特性由TCP或UDP來
Linux shell程式設計:狀態變數
四大特殊狀態變數:$?、 $$、 $!、 $_ $?的作用是:獲取執行上一個指令的執行狀態返回值,返回0表示上一個命令或者程式執行成功,返回的值為非0則表示上一個命令執行失敗。 $$的作用是:獲取當前執行的shell指令碼的程序號PID。 $!的作用是:獲取上一個後臺工
PHP安全程式設計:不要暴露資料庫訪問許可權
資料庫使用中需要關注的主要問題之一是訪問許可權即使用者名稱及密碼的暴露。在程式設計中為了方便,一般都會用一個db.inc檔案儲存,如: <?php $db_user = 'myuser'; $db_pass = 'mypass'; $db_host =
Linux網路程式設計:使用select函式實現socket 收發資料
所謂的回射是指:客戶端A向服務端B傳送資料,服務端B接收到資料之後,再將接收到的資料傳送回客戶端B。所謂的迭代伺服器,
c++11多執行緒程式設計(四):資料共享和競爭條件
在多執行緒環境中,執行緒間的資料共享很簡單,但是在程式中這種簡單的資料共享可能會引起問題,其中一種便是競爭條件。什麼是競爭條件? 競賽條件是發生在多執行緒應用程式中的一種bug 當兩個或多個執行緒並行執行一組操作,訪問相同的記憶體位置,此時,它們中的一個或多個執行緒會修改記
linux網路程式設計之posix 執行緒(四):posix 條件變數與互斥鎖 示例生產者--消費者問題
#include <unistd.h>#include <sys/types.h>#include <pthread.h>#include <semaphore.h>#include <stdlib.h>#include <stdio.h>
Linux VPS 安全配置:禁用22端口、root用戶以及配置Denyhosts防暴力破解
usermod run 主題 工具 wheel mit smt 個人 連不上 最近租用了一臺Vultr東京機房的VPS,每天都會生成許多異常登錄失敗的日誌,疑似受到掃描軟件的暴力破解,遂Google了一下服務器安全防護方面的知識。 廢話不多說,下面將操作過程記錄下來: 註意