最近一直忙畢業的相關事情,加上工作,轉眼間,又到月底了,之前承諾的每月一篇博文,前幾天就一直在尋找到底要寫什麼,近兩天又突然發現有很多東西可以寫。本篇就先延續之前的一篇基於Cookie的安全檢查機制(深入C/C++之基於Cookie的安全檢查(VS2005))來介紹下另外一種在DEBUG版本下的安全檢查,也就是CheckStackVars檢查,話不多說,直接進入正題。
在VS2008下,函數的棧空間裏如果存在數組,就會自動加上CheckStackVars檢查,顧名思義,就是用來檢查局部數據是否訪問越界。相對來說,這種檢查只能起到一定的作用,並不會所有越界訪問都能檢查到,根據後面的原理介紹會瞭解到這點。既然是檢查局部的,那麼在函數內定義的static類型數組或者函數外部的全局數組並不會採用此檢查,既然是檢查數組,那麼如果函數內沒有局部數組時,此檢查也不會存在。
首先來看一個簡單的例子,驗證這個檢查的存在:
void TestVars( void )
{
int bf = 0xeeeeeeee;
char array[10] = { 0 };
int bk = 0xffffffff;
strcpy( array, "masefee" );
}
int main( void )
{
TestVars();
return 0;
}
在這個例子中,存在一個數組array,這裏刻意定義了另外兩個變量,用於看這兩個變量與數組array的內存分佈情況。這樣就能清晰的瞭解到CheckStackVars這個檢查的原理。然後來看看Debug下,TestVars函數內部的3個局部變量的內存分佈情況。斷點打在strcpy這句上,分佈如下:
ff ff ff ff cc cc cc cc cc cc cc cc 00 00 00 00 00 00 00 00 00 00cc cc cc cc cc cc cc cc cc cc ee ee ee ee
bk array bf
上面的關係已經很明確了,我們發現,在C++的代碼中看,bf、array、bk三者在內存分佈上應該是連續的,緊挨着的。但是這裏並不是這樣的,看看bf和array之間居然像個10個字節之遠。原因在於,在VS2008的debug版本下,局部變量之間並不是連續存放在棧內存裏的,而是以4字節對齊的方式,前後都會有保護字節的。這裏的保護字節佔4個字節,值爲0xcc,很明顯這是彙編指令int 3中斷的代碼字節。因此這裏bk和bf變量前後都會有4個字節的0xcc。上面綠色的部分就是,在數組array兩端也有4字節的0xcc。上面黑色加粗的部分即是,array數組一共佔10字節,要以4字節對齊,所以要補兩字節,因此多了兩個0xcc,因此導致bf和array之間相隔10字節。上面array後面緊挨着的本應該是兩個0xcc,用於補充對齊。這裏故意標識到後面去了。這裏這樣標識的意圖是爲了說明CheckStackVars這個檢查的原理。
好了,清楚了內存分佈情況,那麼CheckStackVars在什麼時間執行檢查的呢,在C++代碼上並不能顯示的看到,於是來翻翻TestVars函數的反彙編代碼:
TestVars:
004113B0 push ebp
004113B1 mov ebp,esp
004113B3 sub esp,0F0h
004113B9 push ebx
004113BA push esi
004113BB push edi
004113BC lea edi,[ebp-0F0h]
004113C2 mov ecx,3Ch
004113C7 mov eax,0CCCCCCCCh
004113CC rep stos dword ptr es:[edi]
004113CE mov eax,dword ptr [___security_cookie (417004h)]
004113D3 xor eax,ebp
004113D5 mov dword ptr [ebp-4],eax
004113D8 mov dword ptr [ebp-0Ch],0EEEEEEEEh
004113DF mov byte ptr [ebp-20h],0
004113E3 xor eax,eax
004113E5 mov dword ptr [ebp-1Fh],eax
004113E8 mov dword ptr [ebp-1Bh],eax
004113EB mov byte ptr [ebp-17h],al
004113EE mov dword ptr [ebp-2Ch],0FFFFFFFFh
004113F5 push offset string "masefee" (415804h)
004113FA lea eax,[ebp-20h]
004113FD push eax
004113FE call @ILT+160(_strcpy) (4110A5h)
00411403 add esp,8
00411406 push edx
00411407 mov ecx,ebp
00411409 push eax
0041140A lea edx,[ (411438h)]
00411410 call @ILT+130(@_RTC_CheckStackVars@8) (411087h)
00411415 pop eax
00411416 pop edx
00411417 pop edi
00411418 pop esi
00411419 pop ebx
0041141A mov ecx,dword ptr [ebp-4]
0041141D xor ecx,ebp
0041141F call @ILT+25(@__security_check_cookie@4) (41101Eh)
00411424 add esp,0F0h
0041142A cmp ebp,esp
0041142C call @ILT+320(__RTC_CheckEsp) (411145h)
00411431 mov esp,ebp
00411433 pop ebp
00411434 ret
00411435 lea ecx,[ecx]
00411438 db 01h
00411439 db 00h
0041143A db 00h
0041143B db 00h
0041143C db 40h
0041143D db 14h
0041143E db 41h
0041143F db 00h
00411440 db e0h
00411441 db ffh
00411442 db ffh
00411443 db ffh
00411444 db 0ah
00411445 db 00h
00411446 db 00h
00411447 db 00h
00411448 db 4ch
00411449 db 14h
0041144A db 41h
0041144B db 00h
0041144C db 61h
0041144D db 72h
0041144E db 72h
0041144F db 61h
00411450 db 79h
00411451 db 00h
從TestVars的反彙編代碼可以清楚的看到,黑色加粗的部分就是前一篇博文介紹的,在本篇注意看在strcpy調用之後,又調用了_RTC_CheckStackVars函數,這是一個什麼樣的函數?先來看看他的原型:
void __fastcall _RTC_CheckStackVars( void *_Esp, _RTC_framedesc *_Fd );
這是一個fastcall函數,因此兩個參數都是通過寄存器進行傳遞的。第二個參數是一個結構體類型,再來看看這個結構體的定義:
typedef struct _RTC_framedesc
{
int varCount; // 要檢查的數組的個數
_RTC_vardesc *variables; // 要檢查的數組的相關信息
} _RTC_framedesc;
這個結構體定義在rtcapi.h頭文件中的,_RTC_vardesc 也是一個結構體類型,看看定義:
typedef struct _RTC_vardesc
{
int addr; // 數組的首地址相對於EBP的偏移量
int size; // 數組的大小字節數
char *name; // 數組的名字
} _RTC_vardesc;
以上面的例子來填充這個結構體之後,結構體的數據就是:
_RTC_framedesc.varCount = 1;
_RTC_vardesc->addr = array - EBP; // 這裏array在低地址,所以addr最終爲負
_RTC_vardesc->size = 10;
_RTC_vardesc->name = "array";
好了,這下清楚了信息的存儲,再回到上面的反彙編代碼,在調用_RTC_CheckStackVars函數之前,注意紅色粗體的一句指令,將ebp賦值給了ecx寄存器,再將411438h這個地址值賦值給了edx,由於_RTC_CheckStackVars函數是fastcall,因此通過這兩個寄存器進行傳遞參數,而不是push操作。ecx就是保存的TestVars函數的棧幀,edx這個地址有點奇怪,本來是應該傳遞_RTC_framedesc結構指針的,難道這個411438h地址值就是_RTC_framedesc結構體變量所在的內存地址?從上面的反彙編代碼可以看到,下面從411438h地址開始,多了一段奇怪的數據,本應該函數下面不會有這麼一段數據的,在Debug下大多數情況都是0xcc填充的。咱們仔細觀察下這段數據,或者直接將411438h這個地址值copy到內存窗口裏看:
0x00411438 01 00 00 00 40 14 41 00 e0 ff ff ff 0a 00 00 00 4c 14 41 00 61 72 72 61 79 00
看看上面的數據,是不是就是_RTC_framedesc結構應該有的數據?答案是肯定的,紅色的部分就是_RTC_framedesc.variables指針的值,指向的位置就是緊跟其後,這是編譯器故意這麼處理的。當然可以是其它地方。這是編譯器直接把這些信息記錄在代碼段的,並且緊跟在所記錄的函數代碼之後。因此不要誤認爲這些信息是在程序執行期間才寫進去或填充的_RTC_framedesc結構。
瞭解到這裏,發現整個規則都是有理有據的,並且設計都是很良好的。也能又一次感受MS的偉大。呵呵,廢話了!
上面既然將兩個參數都給了_RTC_CheckStackVars函數,再來看看此函數內部是怎麼檢測的,看看此函數的反彙編:
_RTC_CheckStackVars:
00411500 mov edi,edi
00411502 push ebp
00411503 mov ebp,esp
00411505 push ecx
00411506 push ebx
00411507 push esi
00411508 push edi
00411509 xor edi,edi // 清零
0041150B mov esi,edx // 將_RTC_framedesc結構指針賦值給esi
0041150D cmp dword ptr [esi],edi // 比較varCount是否爲0,if( _Fd->varCount != 0 )
0041150F mov ebx,ecx // 將TestVars的棧幀賦值給ebx
00411511 mov dword ptr [i],edi // 這裏的i應該是循環變量,將數組的個數賦值給i,i = _Fd->varCount ;
00411514 jle _RTC_CheckStackVars+58h (411558h)
00411516 mov eax,dword ptr [esi+4] // +4之後就是_RTC_framedesc.variables指針
00411519 mov ecx,dword ptr [eax+edi] // _RTC_vardesc->addr了,就是數組的首地址相對於TestVars的EBP的偏移量
0041151C add eax,edi // 將eax定位到_RTC_vardesc結構首地址
0041151E cmp dword ptr [ecx+ebx-4],0CCCCCCCCh // [ecx+ebx-4]等價於ebp-addr-4,也就是array的前面4個保護字節
00411526 jne _RTC_CheckStackVars+36h (411536h) // 如果不等於0xcccccccc就報錯_RTC_StackFailure
00411528 mov edx,dword ptr [eax+4] // eax+4就是_RTC_vardesc->size,表示數組的大小
0041152B add edx,ecx // ecx當前是偏移量,加上size後就是array數組尾部相對於ebp的偏移量
0041152D cmp dword ptr [edx+ebx],0CCCCCCCCh // edx+ebx即是數組array尾部的後4個保護字節,然後比較
00411534 je _RTC_CheckStackVars+4Ah (41154Ah)
00411536 mov eax,dword ptr [esi+4] // esi+4爲_RTC_framedesc.variables指針
00411539 mov ecx,dword ptr [eax+edi+8] // eax+edi+8即是_RTC_vardesc->name,用於報錯提示
0041153D mov edx,dword ptr [ebp+4]
00411540 push ecx // 傳入越界的數組名
00411541 push edx // 傳入EBP+4的地址,此地址正是_RTC_CheckStackVars的返回地址,用於定位
00411542 call _RTC_StackFailure (4110CDh) // 調用此函數後,彈出異常MessageBox,提示哪個數組越界
00411547 add esp,8
0041154A mov eax,dword ptr [i] // 存在多個數組需要檢查時有用
0041154D inc eax
0041154E add edi,0Ch // 定位到下一個_RTC_vardesc結構
00411551 cmp eax,dword ptr [esi]
00411553 mov dword ptr [i],eax
00411556 jl _RTC_CheckStackVars+16h (411516h) // 循環
00411558 pop edi
00411559 pop esi
0041155A pop ebx
0041155B mov esp,ebp
0041155D pop ebp
0041155E ret
以上過程稍微解析得有點複雜,其主要原理就是讀取_RTC_vardesc結構,挨個對每個數組進行前後邊界檢查,如果發生更改,則調用_RTC_StackFailure函數,最後彈出錯誤信息框,信息如:
Run-Time Check Failure #2 - Stack around the variable 'array' was corrupted.
這裏需要說明一點,如果存在多個數組需要檢查時,每個數組的name是緊挨着的,同時緊接着跟在多個_RTC_vardesc結構之後,內存分佈如下:
[數組個數, _RTC_vardesc地址] [ 多個_RTC_vardesc結構(數組)][ 每個數組的name]
這些位置分佈都是編譯器直接寫在代碼裏的。
這樣就能實現簡單的邊界檢查了,前面提到了,這種檢查只是會檢查前後邊界,如果在程序中越界訪問,但是沒有修改或者寫的值就是邊界檢查的值0xcccccccc,那也不會檢測出代碼已經有越界隱患。因此最主要的還是要小心謹慎。編譯器總不能爲我們做所有的事情。以上過程會在棧內存里加上邊界檢查值,所以在Debug版本下是比較實用的。在Release下不會這麼浪費空間,因此越界就顯得更加危險了。
從上面的分析過程來看,可以寫出_RTC_CheckStackVars函數的僞代碼,如下:
這段代碼可以直接通過編譯,並起到相應的檢查功能,上面檢查失敗我這裏暫時使用的__asm int 3進行中斷,後面的註釋是真正的_RTC_CheckStackVars函數調用的錯誤函數,_RTC_StackFailure用於彈出錯誤信息和定位調試器的光標到這個返回地址。
以下代碼是用於測試這段僞代碼的功能:
上面的代碼是合法的,調用了檢查函數之後沒有任何的越界訪問,如果要測試失敗的情況,則將:
//array1[ 10 ] = 0;
//array2[ 10 ] = 0;
這兩句的註釋取消,就會在第二個__asm int 3出中斷。
以上就是CheckStackVars的所有原理,基於這種檢查機制還能發散出很多的東西,並且也可以自己實現一套規則,在一些關鍵的代碼處設置這道檢測關卡,也是非常有用的。本文到此結束,希望大家多提意見,歡迎拍磚!