安全編碼實踐一:GS編譯選項和緩存溢出

1.     概述


函數堆棧緩存溢出,是操作系統和應用程序安全漏洞最常見,最嚴重的類型之一。它往往導致可以允許攻擊者可以遠程執行惡意代碼。

例如,以下這段代碼[2,p147]就展示了Windows系統RPC調用中的函數堆棧緩存溢出類型的安全漏洞。它就是導致衝擊波病毒(Blaster)爆發的根源。
 
HRESULT GetMachineName(WCHAR *pwszPath)
{
      WCHAR wszMachineName[ N + 1 ];
      ...
      LPWSTR pwszServeName = wszMachineName;
      while (*pwszPath != L'//')
            *pwszServerName++ = *pwszPath++;
      ...
}

 
在微軟的安全開發週期模型中,專門在安全編碼實踐中推薦:對於微軟的最新C/C++編譯器,使用GS選項編譯選項,加入檢測函數堆棧緩存溢出錯誤額外代碼。

那麼,GS編譯選項的內部原理是什麼?它是如何檢測函數的堆棧緩存溢出?如何使用?本文將會深入探討這些問題。

2.     Windows 系統的堆棧結構

爲了說明/GS 編譯選項的內部原理,我們需要先從Windows 系統的堆棧結構談起。

用下面這段程序舉例:


int test( int iLen, char *pBuf);
 
int _tmain(int argc, _TCHAR* argv[])
{
    char * pBuf = "AAAAAAAAAAAAAAAAAAAA";
 
    test( 10, pBuf );
    return 0;
}
 
int test( int iLen, char *pBuf)
{
    char p[10];
    strncpy( p, pBuf, 20 );
   
    return 0;
}
 
細心的讀者也許已經看出,在執行 strncpy 會有堆棧溢出。我們後面會具體分析這一點。我們先看一下具體的彙編指令是如何操作函數調用時的堆棧的。


在 main 函數調用 test 函數時,有以下指令:

0040100b 8b45fc           mov     eax,[ebp-0x4]
0040100e 50               push    eax                             ;參數pBuf
0040100f 6a0a             push    0xa                             ;參數10
00401011 e80a000000       call    GSTest1!test (00401020)         ;調用test
00401016 83c408           add     esp,0x8
                        
 
可以看出,在函數調用時(C函數調用類型),首先
·         將參數自右向左壓棧
·         執行call 指令,將函數的返回地址(00401016)壓棧,並轉移IP(指令寄存器)爲調用函數的入口地址。

以下是堆棧上的具體數據。

0:000> dd esp
Esp值       返回地址     參數
0013fed4    00401016    0000000a 004050ec 004050ec   
 
test函數的入口指令:
GSTest1!test:
 
00401020 55               push    ebp           ;保存上層函數堆棧基址
00401021 8bec             mov     ebp,esp       ;設置當前函數堆棧基址
00401023 83ec0c           sub     esp,0xc       ;分配局部變量空間
 
test函數的出口指令:
00401035 83c40c           add     esp,0xc       ;釋放堆棧局部變量
00401038 33c0             xor     eax,eax       ;清空相應寄存器
0040103a 8be5             mov     esp,ebp       ;恢復堆棧指針
0040103c 5d               pop     ebp           ;恢復上層函數堆棧基址
0040103d c3               ret                   ;返回
 
在test函數退出時的ret指令,就會將堆棧中保存的返回地址設置到IP(指令寄存器)。這樣,程序就會從函數調用出繼續執行。


由此,我們可以總結一下Windows 系統的堆棧結構。其中包括以下數據:

調用參數
返回地址
EBP上層函數堆棧基址
異常處理代碼入口地址
(如果函數設置異常處理)
局部變量

表1:Windows系統的堆棧結構
 
3.     緩存溢出分析

瞭解了堆棧的基本結構,我們就可進一步闡述攻擊者是如何利用緩存溢出來控制代碼走向,以達到運行惡意代碼的目的。

由於函數的堆棧空間是自上向下分配的。那麼,一個緩存溢出的程序缺陷,將導致溢出部分的數據在堆棧上自下向上覆蓋。如果溢出部分的數據量足夠大的話,就可能覆蓋堆棧上的返回地址。那麼,當函數返回時,控制就不是返回到事先設定的上層函數,而是一個可以由攻擊者指定的地址。

繼續接着上面的實例分析。以下是執行strncpy的彙編指令


00401026 6a14             push    0x14          ;參數20
00401028 8b450c           mov     eax,[ebp+0xc] ;參數pBuf
0040102b 50               push    eax                
0040102c 8d4df4           lea     ecx,[ebp-0xc] ;參數p
0040102f 51               push    ecx
00401030 e80b000000       call    GSTest1!strncpy (00401040)

 
在執行strncpy前,參數p的值爲0013fec4

這時候的堆棧結構是
0:000> dd esp
 
0013feb8  0013fec4 004050ec 00000014 00403bca
0013fec8  0013fed0 00407004 0013fee4 00401016
0013fed8  0000000a 004050ec 004050ec 0013ffc0
 
因爲strncpy會從0013fec4地址處開始覆蓋20個字節的數據,超出了p數組的長度(10字節)。那麼,溢出的數據就會沿着堆棧自低向高覆蓋。

執行strncpy後的堆棧:
0:000> dd esp
 
0013feb8  0013fec4 004050ec 00000014 41414141
0013fec8  41414141 41414141 41414141 41414141
0013fed8  0000000a 004050ec 004050ec 0013ffc0

 
 
這時堆棧上保存的返回地址0x00401016已經被覆蓋爲溢出的數據0x41414141(0x41是A的ASCII代碼)。
於是,函數退出時,就會直接跳至0x41414141處運行。
0:000> r
 
eax=00000000 ebx=7ffdf000 ecx=00000000 edx=41414141 esi=00000a28
edi=00000000 eip=41414141 esp=0013fed8 ebp=41414141 iopl=0
        
 
換句話說,攻擊者可以通過控制用以覆蓋函數的返回地址的溢出數據的值,來控制程序的運行了。如果這段溢出數據是可以遠程發送的,例如是發送到網絡某個端口的數據包,那麼攻擊者就可以遠程運行惡意代碼。

4.     GS編譯選項分析


4.1堆棧的變化


GS編譯選項的原理就是在堆棧上插入一個安全cookie,以測試堆棧上的返回地址是否被修改過。安全cookie爲4個字節,在堆棧上的位置如下。


調用參數
返回地址
EBP上層函數堆棧基址
安全cookie
異常處理代碼入口地址
(如果函數設置異常處理)
局部變量

表2:GS編譯選項的堆棧結構

       那麼,如果是堆棧的局部變量發生緩存溢出的錯誤而導致返回地址被覆蓋的話,由於安全cookie所在的位置,它也一定會被覆蓋。
 

4.2函數的入口和出口代碼

GS編譯選項,對函數的入口和出口代碼都添加了針對安全cookie操作的指令。

test函數的入口指令:

GSTest1!test:
 
00401020 55               push    ebp           ;保存上層函數堆棧基址
00401021 8bec             mov     ebp,esp       ;設置當前函數堆棧基址
00401023 83ec10           sub     esp,0x10
00401026 a130704000       mov     eax,[GSTest1!__security_cookie (00407030)]
0040102b 8945fc           mov     [ebp-0x4],eax
 
首先,堆棧的空間分配從0x0c變化爲0x10,是因爲需要多分配4字節的安全cookie。增加的另外兩條指令是爲了將GSTest1!__security_cookie的值放入堆棧的安全cookie的指定位置。

這時候的堆棧結構如下:

0:000> dd esp
0013fec0  0013fee0 004013e8 0013fed0 6a915791
0013fed0  0013fee4 00401016 0000000a 004050ec

 
0x6a915791就是安全cookie,它存放在返回地址0x00401016前。

test函數的出口指令則變爲:

0040103d 83c40c           add     esp,0xc
00401040 33c0             xor     eax,eax
00401042 8b4dfc           mov     ecx,[ebp-0x4]
00401045 e85b010000       call    GSTest1!__security_check_cookie (004011a5)
0040104a 8be5             mov     esp,ebp
0040104c 5d               pop     ebp
0040104d c3               ret

 
也增加了兩條指令。首先將堆棧上的安全cookie的值放入ecx,然後調用__security_check_cookie函數來檢查其值是否被修改過。

如果一旦發現安全cookie的值被改動,那麼就會轉入異常處理,終止程序運行。這樣,即使存在緩存溢出的錯誤,GS選項也能阻止惡意代碼通過覆蓋函數的返回地址這種攻擊方式。

4.3安全cookie檢查和錯誤處理

安全cookie的檢查通過__security_check_cookie函數。它的邏輯非常簡單:


GSTest1!__security_check_cookie:
004011a5 3b0d30704000     cmp ecx,[GSTest1!__security_cookie (00407030)]

004011ab 7501             jnz  GSTest1!__security_check_cookie+0x9 (004011ae)
004011ad c3               ret
004011ae e9c1ffffff       jmp     GSTest1!report_failure

 
如果堆棧上的安全cookie的值和GSTest1!__security_cookie的值一致的話,那麼函數正常退出。否則,就會執行錯誤處理程序:跳往GSTest1!report_failure。之後,會運行__security_error_handler。如果應用程序沒有特別設定__security_error_handler,那麼缺省的錯誤處理就會彈出以下提示框並終止程序。

 

圖1:GS編譯選項的緩存溢出錯誤提示框
 
4.4如何使用GS編譯選項


以Visual Studio 2003舉例。選擇:項目配置,C/C++,Code Generation,Buffer  Security Check,就可以控制是否使用GS編譯選項來編譯程序。

圖2:使用Visual Studio 2003的GS編譯選項
 
4.5對性能的影響


從上面的分析看出,GS編譯選項會增加4個字節的堆棧的分配空間,以及在函數的入口和出口添加針對安全cookie的若干指令。那麼,它對程序的性能影響如何?

需要指出的是,GS編譯選項不是對每一個函數對設置安全cookies。Visual Studio的編譯程序首先會確定函數是否屬於 “潛在危險”的函數,例如在函數的堆棧上分配了字符串數組,就是一個特徵。只有那些被確認爲“潛在危險”函數,GS編譯選項纔會使用安全cookie。

根據Visual Studio 編譯組的數據,在絕大多數情況下,對性能的影響不超過2%【1】。

所以在微軟的安全開發週期模型中,強烈推薦使用GS編譯選項。相對於輕微的性能損失,GS編譯選項對程序的安全性的提高是非常巨大的。事實上,微軟最新的Windows Vista操作系統的開發中,就應用了GS編譯選項。

4.6侷限
 

GS編譯選項針對的是函數的棧緩存溢出(stack buffer overrun),覆蓋返回地址的攻擊方式。對於程序的其它類型的安全漏洞,它並不能提供有效保護。例如:

堆緩存溢出(heap buffer overrun)
攻擊異常處理程序
等等

所以,決不是使用了GS編譯選項就可以高枕無憂了。需要強調的是,GS編譯選項並不是消除了程序的緩存溢出安全漏洞,而是試圖在特定情況下,降低安全漏洞的危害程度。例如,即使GS編譯選項可以防止惡意代碼被遠程執行,但是程序也會異常終止。如果該程序是一個重要的服務器進程,這就會是一個典型的DOS(Deny of Service)攻擊。

通過合理的開發方式,以確保代碼中沒有安全錯誤,纔是最爲重要的。

 總結


GS編譯選項可以有效降低緩存溢出安全漏洞的危害程度。儘管它對於程序的其它類型的安全漏洞,並不能提供有效保護,作者仍然強烈推薦,在軟件開發中,使用GS編譯選項。在安全領域中,本來就沒有一個工具或技術可以包打天下,解決所有的安全問題。

 參考文獻


1.       The Compiler Security Checks In Depth,Brandon Bray, Microsoft Corporation
2.       The Security Development Lifecycle, Michael Howard, Steve Lipner

  

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