C和C++安全編碼筆記:文件I/O

C和C++程序通常會對文件進行讀寫,並將此作爲它們正常操作的一部分。不計其數的漏洞正是由這些程序與文件系統(其操作由底層操作系統定義)交互方式的不規則性而產生的。這些漏洞最常由文件的識別問題、特權管理不善,以及競爭條件導致。

8.1 文件I/O基礎:安全地執行文件I/O會是一項艱鉅的任務,一方面是因爲有這麼多的接口、操作系統和文件系統的變化。最重要的是,每種操作系統都可以用各種各樣的文件系統。

文件系統:許多UNIX和類UNIX操作系統都使用UNIX文件系統(UNIX File System, UFS)。Linux支持廣泛的文件系統,包括早期的MINIX、MS-DOS和ext2文件系統。Linux還支持較新的日誌文件系統,如ext4、日誌文件系統(Journaled File System, JFS)和ReiserFS等。此外,Linux支持加密文件系統(Cryptographic File System, CFS)和虛擬文件系統/proc。Mac OS X爲幾種不同的文件系統提供內置支持,包括Mac OS分層文件系統擴展格式(Hierarchical File System Extended Format, HFS+)、BSD標準文件系統格式(UFS),網絡文件系統(Network File System, NFS)、ISO 9660(用於CD-ROM),MS-DOS, SMB(服務器消息塊[Windows文件共享標準])、AFP(AppleTalk文件協議[Mac OS文件共享])和通用磁盤格式(Universal Disk Format, UDF)。這些文件系統中有許多,如NFS、AFS(Andrew文件系統)、Open Group DFS(分佈式文件系統),都是分佈式文件系統,它們允許用戶訪問存儲在異構的計算機中的共享文件,就像它們被存儲在本地用戶自己的硬盤驅動器一樣。

無論是C或C++標準都沒有定義目錄或分層文件系統的概念。POSIX規定:系統中的文件被組織在一個分層的結構中,其中所有的非終端節點都是目錄,而所有的終端節點都是任何其它類型的文件。

分層文件系統是常見的,雖然平面文件系統也存在。在一個具有層次結構的文件系統中,文件被組織在一個有層次的樹狀結構中,這個樹狀結構有一個不被任何其它目錄包含的根目錄,所有的非葉節點都是目錄,所有的葉節點都是其它(非目錄)文件系統。由於多個目錄條目可引用同一個文件,因此該層次結構被適當地描述爲有向非循環圖(Directed Acyclic Graph, DAG)。文件由塊(通常在磁盤上)的集合組成。在UFS中,每個文件都有一個與之關聯的固定長度的記錄,稱爲i節點(i-node),它保留所有文件屬性,並保持一個固定的地址塊數。目錄是由目錄條目的列表組成的特殊文件(special file)。目錄條目的內容包括目錄中的文件名和相關的i-節點的數量。文件都有名稱。雖然文件命名約定有所不同。通常情況下,使用一個路徑(path)名來代替一個文件名。路徑名不但包含一個文件或目錄的名稱,還包括如何瀏覽文件系統來找到該文件的信息。絕對路徑名以一個文件分割字符開始(在POSIX系統中,通常是一個正斜槓”/”,而在Windows系統中是反斜槓”\”),這意味着路徑名中的第一個文件名前面是這個進程的根目錄。在MS-DOS和Windows系統上,這個分隔字符也可以通過一個驅動器盤符(例如,C:)前導。如果路徑名不以文件分隔符開始,那麼稱它爲相對路徑名,並且路徑名中的第一個文件名前面是這個進程的當前工作目錄。多個路徑名可以解析到同一個文件。

特殊文件:包括目錄、符號鏈接、命名管道、套接字和設備文件。目錄只包含其它文件(目錄的內容)的一個列表。當用ls -l命令查看時,它們都在權限域的第一個字母上標有d。符號鏈接(symbolic link)是對其它文件的引用。這樣的引用被存儲爲文件路徑的一個文字表述。在權限字符串中,用一個l表示符號鏈接。命名管道(named pipe)使不同的進程能夠通信,並可以在文件系統中的任何地方存在。創建命名管道的命令是mkfifo,如mkfifo mypipe。它們用權限字符串中的第一個字母p來表示。套接字(socket)允許在同一臺機器上運行的兩個進程之間通信。它們用權限字符串的第一個字母s來表示。設備文件(device file)用來申請訪問權限和直接操作相應設備驅動器上的文件。字符設備只提供串行數據流的輸入和輸出(由權限字符串的第一個字母c表示)。塊設備是隨機訪問的(由一個b表示)。各命令執行結果如下圖所示:

8.2 文件I/O接口:C中的文件I/O包括在<stdio.h>中定義的所有函數。I/O操作的安全性依賴於具體的編譯器實現、操作系統和文件系統。較舊的庫與較新的版本相比,通常更容易遭受到安全漏洞攻擊。

字節或char類型的字符用於有限字符集的字符數據。字節輸入函數執行字節字符和字節字符串的輸入:fgetc()、fgets()、gets()、getchar()、fscanf()、scanf()、vfscanf()、vscanf()。

字節輸出函數執行字節字符和字節字符串的輸出:fputc()、fputs()、putc()、putchar()、fprintf()、vfprintf()、vprintf()。

字節輸入/輸出函數是ungetc()函數、字節輸入函數和字節輸出函數的並集。

寬字符或wchar_t類型字符用於自然語言的字符數據。

寬字符輸入函數執行寬字符和寬字符串的輸入:fgetwc()、fgetws()、getwc()、getwchar()、fwscanf()、wscanf()、vfwscanf()、vwscanf()。

寬字符輸出函數執行寬字符和寬字符串的輸出:fputwc()、fputws()、putwc()、putchar()、fwprintf()、wprintf()、vfwprintf()、vwprintf()。

寬字符輸入/輸出函數是ungetwc()函數、寬字符輸入函數和寬字符輸出函數的並集。因爲寬字符輸入/輸出函數更加新,它們在相應的字節輸入/輸出函數設計上進行了一些改進。

數據流:輸入和輸出被映射到邏輯數據流,這些邏輯數據流的屬性比它們所連接到的實際物理設備(如終端和結構化存儲設備支持的文件)更一致。流通過打開一個文件與一個外部文件關聯,這可能涉及創建一個新的文件。創建一個現有的文件會導致其以前的內容被丟棄。如果調用者不對哪些文件可以被打開仔細地加以限制,就可能導致現有的文件被意外覆寫,或更糟的情況,即攻擊者利用這個漏洞破壞有漏洞的系統上的文件。

通過<stdio.h>中所提供的FILE機制訪問的文件稱爲流文件。

在程序啓動時,預定義了三個文本流,並且不必明確打開它們:

(1).stdin:標準輸入(用於讀常規輸入)。

(2).stdout:標準輸出(用於常規輸出)。

(3).stderr:標準錯誤(用於寫入診斷輸出)。

文本流stdin、stdout和stderr是FILE指針類型的表達式。在最初打開時,標準錯誤流不是完全緩衝的。如果流不是一個交互設備,那麼標準輸入和標準輸出流是完全緩衝的。

打開和關閉文件:fopen(filename, mode)函數打開一個文件,其名稱是由文件名指向的字符串,並把它與流相關聯。參數mode指向一個字符串。如果該字符串是有效的,那麼該文件以指定的模式打開;否則,其行爲是未定義的。C99支持以下模式:

(1).r:打開文本文件進行讀取。

(2).w:截斷至長度爲零或創建文本文件用於寫入。

(3).a:追加;打開或創建文本文件用於在文件結束處寫入。

(4).rb:打開二進制文件進行讀取。

(5).wb:截斷至長度爲零或創建二進制文件用於寫入。

(6).ab:追加;打開或創建二進制文件用於在文件結束處寫入。

(7).r+:打開文本文件用於更新(讀取與寫入)。

(8).w+:截斷至零長度或創建文本文件用於更新。

(9).a+:追加;打開或創建文本文件用於在文件結束處更新和寫入。

(10).r+b或rb+:打開二進制文件用於更新(讀取與寫入)。

(11).w+b或wb+:截斷至長度爲零或創建二進制文件用於更新。

(12).a+b或ab+:追加;打開或創建二進制文件用於在文件結束處更新和寫入。

C11增加一個獨佔模式。如果該文件已經存在或無法創建,那麼用獨佔模式(mode參數的最後一個字符是x)打開文件失敗。否則,文件被獨佔(也稱爲非共享(nonshared))訪問式地創建,這個訪問的擴展是支持獨佔訪問的底層系統:增加這種模式解決了一個重要的安全漏洞。

(1).wx:創建獨佔文本文件用於寫入。

(2).wbx:創建獨佔的二進制文件用於寫入。

(3).w+x:創建獨佔的文本文件用於更新。

(4).w+bx或wb+x:創建獨佔的二進制文件用於更新。

調用fclose()函數來關閉文件,使得這個文件可以從控制流脫離。任何未寫入的緩存數據流被傳遞到主機環境,並被寫入到該文件中。任何未讀的緩存數據將被丟棄。關閉相關文件(包括標準文本流)後,一個指向FILE對象指針的值是不確定的。引用一個不確定的值是未定義的行爲。長度爲零的文件(在它上面沒有已寫入輸出流的字符)是否確實存在是實現定義的。關閉的文件可能隨後被相同或另一個程序的執行重新打開,並且其內容被回收或修改。如果main()函數返回到原來的調用者,或如果調用exit()函數時,所有打開的文件在程序終止之前關閉(且所有的輸出流被刷新)。其它終止程序的路徑,如調用abort()函數,不必正確地關閉所有文件。因此,尚未寫入到磁盤中的緩衝數據可能會丟失。Linux保證,甚至在程序異常終止時,這個數據也被刷新到磁盤文件。

POSIX:除了支持標準的C文件I/O函數,POSIX定義了一些自己的文件I/O函數。其中包括具有下列簽名的打開和關閉文件的函數:

int open(const char* path, into flag, …);
int close(int fildes);

open()函數並不操作FILE對象,它創建一個引用某個文件的打開文件描述(open file description),並創建一個引用該打開文件描述的文件描述符(file descriptor)。此文件描述符用於其它I/O函數,如close(),來引用該文件。

文件描述符是每一個進程爲了文件訪問的目的,用來識別一個打開的文件的唯一的非負整數。一個文件描述符的取值範圍是0~OPEN_MAX。一個進程可以同時打開不超過OPEN_MAX個文件描述符。一種常見的利用攻擊是耗盡可用的文件描述符的數量來發動拒絕服務(Dos)攻擊。打開文件描述符是一個進程或一組進程正在如何訪問文件的記錄。文件描述符只是一個標識符或句柄,它實際上並沒有描述什麼。一個打開文件描述符,包括某個文件的文件偏移量、文件狀態和文件訪問模式。

C++中的文件I/O:C++中提供與C相同的系統調用和語義,只有語法是不同的。C++的<iostream>庫包括了<cstdio>,後者是<stdio.h>的C++版本。因此,C++支持所有的C的I/O函數調用以及<iostream>對象。C++中的文件流不使用FILE,而使用ifstream處理基於文件的輸入流,用ofstream處理基於文件的輸出流,用iofstream同時處理輸入和輸出的文件流。所有這些類都繼承自fstream並操作字符(字節)。對於使用wchar_t的寬字符I/O,使用wofstream、wifstream、wiofstream、wfstream來處理。

int test_secure_coding_8_2()
{
	// 從一個文件list.txt中讀取字符數據,並將其寫入到標準輸出
#ifdef  _MSC_VER
	const char* name = "E:/GitCode/Messy_Test/testdata/list.txt";
#else
	const char* name = "testdata/list.txt";
#endif

	std::ifstream infile;
	infile.open(name, std::ifstream::in);
	if (!infile.is_open()) {
		std::cerr << "fail to open file: " << name << std::endl; //fprintf(stderr, "fail to open file: %s\n", name);
		return -1;
	}

	char c;
	while (infile >> c)
		std::cout << c; //fprintf(stdout, "%c", c);
	std::cout << std::endl; //fprintf(stdout, "\n");

	infile.close();
	return 0;
}

C++提供下列的流來操作字符(字節):

(1).cin取代stdin用於標準輸入。

(2).cout取代stdout用於標準輸出。

(3).cerr取代stderr用於無緩衝標準錯誤。

(4).clog用於緩衝標準錯誤,對記錄日誌有用。

對於寬字符流,使用wcout、wcin、wcerr、wclog。

8.3 訪問控制:不同的文件系統有不同的訪問控制模型。UFS和NFS使用的都是UNIX文件權限模型。這絕不是唯一的訪問控制模型。例如,AFS和DFS使用訪問控制列表(Access Control Model, ACL)。以下介紹UNIX文件權限模型。

權限(permission)和特權(privilege)的含義相似但有所不同,特別是在UNIX文件權限模型的上下文中。特權是通過計算機系統委派的權限。因此,特權位於用戶、用戶代理或替代,如UNIX進程中。權限是訪問資源所必要的特權,因此它與資源(如文件)相關。特權模型往往是特定於系統且複雜的。它們往往會出現”完美風暴”,在管理特權和權限中的錯誤往往直接導致安全漏洞。UNIX的設計基於大型多用戶分時系統,如Multics的思想。UNIX的訪問控制模型的基本目標是防止用戶和程序惡意(或意外)修改其他用戶的數據或操作系統的數據。UNIX系統的用戶都有一個用戶名,它是用一個用戶ID(User ID, UID)來確定的。把一個用戶名映射到一個UID所需的信息保存在/etc/passwd文件中。超級UID(root)擁有一個爲0的UID,並可以訪問任何文件。每個用戶都屬於一個組,因此也有一個組ID,或GID。用戶還可以屬於補充組。用戶提供自己的用戶名和密碼給UNIX系統作身份驗證。login程序檢查/etc/passwd或shadow文件/etc/shadow來確定用戶名是否對應到該系統上的有效用戶,並檢查提供的密碼是否與該UID所關聯的密碼對應。

UNIX文件權限:UNIX文件系統中的每個文件都有一個所有者(UID)和一個組(GID)。所有權決定了哪些用戶和進程可以訪問文件。只有文件的所有者或root可以改變其權限。這種特權不能被委派或共享。這些權限是:

(1).讀:讀一個文件或列出一個目錄的內容。

(2).寫:寫入到一個文件或目錄。

(3).執行:執行一個文件或遞歸一個目錄樹。

對於下列每種用戶類別,這些權限可以授予或撤銷:

(1).用戶:該文件的所有者。

(2).組:屬於文件的組成員的用戶。

(3).其他:不是文件的所有者或組成員的用戶。

文件權限一般都用八進制值的向量表示。在這種情況下,所有者被授予讀、寫和執行權限;該文件的組成員的用戶和其他用戶被授予讀取和執行權限。

查看權限的另一種方法是在UNIX上使用ls -l命令,如下圖所示:權限字符串的第一個字符表示文件類型:普通-、目錄d、符號鏈接l、設備b/c、套接字s或FIFO f/p。權限字符串中的其餘字符表示分配給用戶、組和其他部分的權限。這些可以是r(讀取),w(寫入),x(執行),s(set. id)或t(sticky, 粘滯)。當訪問一個文件時,進程的有效用戶ID(Effective User ID, EUID)與文件所有者的UID進行比對。如果該用戶不是所有者,那麼再對GID進行比較,然後再測試其他。

進程特權:實際用戶ID(Real User ID, RUID)是啓動該進程的用戶的ID,它與父進程的用戶ID是相同的,除非它被改變。有效用戶ID是由內核檢查權限時,使用的實際ID,因此它確定了進程的權限。如果新的進程映像文件的設置用戶ID模式位被設置,則新進程映像的EUID被設置爲新進程映射文件的用戶ID。最後,保存的設置用戶ID(Saved Set-User-ID, SSUID)是執行時設置用戶ID程序的進程映像文件的所有者ID。除了進程用戶ID,進程也有進程組ID,它基本上與進程用戶ID是對應的。每個進程都維護一個組列表,稱爲補充組ID(supplementary group ID),進程在其中有成員關係。當內核檢查組權限時此列表用於EGID。由C標準system()調用,或由POSIX的fork()和exec()系統調用從父進程繼承RUID、RGID、EUID、EGID、補充組ID以實例化進程。若要永久放棄特權,則在調用exec()之前把EUID設置爲RUID,以使提升的特權不傳遞給新程序。

更改特權:最小特權原則指出,每一個程序和系統的每一個用戶應該使用必要的特權的最小集合來完成作業。如果你的進程正在以提升的特權運行,並訪問共享目錄或用戶目錄中的文件,則你的程序就可能會被利用,使得它在程序的用戶不具有相應特權的文件上執行操作。暫時或永久刪除提升的特權使得程序在訪問文件時與非特權用戶有同樣的限制。提升的特權,可以通過把EUID設置爲RUID暫予撤銷,它使用操作系統底層權限模型來防止執行任何他們沒有權限來執行的操作。C標準沒有定義用於權限管理的API。

管理特權:”setuid程序”是有自己的執行時設置用戶ID位設置的程序。同樣,”setgid程序”也是有自己的執行時設置組ID位設置的程序。不是所有調用setuid()或setgid()的程序都是setuid或setgid程序。setuid程序可以以root身份運行或以更受限制的特權運行。非root的setuid和setgid程序通常用於執行有限或特定的任務。這些程序只限於把EUID更改爲RUID和SSUID。在可能的情況下,系統應採用這種方法設計,而不是創建設置用戶ID爲root的程序。在撤銷特權時注意正確的撤銷順序。

管理權限:進程特權管理是成功的一半,另一半則是文件權限管理。

(1).安全目錄:在大多數情況下,一個安全的目錄是指只有所有者用戶,或者可能是管理員,才能創建、重命名、刪除,或以其他方式處理文件,除此以外的其他用戶都不能執行這些操作的目錄。其他用戶可以閱讀或搜索目錄,但一般不得以任何方式修改目錄的內容。在安全目錄中進行文件操作,消除了攻擊者篡改文件或文件系統利用程序文件系統中的漏洞的可能性。要創建一個安全的目錄,必須確保目錄和它之上的所有目錄都被這個用戶或超級用戶所擁有,不能被其他用戶寫入,並且不能被任何其他用戶刪除或改名。

(2).新創建的文件權限:當一個文件被創建,權限應獨佔地限於其所有者。C標準在它們的附錄K之外沒有權限的概念,C標準和POSIX標準都沒有定義通過fopen()打開文件的默認權限。在POSIX中,操作系統存儲一個稱爲umask的值,它用來在每個進程創建新文件時代表該進程。umask可以用於禁用由創建文件時的系統調用指定的權限位。umask僅適用於文件或目錄的創建。操作系統通過計算進程請求的權限與對umask按位求反的結果做按位邏輯乘確定訪問權限。創建進程時,進程從其父進程繼承了它的umask值。通常情況下,當用戶登錄時,shell會設置一個默認的umask。

C標準fopen()函數不允許新文件使用指定的權限。無論是C標準還是POSIX標準都沒有定義文件的默認權限。大多數實現的默認值爲0666。僅有的修改此行爲的方法是在調用fopen()函數之前設置umask或在創建文件後調用fchmod()。在文件創建後使用fchmod()來改變權限不是一個好辦法,因爲它引入了競爭條件。例如,攻擊者可以在文件已經創建後但修改權限前訪問該文件。正確的做法是在創建該文件之前修改umask。C標準和POSIX標準都沒有指定這兩個函數之間的相互作用。因此,這種行爲是實現定義的,你需要在你的實現上驗證這種行爲。

C標準的附錄K”邊界檢查接口”,還定義了fopen_s()函數。該標準要求,在創建用戶寫入的文件時,fopen_s()在操作系統支持的程度,使用一種防止其他用戶訪問該文件的文件權限。u模式可以被用來創建一個具有系統默認的文件訪問權限的文件。這些與通過fopen()創建的文件權限都是相同的。

8.4 文件鑑定:許多與文件相關的安全漏洞由程序訪問一個意外的文件對象導致的,因爲文件名只鬆散地與底層的文件對象綁定。文件名沒有提供有關文件對象本身性質的信息。此外,每當在操作中使用文件名時,文件名與一個文件對象的綁定都會被重新申請。操作系統把文件描述符和FILE指針綁定到底層文件對象。

目錄遍歷:目錄內的特殊文件名”.”指的是目錄本身,”..”指的是目錄的父目錄。作爲一種特例,在根目錄中,”..”可能指的是根目錄本身。在Windows系統上,還可能提供驅動器盤符(例如C:),以及其它特殊文件名,如”…”,它相當於”../..”。當一個程序對通常由用戶提供的路徑名進行操作時,若沒有進行足夠的驗證,就會出現目錄遍歷漏洞。接受”../”形式的輸入而沒有適當的驗證,會允許攻擊者遍歷文件系統來訪問任意文件。

等價錯誤:當一個攻擊者提供不同但等效名字的資源來繞過安全檢查時,就會發生路徑等價漏洞。做到這一點的方式有很多種,其中有許多是經常被忽視的。例如,路徑名結尾的文件分割符可以繞過不期望這個字符的訪問規則,從而導致一臺服務器提供它通常不會提供的文件。等價錯誤的另一大類來自區分大小寫的問題。

符號鏈接(symbolic link):是一個方便的解決文件共享的方案。創建符號鏈接實際上創建了一個具有獨特的i-節點(i-node)的新文件。符號鏈接是特殊的文件,其中包含了實際文件的路徑名。符號鏈接是一個實際的文件,但此文件僅包含一個到另一個文件的引用,該引用存儲爲用文本表示的路徑。如果路徑名稱解析過程中遇到符號鏈接,則用符號鏈接的內容替換鏈接的名稱。

符號鏈接上的操作與普通文件操作相似,除非所有下列情況爲真:該鏈接是路徑名的最後一個組件,路徑名沒有尾隨斜線,而且函數需要在符號鏈接本身上起作用。

規範化:是一種解決方案,而不是一個問題,但只有當正確使用時纔是如此。路徑名、目錄名、文件名可能包含使驗證變得困難和不準確的字符。此外,任何路徑名組件都可以是一個符號鏈接,從而進一步掩蓋了文件的實際位置或身份。爲了簡化文件名驗證,建議把名稱翻譯成規範(canonical)形式。規範形式是某種東西的標準形式或陳述。規範化是把一個名字的等價形式解析到單個標準名稱的過程。例如,/usr/../home/rcs相當於/home/rcs,但/home/rcs是規範形式(假設/home不是一個符號鏈接)。規範化文件名,通過使名字更容易比較,使得路徑、目錄或文件名更容易驗證。規範化也使得防止文件識別漏洞,包括目錄遍歷和等價錯誤更容易。規範化也有助於驗證包含符號鏈接的路徑名,因爲規範形式不包括符號鏈接。規範化文件名是困難的,並且涉及對底層文件系統的理解。由於不同的操作系統和文件系統的規範形式可以有所不同,因此最好用操作系統的特定機制進行規範化。規範化在驗證規範路徑名的時間和打開文件的時間之間,存在一種固有的競爭條件。在這段時間內,規範的路徑名可能已經被修改,可能不再引用一個有效的文件。

在一般情況下,文件名和文件之間有一個非常寬鬆的相關性。避免基於一個路徑名、目錄名或文件名做出決策。特別是,不要因爲資源名字而相信它的屬性或使用資源的名稱用於訪問控制。不要使用文件名,而要使用基於操作系統的機制,如UNIX文件權限、訪問控制列表,或其他訪問控制技術。

Windows中的規範化問題更加複雜,由於Windows命名文件的方法很多,包括通用命名約定(UNC)共享、驅動器映射、短文件名、長文件名、Unicode名稱、特殊文件、尾隨點、正斜線、反斜槓、快捷方式,等等。最好的建議是,儘量避免完全基於路徑名、目錄名或文件名做決策。

硬鏈接:可以使用ln命令創建硬鏈接。硬鏈接無法與原目錄條目區分,但不能引用目錄或跨文件系統引用。刪除硬鏈接不會刪除文件,除非該文件的所有引用都已被刪除。引用要麼是一個硬鏈接,要麼是一個打開的文件描述符。

設備文件:不要在專用於文件的設備上執行操作。在許多操作系統中,包括Windows和UNIX,文件名可能會被用來訪問特殊的文件(special file),這些文件實際上是設備。保留的MS-DOS設備名稱包括AUX、CON、PRN、COM1、LPT1。在UNIX系統上使用的設備文件,經常應用訪問權限並在設備驅動器相應的文件上直接操作。在目的是普通字符或二進制文件的設備文件上執行操作,可能會導致崩潰和拒絕服務攻擊。當攻擊者可以用未經授權的方式訪問UNIX中的設備文件時,可能會有安全風險。在Linux上,打開設備而不是文件,可以鎖定某些應用程序。POSIX定義了O_NONBLOCK標誌用於open(),從而確保延遲操作一個文件不會使程序掛起。對於Windows系統,GetFileType()函數可以被用來確定該文件是否是一個磁盤文件。

int test_file_io_getfiletype()
{
#ifdef _MSC_VER
	const char* file_name = "E:/GitCode/Messy_Test/README.md";
	HANDLE handle = CreateFile(file_name, 0, 0, nullptr, OPEN_EXISTING, 0, nullptr);
	if (handle == INVALID_HANDLE_VALUE) {
		fprintf(stderr, "fail to CreateFile: %s\n", file_name);
		return -1;
	}

	if (GetFileType(handle) != FILE_TYPE_DISK) {
		fprintf(stderr, "it's not a disk file: %s\n", file_name);
	}

	CloseHandle(handle);
#endif
	return 0;
}

文件屬性:除了文件名,文件通常可以按其它屬性來識別,例如,通過比較文件的所有權或創建時間。已經創建和關閉的文件的有關信息可以被存儲,然後在文件被重新打開時用於驗證文件的識別。比較文件的多個屬性增加了重新打開的文件與以前曾操作的文件是相同文件的可能性。

POSIX的stat()函數可用於獲取有關某個文件的信息。fstat()函數的功能與stat()類似,但它需要一個文件描述符。你可以使用命令fstat()收集已經打開的文件的有關信息。lstat()函數的功能也與stat()類似,但如果該文件是一個符號鏈接,那麼lstat()報告鏈接的信息,而stat()報告鏈接指向的文件信息。如果stat()、fstat()和lstat()函數執行成功,它們返回0;如果發生錯誤,則返回-1。

int test_file_io_stat()
{
#ifdef _MSC_VER
	const char* file_name = "E:/GitCode/Messy_Test/testdata/list.txt";
#else
	const char* file_name = "testdata/list.txt";
#endif
	struct stat st;
	if (stat(file_name, &st) == -1) {
		fprintf(stderr, "fail to stat:\n");
		return -1;
	}

	return 0;
}

stat()所返回的結構至少包括以下成員:

(1).dev_t st_dev:包含文件的設備ID。

(2).ino_t st_ino: i-節點編號。

(3).mode_t st_mode:保護。

(4).nlink_t st_nlink:硬鏈接的數量。

(5).uid_t st_uid:所有者的用戶ID。

(6).gid_t st_gid:所有者的組ID。

(7).dev_t st_rdev:設備ID(如果是特殊文件)。

(8).off_t st_size:總字節數。

(9).blksize_t st_blksize:用於文件系統I/O的塊大小。

(10).blkcnt_t st_blocks:分配的塊數量。

(11).time_t st_atime:最後訪問時間。

(12).time_t st_mtime:最後修改時間。

(13).time_t st_ctime:最後狀態變更時間。

對於與POSIX兼容的系統上的所有文件類型,結構成員st_mtime, st_mode, st_ino, st_dev, st_uid, st_gid, st_atime, st_ctime都應該保存有意義的值。st_ino域包含文件序號。st_dev域標識包含該文件的路徑。st_ino和st_dev兩者共同唯一標識該文件。但是,重啓或系統崩潰後,st_dev值不一定是一致的,因此,如果在嘗試重新打開文件前,有系統崩潰或重啓,你可能不能夠使用此域來識別文件。

8.5 競爭條件:可以產生自受信(trusted)或非受信的(untrusted)控制流。受信的控制流包括同一程序內緊密耦合的執行線程。非受信的控制流是一個單獨的、併發執行的應用程序或進程,它們的起源往往是未知的。

任何支持多任務處理共享資源的系統,都具有源自非受信控制流的競爭條件的可能性。文件和目錄通常作爲競爭對象。一個文件在一段時間內由獨立的函數調用打開、讀取或寫入、關閉,可能重新打開的文件訪問序列,容易造成競爭窗口。打開的文件可以被同等的線程共享,而文件系統可以由獨立的進程操縱。

檢查時間和使用時間:文件I/O期間可能出現檢查時間和使用時間(Time Of Check, Time Of Use, TOCTOU)競爭條件。首先測試(檢查)某個競爭對象屬性,然後再訪問(使用)此競爭對象,TOCTOU競爭條件形成一個競爭窗口。TOCTOU漏洞可能是首先調用stat(),然後調用open(),或者它可能是一個被打開、寫入、關閉,並被一個單獨的線程重新打開的文件,或者它也可能是先調用一個access(),然後再調用fopen()。

創建而不是替換:C標準fopen()函數和POSIX open()函數都將打開一個現有的文件,如果指定的文件不存在,則創建一個新的文件。防止攻擊者在現有的文件上操作的方法之一是,僅當文件不存在時纔打開一個文件。爲了消除任何潛在的競爭條件,無論是確定該文件是否存在的測試,還是打開的操作,都必須自動進行。

獨佔訪問:由獨立的進程產生的競爭條件不能用同步原語來解決,因爲這些過程不可能訪問共享的全局數據(如一個互斥變量)。C標準附錄K,”邊界檢查接口”包括fopen_s()函數。在底層系統支持的概念的程度上,爲寫入而打開的文件以獨佔(也稱爲非共享)訪問方式打開。通過將文件當作鎖來使用,仍可以同步這類併發控制流。

文件鎖(file lock):文件或文件區域可以被鎖定,從而阻止兩個進程併發地對其進行訪問。Windows支持兩種形式的文件鎖定:共享鎖(shared lock)禁止對鎖定的文件區域的所有寫訪問,但允許所有進程的併發讀訪問;排他鎖(exclusive lock)則對鎖定的進程授予不受限制的文件訪問權,同時拒絕所有其它進程的訪問。對LockFile()的調用可獲得共享訪問;排他訪問可以經由LockFileEx()實現。在這兩種情況下,都可以通過調用UnlockFile()來移除鎖。共享鎖和排他鎖都可以消除鎖定區域中發生競爭條件的可能性。排他鎖類似於一種互斥解決方案,共享鎖則通過移除”改變鎖定的文件區域的狀態(這個競爭條件的一個必備屬性)”的可能性,以消除競爭條件。這些Windows文件鎖定機制稱爲強制性鎖(mandatory lock),因爲每一個嘗試訪問鎖定的文件區域的進程都受到限制。Linux既實現了強制性鎖,又實現了建議性鎖(advisory lock)。建議性鎖並不是由操作系統強迫實施的。

共享目錄:當兩個或更多用戶,或一組用戶都擁有對某個目錄的寫權限時,共享和欺騙的潛在風險比對幾個文件的共享訪問情況要大得多。因通過硬鏈接和符號鏈接的惡意重建所導致的漏洞告訴我們最好避免共享目錄。

程序員經常在對所有用戶都是可寫(如UNIX上的/tmp和/var/tmp目錄和Windows上的C:\TEMP)並可以定期清除(例如,每天晚上或重啓時)的目錄中創建臨時文件。臨時文件常用於輔助存儲並不需要或者不能駐留在內存中的數據,並通過文件系統傳輸數據,作爲與其它進程進行通信的一種手段。例如,一個進程用一個衆所周知的名字或一個臨時名稱在共享目錄中創建一個臨時文件,並把它傳達給合作的進程,那麼就可以使用該文件,在這些合作的進程之間共享信息。這是一個危險的做法,因爲一個在共享目錄中的衆所周知的文件很容易被攻擊者劫持或操縱。緩解策略包括以下內容:(1).使用其它低級別的IPC(進程間通信)機制,如套接字或共享內存。(2).使用更高級別的IPC機制,如遠程過程調用。(3).使用只能被應用實例(確保在同一平臺上運行的應用程序的多個實例不存在競爭)訪問的安全目錄或jail。

在共享目錄創建臨時文件沒有完全安全的方式。爲了降低風險,可以把文件創建爲具有獨特並且不可預知的文件名、僅當文件不存在時打開(原子打開)、用獨佔訪問模式打開、用適當權限打開,並在程序退出之前刪除。安全地創建臨時文件容易出錯,並且依賴於使用的C運行時庫的版本、操作系統和文件系統。不要在共享目錄中創建臨時文件。

8.6 緩解策略:

關閉競爭窗口:由於競爭條件漏洞只存在於競爭窗口期間,因此最顯而易見的緩解方案就是儘可能地消除競爭窗口。消除競爭窗口的技術:

(1).互斥緩解方案:UNIX和Windows支持很多能夠在一個多線程應用程序中實現臨界區的同步原語,包括互斥變量、信號量、管道、命名管道、條件變量、CRITICAL_SECTION(臨界區)對象以及鎖變量等。一旦識別到兩個或更多相沖突的競爭窗口,就應該將它們作爲互斥的臨界區保護起來。對同步原語的使用要求我們小心翼翼地將臨界區的大小減到最小。當競爭條件產生自不同進程時,僅當同步對象都位於共享內存並被多進程感知到,才能使用線程同步原語。在不同的進程間實現互斥的常用緩解方案是使用Windows具名的互斥體對象或POSIX命名信號。在UNIX中一個稍差的方法是用文件作爲鎖。線程間的同步可能引入死鎖的潛在威脅。

(2).線程安全的函數:在多線程應用程序中,僅僅確保應用程序自己的指令內不包含競爭條件是不夠的,被調用的函數也有可能造成競爭條件。當宣告一個函數爲線程安全的時候,就意味着作者相信這個函數可以被併發線程調用,同時該函數不會導致任何競爭條件問題。不應該假定所有函數都是線程安全的,即使是操作系統提供的API。如果必須調用一個非線程安全的函數,那麼最好將它處理爲一個臨界區,以避免與任何其它代碼調用衝突。

(3).使用原子操作:同步原語依賴於原子(不可分割的)操作。

(4).重新打開文件:重新打開一個文件流一般應避免,但對於長期運行的應用程序,這可能是必要的,以避免消耗可用文件描述符。由於文件名在每次打開時重新與文件關聯,因此無法保證重新打開的文件就是原始文件。

消除競爭對象:競爭條件的存在,部分原因是某個對象(競爭對象)被並行的執行流所共享。如果可以消除共享對象或移除對共享對象的訪問,那麼就不可能存在競爭漏洞了。

(1).同一臺計算機上任何兩個併發的執行流都可以共享訪問該計算機的設備和系統提供的形形色色的資源。其中最重要也最容易產生漏洞的共享資源是文件系統。Windows系統還擁有另一個關鍵的共享資源----註冊表。安全起見,應該爲系統資源設置最小的訪問權限,並且應該定期地安裝安全補丁。軟件開發人員也應該消除對系統資源不必要的使用,以儘量減少漏洞的暴露。在線程中,儘量少地使用全局變量、靜態變量和系統環境變量,可以將潛在的競爭對象出現的可能性降至最低。

(2).使用文件描述符,而非文件名:在一個與文件有關的競爭條件中的競爭對象通常不是文件,而是文件所在的目錄。

控制對競爭對象的訪問:競爭對象的改變狀態屬性規定,”必須至少有一個(併發的)控制流會改變競爭對象的狀態(有多個流可用對其進行訪問)”。這表明,如果很多進程只是對共享對象進行併發的讀取,那麼對象將保持不變的狀態,且不存在競爭條件。

(1).最小特權原則:有時候,可用通過減少進程的特權來消除競爭條件,而其他時候減少特權僅僅可以限制漏洞的暴露。無論如何,最小特權原則都是一種緩解競爭條件以及其它漏洞的明智策略。

(2).安全目錄:用以檢驗文件訪問權限的算法必須檢查的東西不僅僅包括文件自身的權限,還包括從父目錄開始,向上至文件系統根目錄的每一個包含目錄的權限。保證文件操作在安全目錄中執行。在大多數UNIX系統中還可以使用chroot jail技術提供安全的目錄結構。

(3).容器的虛擬化:容器提供輕量級的虛擬化技術,讓你隔離進程和資源,而不需要提供指令解釋機制和其它完全虛擬化的複雜性。容器可以被看作是jail的高級版本,它隔離文件系統、單獨的進程ID、網絡命名空間等,並限制諸如內存和CPU資源的使用。這種虛擬化形式帶來的開銷通常很小或根本沒有,因爲在虛擬分區中的程序使用操作系統的正常系統調用接口且不需要仿真或在中間的虛擬機中運行,就像全系統虛擬化技術,如VMware的情況。容器的虛擬化可用於Linux、Windows和Solaris。

(4).暴露:避免通過用戶接口或其它的API暴露你的文件系統的目錄結構或文件名。

競爭檢測工具:

(1).靜態分析:靜態分析工具並不通過實際執行軟件來進行競爭條件軟件分析。這種工具對軟件源代碼(或者,在某些情況下,二進制執行文件)進行解析,這種解析有時依賴於用戶提供的搜索信息和準則。靜態分析工具能報告那些顯而易見的競爭條件,有時還能根據可察覺的風險爲每個報告項目劃分等級。

(2).動態分析:動態分析工具通過將偵測過程與實際的程序執行相結合,克服了靜態分析工具存在的一些問題。這種方式的優勢在於可以使工具獲得真實的運行時環境。一個商業工具是來自英特爾公司的Thread Checker。Thread Checker對Linux和Windows上的C++代碼的線程競爭和死鎖執行動態分析。Helgrind是Valgrind包的工具之一。

以上代碼段的完整code見:GitHub/Messy_Test

GitHubhttps://github.com/fengbingchun/Messy_Test

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章