摘要:說明 Visual C++ 編譯器和庫在幫助開發人員提高其應用程序可靠性和安全性方面的功能。
樣例應用程序中包含六個測試,用來模擬程序員可能會犯的某些錯誤,或者模擬對未采取任何保護措施的程序進行惡意輸入所產生的某些後果:
1.通過多次運行循環程序覆蓋緩沖區。由於只定義了一個緩沖區,因此覆蓋緩沖區將影響包含返回地址的堆棧的剩余部分。
2.當另一個緩沖區也在堆棧中時覆蓋緩沖區。
3.不覆蓋緩沖區。
4.使用未初始化的變量。
5.執行可能導致丟失信息的轉換。
6.以更復雜的方式使用未初始化的變量。
其中的某些測試與 /GS 和 /RTC 選項相關。
/GS -- 緩沖區安全性檢查
如果使用 /GS 進行編譯,將在程序中插入代碼,以檢測可能覆蓋函數返回地址的緩沖區溢出。如果發生了緩沖區溢出,系統將向用戶顯示一個警告對話框,然後終止程序。這樣,攻擊者將無法控制應用程序。用戶也可以編寫自定義的錯誤處理例程,以代替默認對話框來處理錯誤。
在返回地址之前將插入一個專門的 cookie(系列字節),以使得任何緩沖區溢出都將更改該 cookie。在函數返回之前,將測試 cookie 的值。如果 cookie 值已被更改,將會調用處理程序。服務器或服務可能會要求進行不同的處理,而不是顯示一個對話框,請參閱 MSDN,以獲取有關編寫自己的處理程序的詳細信息。
此 cookie 由 C 運行庫在程序啟動時生成,攻擊者將無法知曉 cookie 值,並且在每次運行程序時,該值都不相同。由於使用了 CRT,因此 C 運行庫不會象發生誤報時那樣初始化兩次以及重復生成 cookie。
此編譯器選項適用於已發布的代碼。用於編譯樣例代碼的命令提示代碼為:
cl /O2 /ML /GS /EHsc GS-RTC.cpp
(/O2 選項將打開優化功能。它不是調試版本。)運行此命令將創建 gs-rtc.exe。
測試 1 設計用於顯示 /GS 選項的功能:
void Test1()
{
char buffer1[100];
for (int i=0 ; i < 200; i++)
{
buffer1[i] = 'a';
}
buffer1[sizeof(buffer1)-1] = 0;
cout << buffer1 << endl;
}
其中的 for 循環將執行很多次,從而使返回地址以及其他地址溢出。要運行測試 1,請使用 /GS 選項編譯程序,然後執行以下命令:
gs-rtc 1
此時將顯示以下對話框:
圖 1. 使用 /GS 選項時生成的緩沖區溢出錯誤
作為對比,下面為不使用 /GS 選項編譯相同的代碼:
cl /Od /MLd /EHsc /ZI GS-RTC.cpp /link
再次運行測試,此時系統將顯示以下對話框:
圖 2. 不使用 /GS 選項時生成的錯誤。
使用緩沖區溢出攻擊方式的攻擊者將使用精心設計、可為攻擊者提供控制能力的地址來覆蓋返回地址:此樣例僅在返回地址上寫入了多個 a(十六進制的 61)。
/GS 將無法檢測到不覆蓋返回地址、但會破壞其他內存並導致結果錯誤的溢出。請嘗試使用到目前為止所顯示的任意一條編譯命令來運行測試 2。
gs-rtc 2
雖然測試 2 覆蓋內存,但 /GS 選項卻無法檢測出來。在此代碼的優化版本中,刪除了循環語句後面的空終止符分配,因此未出現覆蓋所引起的後果。通常這種情況不會發生。
/RTC
RTC 表示運行時檢查。RTC 有若干子選項。與 /GS 不同,/RTC 設計用於調試版本,而不用於優化代碼。與 /GS 相同的是,如果您不喜歡默認對話框,可以編寫自己的處理程序。
使用 Microsoft® Visual Studio® 調試應用程序時,RTC 對話框可以為您提供在錯誤發生之處調試應用程序的選項。此外,還可以在 Visual Studio 中創建若干個配置,每個配置都包含各種選項的不同組合,例如,對發布版本使用 /GS 選項,而對調試版本使用一個或多個 /RTC 選項。
/RTCs - 堆棧幀運行時錯誤檢查
此選項在保護堆棧不被破壞方面采取了若干措施。
• 在每次調用函數時,將所有局部變量初始化為非零值。這樣可以防止以前的調用對堆棧中的值的無意使用。
• 驗證堆棧指針能夠檢查到堆棧破壞,例如,在一個位置將函數定義為 __stdcall,而在另一個位置將函數定義為 __cdecl 可導致堆棧破壞。
• 檢測局部變量的溢出和不足。這與 /GS 不同,因為它僅適用於調試版本,並且檢測緩沖區的兩端以及所有緩沖區是否遭到破壞。
cl /Od /MLd /ZI /EHsc /RTCs GS-RTC.cpp
此命令將關閉優化 (/Od),並設置 _DEBUG 預處理器定義。
使用此命令編譯樣例後,請再次嘗試運行 測試 1。此時它將會捕捉到覆蓋操作。
測試 2 說明了不包括返回地址的堆棧溢出:
void Test2()
{
char buffer1[100];
char buffer2[100];
buffer1[0] = 0;
for (int i=0 ; i <= sizeof(buffer2); i++)
{
buffer2[i] = 'a';
}
buffer2[sizeof(buffer2)-1] = 0;
cout << buffer2 << '-' << buffer1 << endl;
}
此循環使 buffer2 溢出一個字符,因為它使用 <= 代替了 <。它將覆蓋 buffer1 的第一個字符。使用 /RTCs 編譯器開關進行編譯並使用 gs-rtc 2 運行時,系統將顯示下面的對話框:
圖 3. 使用 gs-rtc 2 選項時生成的錯誤。
測試 3 對緩沖區不足進行了說明:
void Test3()
{
char buffer1[100];
char buffer2[100];
memset(buffer1,'a',sizeof(buffer1)-1);
buffer1[sizeof(buffer1)-1]=0;
memset(buffer2,'b',sizeof(buffer2)-1);
buffer2[sizeof(buffer2)-1]=0;
*(buffer1-1) = 'c';
cout << buffer1 << endl;
cout << buffer2 << endl;
}
(要運行測試 3,請使用命令“gs-rtc 3”。)在本例中,buffer2 的最後一個字節被覆蓋,並且對話框將顯示 buffer1 出現的問題。如果不進行運行時檢查,此測試的輸出結果為:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
由於這一寫入操作破壞了 buffer2 的終止符,因此,所有將 buffer2 用作空結束字符串的操作都將獲得一個長度為期望值兩倍的字符串。
測試 4 顯示了 /RTCs 選項如何將未初始化變量設置為“標志”值:
void Test4()
{
unsigned int var;
cout << hex << var;
}
在使用 /RTCs 編譯之後運行 測試 4時,將得到十六進制值 cccccccc。如果不使用 /RTCs 進行編譯,將得到堆棧中剩余的隨機值。
/RTCc - 檢測導致數據丟失的分配
此選項將插入代碼,以便在分配導致數據丟失時向您發出警報,這樣可以確保在轉換為較小類型時從不丟失數據。測試 5 說明以下內容。
void Test5(int value)
{
unsigned char ch;
ch = (unsigned char)value;
}
請執行以下命令來編譯此代碼:
cl /Od /MLd /ZI /EHsc /RTCc GS-RTC.cpp
請在命令提示符處使用其他數字來運行此命令。例如,此命令將觸發錯誤:
gs-rtc 5 300
圖 4. 在 /RTCc 開關中使用大於 255 的數值時所生成的錯誤。
無符號字符最多可容納 255 個字符,因此向程序中輸入 300 將導致數據丟失。使用 gs-rtc 5 200 再次運行命令,此時將不會出現錯誤。
如果要轉換為較小類型,並要故意丟失上面的數位,則可以使用如下所示的掩碼:
ch = (unsigned char)(value & 0xFF);
/RTCu - 報告使用了未初始化的變量
當訪問未初始化的變量時,此選項將會發出警告。測試 6 包含三個子測試,代碼如下:
void Test6(int value)
{
int uninitialized;
int var;
switch (value) {
case 3:
uninitialized = 4;
case 2:
var = 5 * uninitialized;
break;
case 1:
int *var2;
var2= &uninitialized;
var = 5 * uninitialized;
break;
}
}
(請注意,執行 case 3 中的語句後,程序將直接轉到 case 2 中執行。)使用以下命令編譯此代碼:
cl /Od /MLd /ZI /EHsc /RTCu GS-RTC.cpp
運行 gs-rtc 6 2 時,系統將顯示以下對話框:
圖 5. 使用 gs-rtc 6 2 時所生成的錯誤
運行 gs-rtc 6 3 時,系統將不顯示對話框,因為變量已被初始化。"但是,即使使用了未初始化的變量,gs-rtc 6 1 也不會出現錯誤,因為編譯器不跟蹤可以通過指針來初始化的變量。
結論
下表總結了使用每種編譯器選項的各種測試及結果。
常規編譯 /GS /RTCs /RTCu /RTCc gs-rtc 1 返回地址被破壞。程序終止(但仍可能受到緩沖區溢出攻擊)。 檢測到緩沖區溢出。程序終止。 檢測到數據破壞 不適用 不適用 gs-rtc 2 Buffer1 使用隨機字符進行了擴展,因為終止空字符被覆蓋。 Buffer1 使用隨機字符進行了擴展,因為終止空字符被覆蓋。 檢測到數據破壞 不適用 不適用 gs-rtc 3 使用 buffer1 的所有內容擴展Buffer2 使用 buffer1 的所有內容擴展Buffer2 檢測到數據破壞 不適用 不適用 gs-rtc 4 輸出隨機整數 不適用 輸出 cccccccc 不適用 不適用 gs-rtc 5 200 無 不適用 不適用 不適用 無 - 不截斷 gs-rtc 5 300 無 不適用 不適用 不適用 檢測到數據丟失 gs-rtc 6 1 無 不適用 不適用 不適用 未檢測到使用了未初始化變量 -- 使用指針屏蔽 gs-rtc 6 2 無 不適用 不適用 不適用 檢測到使用了未初始化的變量 gs-rtc 6 3 無 不適用 不適用 不適用 無,變量已經初始化