深入C/C++之基於CheckStackVars的安全檢查(VS2008)

最近一直忙畢業的相關事情,加上工作,轉眼間,又到月底了,之前承諾的每月一篇博文,前幾天就一直在尋找到底要寫什麼,近兩天又突然發現有很多東西可以寫。本篇就先延續之前的一篇基於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的所有原理,基於這種檢查機制還能發散出很多的東西,並且也可以自己實現一套規則,在一些關鍵的代碼處設置這道檢測關卡,也是非常有用的。本文到此結束,希望大家多提意見,歡迎拍磚!

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