C和C++安全編碼筆記:字符串

1. 安全概念

計算機安全(computer security):指的是阻止攻擊者通過未授權訪問或未授權使用計算機和網絡達到目的。安全包含開發和配置兩方面的元素。開發安全要求具有安全的設計和無瑕疵的實現;配置安全則要求系統和網絡被安全地予以部署以免遭攻擊。

安全策略(security policy):指系統管理員或網絡管理員爲使系統免遭威脅而設定的一些規則和操作。

安全缺陷(security flaw):指會導致潛在安全風險的軟件瑕疵(software defect)。軟件瑕疵是人類思維錯誤(包括疏忽)被編碼到軟件中的結果。並非所有軟件瑕疵都有安全風險,只有那些確實有安全風險的軟件瑕疵才稱爲安全缺陷。

漏洞(vulnerability):指允許攻擊者違反顯示或隱式的安全策略的一組條件。並非所有的安全缺陷都會導致漏洞。然而,如果一個安全缺陷會導致輸入數據(例如,命令行參數)越過安全界限進入到程序中,那該安全缺陷就會導致軟件漏洞。

軟件中的漏洞是可以利用(exploitation)的。利用的形式多種多樣,包括蠕蟲、病毒和木馬等。

利用(exploit):指藉助軟件漏洞來違反一個顯式或隱式的安全策略的軟件或技術。

緩解措施(mitigation):指能夠保護或者限制對漏洞進行利用的方法、技術、過程、工具或運行庫。緩解措施是指針對軟件缺陷的解決方案,或者用於防止軟件漏洞被利用的應急方案。緩解措施也可稱爲對策(countermeasure)或規避策略(avoidance strategy)。

2. 字符串

2.1 字符串:由一個以第一個空(null)字符作爲結束的連續字符序列組成,幷包含此空字符。一個指向字符串的指針實際上指向該字符串的起始字符。字符串長度指空字符之前的字節數,字符串的值則是它所包含的按順序排列的字符值的序列。

void calculate_array_size(int arr1[], char arr3[])
{
	int arr2[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
	fprintf(stdout, "arr2 element count: %d\n", sizeof(arr2) / sizeof(arr2[0])); // 8

	// 此處arr1是一個參數,所以它的類型是指針,在x64中,sizeof(int*)=8,在x86中,sizeof(int*)=4
	// 數組名作爲函數參數會被C語言轉換爲指針,而不是sizeof的"參數",因爲sizeof不是函數而是運算符
	// sizeof運算符在應用於聲明爲數組或函數類型的參數時,它產生調整後的(即指針)類型大小
	fprintf(stdout, "arr1 element count: %d\n", sizeof(arr1) / sizeof(arr1[0])); // 2, error
	fprintf(stdout, "sizeof(int*): %d\n", sizeof(int*)); // 8 // note: x64, not x86

	fprintf(stdout, "arr3 byte count: %d\n", strlen(arr3)); // 10
}

void string_literal()
{
	const char s1[4] = "abc"; // 不推薦,任何隨後將數組作爲一個空字節結尾的字符串的使用都會導致漏洞,因爲s1沒有正確地以空字符結尾
	const char s2[] = "abc";  // 推薦,對於一個用字符串字面值初始化的字符串,不指定它的界限,因爲編譯器會自動爲整個字符串字面值分配足夠的空間,包括終止的空字符
	fprintf(stdout, "s1 length: %d, s2 length: %d\n", strlen(s1), strlen(s2)); // 3, 3
}

void string_size()
{
	wchar_t wide_str1[] = L"0123456789";
	// 計算容納寬字符串的一個副本所需的字節數(包括終止字符)
	wchar_t* wide_str2 = (wchar_t*)malloc((wcslen(wide_str1) + 1) * sizeof(wchar_t));
	free(wide_str2);
}

int test_secure_coding_2_1()
{
	int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
	char arr3[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', '\0' };
	calculate_array_size(arr1, arr3);

	string_literal();

	char x = 'a';
	fprintf(stdout, "sizeof('a'): %d, sizeof(x): %d\n", sizeof('a'), sizeof(x)); // 1, 1

	string_size();

	return 0;
}

字符串並不是C或C++的內置類型。標準C語言庫支持的類型爲char的字符串和類型爲wchar_t的寬字符串。

字符串實現爲字符數組而且容易遭受與數組同樣的問題。

C標準允許創建指向數組對象的末元素之後加1位置的指針,雖然這些指針無法在不產生未定義行爲的狀況下解引用。

strlen()函數可以用來確定一個正確地以空字符結尾的字符串的長度,但不能用來確定一個數組的可用空間。在獲取一個數組的大小時,不要對一個指針應用sizeof運算符

執行字符集:一個字符串中的字符都屬於在執行環境中解釋的字符集。這些字符由C標準定義的一個基本字符集和一組零個或多個擴展字符(它們不是基本字符集的成員)組成。執行字符集的成員的值是具體實現定義的,但可能(例如)是美國7位ASCII字符集的值。

C使用一個語言環境(locale)的概念,它可以由setlocale()函數改變,用來跟蹤各種約定,如具體實現支持的語言和標點符號。當前語言環境確定哪些字符可用作可擴展字符。

基本執行字符集中必須存在一個字節的所有位都設置爲0的字符,稱爲空字符(null),它是用來終止字符串的。

執行字符集可能包含大量的字符,因此需要多個字節來表示擴展字符集中的一些單個字符。這就是所謂的多字節(multibyte)字符集。一個字符串,可能有時會稱爲一個多字節字符串,以強調它可能會存在多字節字符。這些與寬字符串不同,因爲在寬字符串中每個字符都具有相同的長度。

UTF-8:是一個多字節字符集,它可以表示在Unicode字符集中的每個字符,而且與美國7位ASCII字符集向後兼容。每個UTF-8字符由1~4個字節表示

UTF-8的解碼器有時會成爲一個安全漏洞。在某些情況下,攻擊者可以通過向它發送UTF-8語法不允許的一個八位字節序列,來利用一個不謹慎的UTF-8解碼器。

寬字符串:若要處理大字符集的字節,程序可以將每個字符都表示爲一個寬字符。大多數實現選擇16位或32位來表示一個寬字符。一個寬字符串是一個連續的寬字符序列,它包括並由第一個null寬字符終止。

字符串字面值:是一個包圍在雙引號中的零個或更多個字符的序列。寬字符串字面值除了以字面L作爲前綴外,其它的表示方式與字符串字面值相同。

在C中,字符串字面值的類型是一個char數組,但在C++中,它是一個const char數組。因此,一個字符串字面值在C中是可修改的。然後,如果程序試圖修改這樣的一個數組,該行爲是未定義的,因此這種行爲是禁止的。

不要試圖修改字符串字面值。原因:(1).編譯器有時會把多個相同的字符串字面值存儲在相同的地址中,這樣導致修改一個這樣的字面值可能也會改變其它字面值。(2).字符串字面值經常存儲在只讀存儲器(ROM)中。

對於字符串,一個字符串字面值指定的大小是字面值中的字符數再加上1(用於終止的空字符)。

不要指定一個用字符串字面值初始化的字符數組的界限。因爲編譯器會自動爲整個字符串字面值分配足夠的空間,包括終止的空字符。

C++中的字符串:C++標準的類模板std::basic_string,該模板代表一個字符序列。它支持序列存取操作也支持字符串操作(如搜索和串連),並且是由字符類型參數化的:

string是模板特化basic_string<char>的一個typedef。

wstring是模板特化basic_string<wchar_t>的一個typedef。

在標準C++的string類中,其內部表示並不一定非得是以空字符結尾的,雖然所有常見的實現都是以空字符結尾的。

字符類型:char、signed char和unsigned char統稱爲字符類型。編譯器可以自由地定義char,使它與signed char或unsigned char具有相同的範圍、表示方式和行爲。C標準選擇字符類型遵從如下一致的理念:(1).signed char和unsigned char適用於小整數值;(2).普通的char用於一個字符串字面值的每個元素的類型,也用於與整數的數據相對的字符數據(其中符號沒有意義)。

存儲在unsigned char類型對象中的值,保證會當作一個純粹的二進制表示法來表示屬性值。C標準定義的純粹二進制表示法爲”一種使用二進制數字0和1的整數的位置表示法,其中,其值用連續二進制位乘以從1開始的2個連續整數次冪之和表示,除非此二進制位是最高位”。unsigned char類型的對象都保證沒有填充位並因此沒有表示形式的陷阱。所以,任何類型的非二進制位域(non-bit-field)的對象都可以複製到一個unsigned char數組中(例如,通過memcpy()),並每次1個字節地檢查它們的表示形式。

計算字符串大小:對一個以空字符結尾的字節字符串,strlen()函數對終止空字節前面的字符數量進行計數(長度)。然而,寬字符可以包含空字節,尤其是從ASCII字符集獲取時。

保證字符串的存儲空間具有容納字符數據和空終結符的足夠空間

2.2 常見的字符串操作錯誤:在C和C++中,操作字符串最常見的錯誤有4種,分別是無界字符串複製(unbounded string copy)、差一錯誤(off-by-one error)、空結尾錯誤(null termination error)以及字符串截斷(string truncation)。

void test_unbounded_string_copy()
{
	char buf[12];
	std::cin >> buf; // 如果用戶輸入多於11個字符,會導致寫越界
	std::cout << "echo: " << buf << std::endl;

	std::cin.width(12); // 通過將域寬成員設置爲字符數組的長度消除了溢出
	std::cin >> buf;
	std::cout << "echo: " << buf << std::endl;
}

void test_off_by_one_error()
{
#ifdef _MSC_VER
	char s1[] = "012345678";
	char s2[] = "0123456789";

	strcpy_s(s1, sizeof(s2), s2); // error
	//char* s3 = (char*)malloc(strlen(s2) + 1); // note: when free, it will crash
	char s4[20];
	int r = strcpy_s(s4, _countof(s4), s2); // gcc version > 5.0
	fprintf(stdout, "s4: %s\n", s4);

	//char* dest = (char*)malloc(strlen(s1)); // error
	char* dest = (char*)malloc(strlen(s1) + 1);
	int i = 0;
	//for (i = 1; i <= 11; i++) // error
	for (i = 0; i < strlen(s1); ++i) {
		dest[i] = s1[i];
	}
	dest[i] = '\0';

	fprintf(stdout, "dest: %s\n", dest);
	free(dest);
#endif
}

void test_null_termination_error()
{
	char a[16], b[16], c[16];
	// No null-character is implicitly appended at the end of destination if source is longer than num.
	// Thus, in this case, destination shall not be considered a null terminated C string (reading it as such would overflow)
	//strncpy(a, "0123456789abcdef", sizeof(a)); // error, a並未以空字符結尾
	//fprintf(stdout, "a: %s\n", a); // a並未以空字符結尾,導致無法正常打印a
	strncpy(a, "0123456789abcde", sizeof(a));
	//strncpy(b, "0123456789abcdef", sizeof(b)); // error, b並未以空字符結尾
	//fprintf(stdout, "b: %s\n", b); // b並未以空字符結尾,導致無法正常打印b
	strncpy(b, "0123456789abcde", sizeof(b));
	// To avoid overflows, the size of the array pointed by destination shall be long enough to contain the 
	// same C string as source (including the terminating null character)
	strcpy(c, a); // 若a並未以空字符結尾,那麼c也未以空字符結尾,而且c可能寫得遠遠超出了數組界限,導致無法正常打印c
	fprintf(stdout, "a: %s, b: %s, c: %s\n", a, b, c);

	char d[16];
	strncpy(d, "0123456789abcdefghijk", sizeof(d) - 1);
	d[sizeof(d) - 1] = '\0';
	fprintf(stdout, "d: %s\n", d);
}

int test_secure_coding_2_2()
{
	//test_unbounded_string_copy();
	//test_off_by_one_error();
	test_null_termination_error();

	return 0;
}

無界字符串複製:發生於從源數據複製數據到一個定長的字符數組時。從無界數據源(例如stdin)讀入數據,由於事先無法得知用戶將會輸入多少個字符,因此不可能預先分配一個長度足夠的數組。常見的解決方案是靜態分配一個認爲長度遠遠大於所需的數組。不要從一個無界源複製數據到定長數組

不要使用廢棄或過時的函數

當分配的空間不足以複製一個程序的輸入(比如一個命令行參數)時,就會產生漏洞。如strcpy()、strcat()和sprintf()函數,執行無界複製操作。

snprintf()函數是一個相對安全的函數,但像其它格式的輸出函數一樣,它也容易產生格式化字符串漏洞。需要對snprintf()的返回值進行檢查,因爲函數可能會失敗,這不僅是因爲緩衝區空間不足,還有其它原因,如在函數執行過程中發生內存不足的狀況。檢測和處理輸入和輸出錯誤。檢測和處理導致未定義行爲的輸入輸出錯誤。

差一錯誤:與無界字符串複製有相似之處,即都涉及對數組的越界寫問題。

空字符結尾錯誤:一個字符串正確地以空字符結尾,是指在數組最後一個元素處或在它之前存在一個空終結符。如果一個字符串沒有以空字符結尾,程序可能會被欺騙,導致在數組邊界之外讀取或寫入數據

字符串必須在數組的最後一個元素的地址處或在它之前包含一個空終止字符,纔可以安全地作爲標準字符串處理函數如strcpy()函數或strlen()函數的參數被傳遞。空終止字符之所以是必要的,是因爲前面這些函數以及其它由C標準定義的字符串處理函數,都依賴於它的存在來標記字符串的結尾。同樣,如果程序對一個字符數組迭代循環的終止條件取決於爲字符串分配的內存是否存在一個空終止字符,字符串也必須以空字符結尾。按要求提供空字節結尾的字符串

字符串截斷:當目標字符數組的長度不足以容納一個字符串的內容時,就會發生字符串截斷。截斷通常發生於讀取用戶輸入或字符串複製時,通常是程序員試圖防止緩衝區溢出的結果。儘管沒有緩衝區溢出危害那麼大,但字符串截斷會丟失數據,有時也會導致軟件漏洞。

與函數無關的字符串錯誤:大部分在標準字符串處理庫<string.h>中定義的函數都非常容易出錯,包括strcpy、strcat、strncpy、strncat、strtok等。例如,微軟Visual Studio已經廢棄了許多這樣的函數。空字符結尾的字符串是用字符數組實現的

2.3 字符串漏洞及其利用:

緩衝區溢出(buffer overflow):實際上是一個運行時事件。當向爲某特定數據結構分配的內存空間的邊界之外寫入數據時,即會發生緩衝區溢出。

進程內存組織:進程:已載入內存並受操作系統管理的程序實例。

進程的內存一般分爲code(代碼段)、data(數據段)、heap(堆)以及stack(棧)。code和data段包含了程序的指令和只讀數據。它們可以被標記爲只讀,從而當試圖對其對應的內存進行修改時,就會引發錯誤。(把內存標記爲只讀有兩種方法,一是使用支持該功能的計算機硬件平臺的內存管理硬件,二是安排內存,使可寫的數據和只讀數據存儲在不同的頁面。)data段包含了初始化數據、未初始化數據、靜態變量以及全局變量。heap則用於動態地分配進程內存。stack是一個後進先出(last-in, first-out, LIFO)數據結構,用於支持進程的執行。

進程內存的精確組織形式依賴於操作系統、編譯器、鏈接器以及載入器----換言之,依賴於編程語言的實現。

棧管理:棧通過維護自動的進程狀態數據來支持程序的執行。要做到將程序控制返回到正確的位置,就需要將返回地址的序列存儲起來。棧很適合做這項工作。除了返回地址以外,棧還被用來保存子例程的參數以及局部(或自動)變量。幀(frame)指由函數調用引發的壓入棧的數據。當前幀的地址被存儲到幀或者基址寄存器中。幀指針在棧中是一個定點的引用。當調用一個子例程時,調用端函數的幀指針同樣被壓入棧,這樣當被調用子例程退出時,幀指針能被重新恢復。

棧溢出:當緩衝區溢出覆寫分配給執行棧內存中的數據時,就會導致棧溢出(stack smashing)。這種情況會對程序的可靠性和安全性造成嚴重的後果。stack段的緩衝區溢出使得攻擊者能夠修改自動變量的值或執行任意的代碼。

代碼注入:如果由於一個軟件缺陷導致(函數的)返回地址被覆寫,那麼被覆寫後的地址很少會指向有效的指令。結果,將控制轉移到該地址通常會引發異常並導致棧混亂。然而,攻擊者也有可能蓄意構造出一個字符串,其中包含一個指向某些惡意代碼的指針,該代碼也由攻擊者提供。當子例程返回時,控制就被轉移到了那段(惡意的)代碼。這樣,惡意代碼就會以與具有該漏洞的程序相同的權限執行。惡意代碼可以執行以其它任何形式編程所能執行的功能,不過它們通常只是簡單地在受害機器上開一個遠程shell。鑑於此,被注入的惡意代碼通常也被稱爲外殼代碼(shellcode)。

弧注入:通過修改函數返回地址的方式改變了程序的控制流。這種技術稱爲弧注入(arc injection,有時也稱爲return-into-libc),它將控制轉移到已經存在於程序內存空間中的代碼中。弧注入的利用方式是在程序的控制流”圖”中插入一段新的”弧”(表示控制流轉移),而不是進行代碼注入。

返回導向編程:該攻擊技術與弧注入是類似的,但漏洞利用代碼不是返回函數,而是返回跟在return指令後的指令序列。任何這樣的可使用的指令序列都稱爲小工具(gadget)。返回導向編程語言,由一組小工具組成。每個小工具指定要放置在棧中的某些值,供代碼段中的一個或多個指令序列使用。小工具執行良好定義的操作,如卸載、加法或跳轉。

2.4 字符串漏洞緩解策略:

void test_basic_string()
{
	std::string str;
	std::cin >> str;
	std::cout << "str: " << str << std::endl;

	// 使用迭代器編譯一個字符串的內容
	for (std::string::const_iterator it = str.cbegin(); it != str.cend(); ++it) {
		std::cout << *it;
	}
	std::cout << std::endl;
}

void test_string_reference_invalid()
{
	char input[] = "feng;bing;chun;email";
	std::string email;
	std::string::iterator loc = email.begin();
	for (int i = 0; i < strlen(input); ++i) {
		if (input[i] != ';') {
			//email.insert(loc++, input[i]); // 非法迭代器
			loc = email.insert(loc, input[i]);
		} else {
			//email.insert(loc++, ' '); // 非法迭代器
			loc = email.insert(loc, ' ');
		}
		++loc;
	}
	fprintf(stdout, "email: %s\n", email.c_str());
}

int test_secure_coding_2_4()
{
	//test_basic_string();
	test_string_reference_invalid();

	return 0;
}

字符串處理:”採用並實現一個管理字符串的一致計劃”,建議選擇一種方法來處理字符串並在項目中始終如一地執行。否則,決定權就落到了單個程序員身上,他們很可能採取不同、不一致的方法。

C11附錄K邊界檢查接口:設計目的主要是實現現有函數的更安全的替代品。例如,C11附錄K定義了strcpy_s、strcat_s、strncpy_s和strncat_s函數,分別作爲strcpy、strcat、strncpy和strncat的替代品,適用於源字符串長度未知的或保證小於已知目標緩衝區大小的情況。

C11附錄K函數是微軟爲響應許多衆所周知的安全事故而創建的,以幫助改變其現有的遺留代碼庫。這些函數已作爲ISO/IEC TR 24731-1公佈,後來又合併入C11中。(預期這樣的實現定義了__STDC_LIB_EXT1__宏。)C11附錄K是一個規範性的,但可選的附錄,你應該確保它在自己所有的目標平臺上可用。

大多數邊界檢查函數,在檢測到錯誤,如參數無效或輸出緩衝區沒有足夠的可用字節時,會調用一種特殊的運行時約束處理程序(runtime-constraint-handler)函數。此函數可能會打印一個錯誤信息和中止程序。程序員可以通過set_constraint_handler_s()函數控制哪個處理函數被調用,並可以使處理程序簡單地返回,如果需要返回的話。如果處理簡單地返回,調用處理程序的函數,用它的返回值提示它的調用者處理失敗。當調用TR24731-1定義的函數時,使用運行約束處理程序。如果把運行時約束處理程序設置爲ignore_handler_s()函數,那麼任何庫函數違反運行時約束時,都將返回到它的調用者。調用者可以在庫函數的規格說明的基礎上確定是否發生運行約束違反。約束處理程序設置在main(),以便在整個應用程序中保持一致的錯誤處理策略。

動態分配函數:由被調用者分配,由調用者釋放。ISO/IEC TR 24731-2定義了許多標準C字符串處理函數的替代品,這些替代品使用動態分配的內存,即自動調整緩衝區大小以容納所需的數據,以確保不會發生緩衝區溢出。使用這樣的函數需要引入隨後的釋放緩衝區的額外調用。

C++ std::basic_string:此類代表一個字符序列。它支持序列操作以及字符串操作,並由字符類型參數化。basic_string類使用一種動態方法,按需要爲字符串分配內存,緩衝區總是自動調整大小以容納所需的數據,通常是通過調用realloc函數。basic_string類實現了”由被調用者分配,由被調用者釋放”的內存管理策略。basic_string類比以空字符結尾的字節串更不容易有安全漏洞,但編碼錯誤仍然可能導致安全漏洞。

使字符串對象的引用失效:修改字符串的操作會使引用、指針和引用字符串對象的迭代器失效,這可能會導致錯誤。使用無效的迭代器是未定義的行爲,並可能會導致安全漏洞。雖然當操作引用字符串的範圍之外的內存時,C++一般拋出一個std::out_of_range類型的異常,但是爲了使效率最高,下標成員std::string::operator[](不執行邊界檢查)不會拋出異常。c_str()方法可以用來生成一個與字符串對象內容相同的以空字符結尾的字符序列,並把它作爲一個字符數組的指針返回。

使用basic_string的其它常見錯誤:(1).使用無效或者未初始化的迭代器;(2).傳遞出界的索引;(3).使用實際上不是一個區間的迭代器區間;(4).傳遞一個無效的迭代器位置。

2.5 字符串處理函數:

void test_fgets()
{
	char buf[10];

	if (fgets(buf, sizeof(buf), stdin)) {
		// fgets成功,掃描查找換行符
		char* p = strchr(buf, '\n');
		if (p) {
			*p = '\0';
		} else {
			// 未找到換行符,刷新stdin到行尾
			int ch;
			while (((ch = getchar()) != '\n') && !feof(stdin) && !ferror(stdin));
		}
	} else { // fgets失敗
		fprintf(stderr, "fail to fgets\n");
	}

	fprintf(stdout, "buf: %s\n", buf);
}

void test_getchar()
{
	const int BUFSIZE = 10;
	char buf[BUFSIZE];
	int ch;
	int index = 0;
	int chars_read = 0;

	while (((ch = getchar()) != '\n') && !feof(stdin) && !ferror(stdin)) {
		if (index < BUFSIZE - 1) {
			buf[index++] = (unsigned char)ch;
		}
		++chars_read;
	}

	buf[index] = '\0'; // 空終結符

	if (feof(stdin)) { fprintf(stderr, "EOF\n"); } // 處理EOF
	if (ferror(stdin)) { fprintf(stderr, "ERROR\n"); } // 處理錯誤
	if (chars_read > index) { fprintf(stderr, "truncated\n"); } // 處理截斷

	fprintf(stdout, "buf: %s\n", buf);
}

void test_gets_s()
{
#ifdef _MSC_VER
	char buf[10];
	if (gets_s(buf, sizeof(buf)) == NULL) { // 處理錯誤
		fprintf(stderr, "fail to gets_s\n");
	}

	fprintf(stdout, "buf: %s\n", buf);
#endif
}

void test_strncpy()
{
	char source[] = "http://blog.csdn.net/fengbingchun";
	char* dest = (char*)malloc(sizeof(source) + 1);
	size_t dest_size = strlen(dest);
	strncpy(dest, source, dest_size - 1);
	dest[dest_size - 1] = '\0';
	fprintf(stdout, "dest: %s\n", dest);
	free(dest);
}

void test_strncpy_s()
{
#ifdef _MSC_VER
	char src1[100] = "hello";
	char src2[7] = { 'g', 'o', 'o', 'd', 'b', 'y', 'e' };
	char dst1[6], dst2[5], dst3[5];
	errno_t r1, r2, r3;

	r1 = strncpy_s(dst1, sizeof(dst1), src1, sizeof(src1));
	fprintf(stdout, "dst1: %s, r1: %d\n", dst1, r1); // hello\0
	r2 = strncpy_s(dst2, sizeof(dst2), src2, 4);
	fprintf(stdout, "dst2: %s, r2: %d\n", dst2, r2); // good\0
	//r3 = strncpy_s(dst3, sizeof(dst3), src1, sizeof(src1)); // crash, r3並沒有返回非零值,原因應該是沒有開啓運行時約束
	//fprintf(stdout, "dst3: %s, r3: %d\n", dst3, r3);
#endif
}

int test_secure_coding_2_5()
{
	//test_fgets();
	//test_getchar();
	//test_gets_s();
	//test_strncpy();
	test_strncpy_s();

	return 0;
}

gets():永遠不要使用gets(),它不對緩衝區溢出進行任何檢測

C99:用fgets()或getchar()取代gets()。

fgets()函數:接受兩個額外的參數:期望讀入的字符數和輸入流。與gets()不同,fgets()函數保留換行符。當使用fgets()時,可能只讀取了一行的部分數據,然而,可以確定用戶的輸入是否被截斷了,因爲那樣的話,輸入緩衝區內將不會包含一個換行符。fget()函數從流中最多讀入比指定數量少1個的字符到一個數組中。如果遇到換行符或者EOF標誌,則不會繼續讀取。在最後一個字符讀入數組中後,一個空字符隨即被寫入緩衝區的結尾處。

getchar()函數:返回stdin指向的輸入流中的下一個字符。如果流在EOF處,則該流的EOF標記就會被設置,且getchar()返回EOF。如果發生讀取錯誤,則該流的錯誤標記就會被設置,且getchar()返回EOF。

C11的gets_s()函數是gets()的一個兼容且更安全的版本。它只從stdin指向的流中讀取,且不保留換行符。gets_s()函數接受一個額外的參數rsize_t,用於指定輸入的最大字符數。如果這個參數等於0或者比RSIZE_MAX更大,或者目標字符數組指針爲NULL,將產生一個錯誤條件。如果產生了錯誤條件,那麼將不會有任何的輸入動作,並且目標字符數組將不會被更改。否則,該函數最多讀入比指定數量少1的字符,並且在最後一個字符讀入數組後立即在其後加上空字符。如果gets_s()函數執行成功,則返回一個指向字符數組的指針,否則返回一個空指針。如果指定的輸入字符數超過目標緩衝區的長度,那麼gets_s()函數仍然可能導致緩衝區溢出。

strcpy()和strcat()函數:是緩衝區溢出的頻繁來源,因爲它們不允許調用者指定目標數組的大小,許多預防策略都建議使用這些函數的更安全的變種。

在C11附錄K中,strcpy_s()和strcat_s()函數被定義爲與strcpy()和strcat()函數非常接近的替代函數。strcpy_s()函數有一個額外的參數,用於給定目標數組的大小,來防止緩衝區溢出。strcpy_s()僅在源字符串可被完全複製到目標緩衝區且不引起目標緩衝區溢出的情況下才會調用成功。strcpy_s()函數執行各種運行時約束。

strcat_s()函數將源字符串中的字符追加到目標字符串的末尾,直至遇到空結束符爲止,並且追加的字符包含結尾的空字符。

在沒有正確指定目標緩衝區的最大長度的情況下,strcpy_s()和strcat_s()仍然可能會引起緩衝區溢出的問題。

strncpy()和strncat()函數:與strcpy()和strcat()函數類似,但每個函數都有一個額外的size_t類型的參數n用於限制要被複制的字符數量。這些函數可以被認爲是截斷型的複製和拼接函數。因爲strncpy()函數不能保證用空字符終止目標字符串,所有程序員必須小心,以確保目標字符串是正確地以空字符終止的,並且沒有覆蓋最後一個字符。C標準的strncpy()函數經常被推薦爲strcpy()函數”更安全”的替代品,然而,strncpy()容易發生串終止錯誤。

strncat(char* s1, const char* s2, size_t n)函數:從s2指向的數組追加不超過n個字符(空字符和它後面的字符不追加)到s1指向的字符串結尾。s2最初的字符覆蓋了s1末尾的空字符。終止空字符總是被附加到結果字符串。因此,在s1指向的數組中的最大字符數量是strlen(s1)+n+1。

必須謹慎使用strncpy()和strncat()函數,或根本不使用它們,尤其是在有更不易出錯的替代品的時候。這兩個函數都要求指定剩餘的長度而不是緩衝區的總長度。由於剩餘的長度在每次添加或刪除數據時都會改變,因此程序員必須跟蹤這些改變或重新計算剩餘長度。這個過程很容易出錯,並且可能會導致漏洞。

C11附錄K指定strncpy_s()和strncat_s()函數作爲strncpy()和strncat()很接近的替代品。

strncpy_s()函數有一個額外的參數用於給出目標數組的大小,以防止緩衝區溢出。如果發生運行時約束違反,則目標數組被設置爲空字符串,以增加問題的能見度。strncpy_s()函數返回0表示成功。

strncat_s()函數從源字符串附加不超過指定數目的連續字符(空字符後面的字符將不會被複制)到目標字符數組中。源字符串的首字符會覆蓋目標數組原來結尾的空字符。如果沒有從源字符串複製空字符,則在附加後的字符串結尾寫入一個空字符。

memcpy()和memmove():在C11附錄K中定義的memcpy_s()和memmove_s()函數,與相應的安全性較低的memcpy()和memmove()函數類似,但提供了一些額外的保障。爲了防止緩衝區溢出,memcpy_s()和memmove_s()函數具有額外的參數來指定目標數組的大小。

strlen()函數:沒有特別的缺陷,但由於底層字符串表示的弱點,它的操作可能被破壞。strlen()函數接受一個指向一個字符數組的指針,並返回終止空字符之前的字符數量。如果字符數組不是正確地以空字符結尾的,strlen()函數可能會返回一個錯誤的超大的數值,使用它時,就可能會導致漏洞。在將字符串傳遞給strlen()函數之前,有必要確保它們是正確地以空值結尾的,從而使函數的結果在預期範圍內。C11提供了一種替代strlen()的函數----帶邊界檢查的strnlen_s()函數。

2.6 運行時保護策略:

檢測和恢復的緩解策略通常要求對運行時環境做出一定的改變,以便可以在緩衝區溢出發生時對其進行檢測,以便應用程序或操作系統可以從錯誤中恢復(或者至少”安全地”失效)。

輸入驗證:任何到達某個跨越信任邊界的程序接口的數據都需要驗證。這類數據的例子包括main()函數的argv和argc參數、環境變量,以及從套接字、管道、文件、信號、共享內存和設備中讀取的數據。

對象大小檢查:GNU C編譯器(GCC)推出了__builtin_object_size()函數來獲取指針指向的對象的大小。定義了_FORTIFY_SOURCE時,GCC把strcpy()實現爲調用__builtin___strcpy_chk()的內聯函數。

Visual Studio中編譯器生成的運行時檢查:/RTCs編譯器標誌。

棧探測儀(canary):是另一種用來檢測和阻止棧溢出攻擊的機制。探測儀用於保護棧上的返回地址免遭通過內存的連續寫操作(例如,調用strcpy()所導致的結果),而不是執行一般化的邊界檢查。GCC的棧溢出保護器(Stack-Smashing Protector也被稱爲ProPolice),以及微軟的Visual C++ .NET編譯器中作爲緩衝區溢出檢測能力的那部分,都實現了探測儀。

棧溢出保護器:GCC引入了棧溢出保護(SSP)的功能,它實現了來自StackGuard的探測儀。SSP也被稱爲ProPolice,它是GCC的一個擴展,用以保護用C編寫的應用程序免遭大多數常見形式的棧緩衝區溢出的漏洞利用,它以GCC的中間語言翻譯器的形式實現。SSP特性通過GCC的命令行參數啓用,-fstack-protector和-fno-stack-protector選項可以爲帶有易受攻擊的對象(如數組)的函數打開或關閉棧溢出保護。-fstack-protector-all和-fno-stack-protector-all選項可以打開或關閉對每一個函數的保護,而不僅僅侷限於對具有字符數組的函數的保護。

檢測和恢復:地址空間佈局隨機化(Address Space Layout Randomization, ASLR)是許多操作系統的一項安全功能,其目的是爲了防止執行任意代碼。該功能對程序所使用的內存頁的地址隨機化。

不可執行棧:是一種針對緩衝區溢出的運行時解決方案,設計它的目的在於防止在棧段(stack segment)內運行可執行代碼。很多操作系統都可以被配置爲使用不可執行棧。

W^X:多種操作系統,包括OpenBSD、Windows、Linux和OS X,在內核強制減少權限,使得進程地址空間中的任何部分都不能同時既可寫又可執行。這一策略被稱爲W xor X,或更爲簡潔的W^X,並通過使用多種CPU的不執行(No eXecute, NX)位來支持這種功能。NX位使內存頁可以標記爲數據,以禁用這些頁面上的代碼的執行。

2.8 小結:

當在爲一個特定數據結構分配的內存的邊界之外寫入數據時,就會發生緩衝區溢出。緩衝區溢出在C和C++程序中司空見慣,因爲這兩種語言:(1).將字符串定義爲以空字符結尾的字符數組;(2).不進行隱式的邊界檢查;(3).提供了不執行邊界檢查的標準字符串調用庫。

常用的C和C++編譯器在編譯時並不識別可能的緩衝區溢出情形,在運行時也不會報告緩衝區溢出異常。只有當測試數據能夠引發一個可偵測的溢出時,才能使用動態分析工具來探查緩衝區溢出。

並非所有的緩衝區溢出都會導致可利用的軟件漏洞。然而,如果程序的輸入數據是由(可能懷有惡意的)用戶控制的,那麼緩衝區溢出就很可能會造成程序漏洞,從而易受攻擊。

常見的環境策略就是採用新的、提供更多安全保障的字符串操作庫。例如,C11附錄K邊界檢查接口就是爲既有函數調用設計的方便的替代品。因此,這些函數可以作爲預防性的維護措施,以減少現有遺留代碼庫產生漏洞的可能性。

運行時解決方案,例如邊界檢查、探測儀和安全庫,都具有運行時性能開銷並且可能發生衝突。例如將探測儀與安全庫一起使用並無意義,因爲它們多是以不同的方式執行或多或少相同的功能。

GitHubhttps://github.com/fengbingchun/Messy_Test

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