GCC 中的編譯器堆棧保護技術

火星人 @ 2014-03-12 , reply:0
←手機掃碼閱讀

  
以堆棧溢出為代表的緩衝區溢出攻擊已經成為一種普遍的安全漏洞和攻擊手段。本文首先對編譯器層面的堆棧保護技術作簡要介紹,然後通過實例來展示 GCC 中堆棧保護的實現方式和效果。最後介紹一些 GCC 堆棧保護的缺陷和局限。

以堆棧溢出為代表的緩衝區溢出已成為最為普遍的安全漏洞。由此引發的安全問題比比皆是。早在 1988 年,美國康奈爾大學的計算機科學系研究生莫里斯 (Morris) 利用 UNIX fingered 程序的溢出漏洞,寫了一段惡意程序並傳播到其他機器上,結果造成 6000 台 Internet 上的伺服器癱瘓,占當時總數的 10%。各種操作系統上出現的溢出漏洞也數不勝數。為了儘可能避免緩衝區溢出漏洞被攻擊者利用,現今的編譯器設計者已經開始在編譯器層面上對堆棧進行保護。現在已經有了好幾種編譯器堆棧保護的實現,其中最著名的是 StackGuard 和 Stack-smashing Protection (SSP,又名 ProPolice)。

編譯器堆棧保護原理

我們知道攻擊者利用堆棧溢出漏洞時,通常會破壞當前的函數棧。例如,攻擊者利用清單 1 中的函數的堆棧溢出漏洞時,典型的情況是攻擊者會試圖讓程序往 name 數組中寫超過數組長度的數據,直到函數棧中的返回地址被覆蓋,使該函數返回時跳轉至攻擊者注入的惡意代碼或 shellcode 處執行(關於溢出攻擊的原理參見《Linux 下緩衝區溢出攻擊的原理及對策》)。溢出攻擊后,函數棧變成了圖 2 所示的情形,與溢出前(圖 1)比較可以看出原本堆棧中的 EBP,返回地址已經被溢出字元串覆蓋,即函數棧已經被破壞。


清單 1. 可能存在溢出漏洞的代碼
int vulFunc() {      char name[10];      //…      return 0;  }  


圖 1. 溢出前的函數棧


圖 2. 溢出后的函數棧

如果能在運行時檢測出這種破壞,就有可能對函數棧進行保護。目前的堆棧保護實現大多使用基於 “Canaries” 的探測技術來完成對這種破壞的檢測。

“Canaries” 探測:

要檢測對函數棧的破壞,需要修改函數棧的組織,在緩衝區和控制信息(如 EBP 等)間插入一個 canary word。這樣,當緩衝區被溢出時,在返回地址被覆蓋之前 canary word 會首先被覆蓋。通過檢查 canary word 的值是否被修改,就可以判斷是否發生了溢出攻擊。

常見的 canary word:

  • Terminator canaries
  • 由於絕大多數的溢出漏洞都是由那些不做數組越界檢查的 C 字元串處理函數引起的,而這些字元串都是以 NULL 作為終結字元的。選擇 NULL, CR, LF 這樣的字元作為 canary word 就成了很自然的事情。例如,若 canary word 為 0x000aff0d,為了使溢出不被檢測到,攻擊者需要在溢出字元串中包含 0x000aff0d 並精確計算 canaries 的位置,使 canaries 看上去沒有被改變。然而,0x000aff0d 中的 0x00 會使 strcpy() 結束複製從而防止返回地址被覆蓋。而 0x0a 會使 gets() 結束讀取。插入的 terminator canaries 給攻擊者製造了很大的麻煩。
  • Random canaries
  • 這種 canaries 是隨機產生的。並且這樣的隨機數通常不能被攻擊者讀取。這種隨機數在程序初始化時產生,然後保存在一個未被隱射到虛擬地址空間的內存頁中。這樣當攻擊者試圖通過指針訪問保存隨機數的內存時就會引發 segment fault。但是由於這個隨機數的副本最終會作為 canary word 被保存在函數棧中,攻擊者仍有可能通過函數棧獲得 canary word 的值。
  • Random XOR canaries
  • 這種 canaries 是由一個隨機數和函數棧中的所有控制信息、返回地址通過異或運算得到。這樣,函數棧中的 canaries 或者任何控制信息、返回地址被修改就都能被檢測到了。

目前主要的編譯器堆棧保護實現,如 Stack Guard,Stack-smashing Protection(SSP) 均把 Canaries 探測作為主要的保護技術,但是 Canaries 的產生方式各有不同。下面以 GCC 為例,簡要介紹堆棧保護技術在 GCC 中的應用。





GCC 中的堆棧保護實現

Stack Guard 是第一個使用 Canaries 探測的堆棧保護實現,它於 1997 年作為 GCC 的一個擴展發布。最初版本的 Stack Guard 使用 0x00000000 作為 canary word。儘管很多人建議把 Stack Guard 納入 GCC,作為 GCC 的一部分來提供堆棧保護。但實際上,GCC 3.x 沒有實現任何的堆棧保護。直到 GCC 4.1 堆棧保護才被加入,並且 GCC4.1 所採用的堆棧保護實現並非 Stack Guard,而是 Stack-smashing Protection(SSP,又稱 ProPolice)。

SSP 在 Stack Guard 的基礎上進行了改進和提高。它是由 IBM 的工程師 Hiroaki Rtoh 開發並維護的。與 Stack Guard 相比,SSP 保護函數返回地址的同時還保護了棧中的 EBP 等信息。此外,SSP 還有意將局部變數中的數組放在函數棧的高地址,而將其他變數放在低地址。這樣就使得通過溢出一個數組來修改其他變數(比如一個函數指針)變得更為困難。

GCC 4.1 中三個與堆棧保護有關的編譯選項

-fstack-protector:

啟用堆棧保護,不過只為局部變數中含有 char 數組的函數插入保護代碼。

-fstack-protector-all:

啟用堆棧保護,為所有函數插入保護代碼。

-fno-stack-protector:

禁用堆棧保護。

GCC 中的 Canaries 探測

下面通過一個例子分析 GCC 堆棧保護所生成的代碼。分別使用 -fstack-protector 選項和 -fno-stack-protector 編譯清單2中的代碼得到可執行文件 demo_sp (-fstack-protector),demo_nosp (-fno-stack-protector)。


清單 2. demo.c
int main() {      int i;      char buffer[64];      i = 1;      buffer[0] = 'a';      return 0;  }  

然後用 gdb 分別反彙編 demo_sp,deno_nosp。


清單 3. demo_nosp 的彙編代碼
(gdb) disas main  Dump of assembler code for function main:  0x08048344 <main+0>:    lea    0x4(%esp),%ecx  0x08048348 <main+4>:    and    $0xfffffff0,%esp  0x0804834b <main+7>:    pushl  0xfffffffc(%ecx)  0x0804834e <main+10>:   push   %ebp  0x0804834f <main+11>:   mov    %esp,%ebp  0x08048351 <main+13>:   push   %ecx  0x08048352 <main+14>:   sub    $0x50,%esp  0x08048355 <main+17>:   movl   $0x1,0xfffffff8(%ebp)  0x0804835c <main+24>:   movb   $0x61,0xffffffb8(%ebp)  0x08048360 <main+28>:   mov    $0x0,%eax  0x08048365 <main+33>:   add    $0x50,%esp  0x08048368 <main+36>:   pop    %ecx  0x08048369 <main+37>:   pop    %ebp  0x0804836a <main+38>:   lea    0xfffffffc(%ecx),%esp  0x0804836d <main+41>:   ret      End of assembler dump.  


清單 4. demo_sp 的彙編代碼
(gdb) disas main  Dump of assembler code for function main:  0x08048394 <main+0>:    lea    0x4(%esp),%ecx  0x08048398 <main+4>:    and    $0xfffffff0,%esp  0x0804839b <main+7>:    pushl  0xfffffffc(%ecx)  0x0804839e <main+10>:   push   %ebp  0x0804839f <main+11>:   mov    %esp,%ebp  0x080483a1 <main+13>:   push   %ecx  0x080483a2 <main+14>:   sub    $0x54,%esp  0x080483a5 <main+17>:   mov    %gs:0x14,%eax  0x080483ab <main+23>:   mov    %eax,0xfffffff8(%ebp)  0x080483ae <main+26>:   xor    %eax,%eax  0x080483b0 <main+28>:   movl   $0x1,0xffffffb4(%ebp)  0x080483b7 <main+35>:   movb   $0x61,0xffffffb8(%ebp)  0x080483bb <main+39>:   mov    $0x0,%eax  0x080483c0 <main+44>:   mov    0xfffffff8(%ebp),%edx  0x080483c3 <main+47>:   xor    %gs:0x14,%edx  0x080483ca <main+54>:   je     0x80483d1 <main+61>  0x080483cc <main+56>:   call   0x80482fc <__stack_chk_fail@plt>  0x080483d1 <main+61>:   add    $0x54,%esp  0x080483d4 <main+64>:   pop    %ecx  0x080483d5 <main+65>:   pop    %ebp  0x080483d6 <main+66>:   lea    0xfffffffc(%ecx),%esp  0x080483d9 <main+69>:   ret      End of assembler dump.  

demo_nosp 的彙編代碼中地址為 0x08048344 的指令將 esp+4 存入 ecx,此時 esp 指向的內存中保存的是返回地址。地址為 0x0804834b 的指令將 ecx-4 所指向的內存壓棧,由於之前已將 esp+4 存入 ecx,所以該指令執行后原先 esp 指向的內容將被壓棧,即返回地址被再次壓棧。0x08048348 處的 and 指令使堆頂以 16 位元組對齊。從 0x0804834e 到 0x08048352 的指令是則保存了舊的 EBP,並為函數設置了新的棧框。當函數完成時,0x08048360 處的 mov 指令將返回值放入 EAX,然後恢復原來的 EBP,ESP。不難看出,demo_nosp 的彙編代碼中,沒有任何對堆棧進行檢查和保護的代碼。

將用 -fstack-protector 選項編譯的 demo_sp 與沒有堆棧保護的 demo_nosp 的彙編代碼相比較,兩者最顯著的區別就是在函數真正執行前多了 3 條語句:

0x080483a5 <main+17>:   mov    %gs:0x14,%eax  0x080483ab <main+23>:   mov    %eax,0xfffffff8(%ebp)  0x080483ae <main+26>:   xor    %eax,%eax  

在函數返回前又多了 4 條語句:

0x080483c0 <main+44>:   mov    0xfffffff8(%ebp),%edx  0x080483c3 <main+47>:   xor    %gs:0x14,%edx  0x080483ca <main+54>:   je     0x80483d1 <main+61>  0x080483cc <main+56>:   call   0x80482fc <__stack_chk_fail@plt>  

這多出來的語句便是 SSP 堆棧保護的關鍵所在,通過這幾句代碼就在函數棧框中插入了一個 Canary,並實現了通過這個 canary 來檢測函數棧是否被破壞。

%gs:0x14 中保存是一個隨機數,0x080483a5 到 0x080483ae 處的 3 條語句將這個隨機數放入了棧中 [EBP-8] 的位置。函數返回前 0x080483c0 到 0x080483cc 處的 4 條語句則將棧中 [EBP-8] 處保存的 Canary 取出並與 %gs:0x14 中的隨機數作比較。若不等,則說明函數執行過程中發生了溢出,函數棧框已經被破壞,此時程序會跳轉到 __stack_chk_fail 輸出錯誤消息然後中止運行。若相等,則函數正常返回。

調整局部變數的順序

以上代碼揭露了 GCC 中 canary 的實現方式。仔細觀察 demo_sp 和 demo_nosp 的彙編代碼,不難發現兩者還有一個細微的區別:開啟了堆棧保護的 semo_sp 程序中,局部變數的順序被重新組織了。

程序中, movl $0x1,0xffffff**(%ebp) 對應於 i = 1;

movb $0x61,0xffffff**(%ebp) 對應於 buffer[0] = ‘a’;

demo_nosp 中,變數 i 的地址為 0xfffffff8(%ebp),buffer[0] 的地址為 0xffffffb8(%ebp)。可見,demo_nosp 中,變數 i 在 buffer 數組之前,變數在內存中的順序與代碼中定義的順序相同,見圖 3 左。而在 demo_sp 中,變數 i 的地址為 0xffffffb4 (%ebp),buffer[0] 的地址為 0xffffffb8(%ebp),即 buffer 數組被挪到了變數 i 的前面,見圖 3 右。


圖 3. 調整變數順序前後的函數棧

demo_sp 中局部變數的組織方式對防禦某些溢出攻擊是有益的。如果數組在其他變數之後(圖 3 左),那麼即使返回地址受到 canary 的保護而無法修改,攻擊者也可能通過溢出數組來修改其他局部變數(如本例中的 int i)。當被修改的其他局部變數是一個函數指針時,攻擊者就很可能利用這種溢出,將函數指針用 shellcode 的地址覆蓋,從而實施攻擊。然而如果用圖 3 右的方式來組織堆棧,就會給這類溢出攻擊帶來很大的困難。





GCC 堆棧保護效果

以上我們從實現的角度分析了 GCC 中的堆棧保護。下面將用一個小程序 overflow_test.c 來驗證 GCC 堆棧保護的實際效果。


清單 5. 溢出攻擊模擬程序 overflow_test.c
#include <stdio.h>  #include <stdlib.h>  char shellcode[] =      "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"      "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"      "\x80\xe8\xdc\xff\xff\xff/bin/sh";    int test()  {      int i;      unsigned int stack[10];      char my_str[16];      printf("addr of shellcode in decimal: %d\n", &shellcode);      for (i = 0; i < 10; i++)          stack[i] = 0;        while (1) {          printf("index of item to fill: (-1 to quit): ");          scanf("%d",&i);          if (i == -1) {              break;          }          printf("value of item[%d]:", i);          scanf("%d",&stack[i]);      }        return 0;  }    int main()  {      test();      printf("Overflow Failed\n");        return 0;  }  

該程序不是一個實際的漏洞程序,也不是一個攻擊程序,它只是通過模擬溢出攻擊來驗證 GCC 堆棧保護的一個測試程序。它首先會列印出 shellcode 的地址,然後接受用戶的輸入,為 stack 數組中指定的元素賦值,並且不會對數組邊界進行檢查。

關閉堆棧保護,編譯程序

aktoon@aktoon-thinkpad:~/SCAD/overflow_test$ gcc –fno-stack-protector -o   overflow_test ./overflow_test.c  

不難算出關閉堆棧保護時,stack[12] 指向的位置就是棧中存放返回地址的地方。在 stack [10],stack[11],stack[12] 處填入 shellcode 的地址來模擬通常的溢出攻擊:

aktoon@aktoon-thinkpad:~/SCAD/overflow_test$ ./overflow_test   addr of shellcode in decimal: 134518560  index of item to fill: (-1 to quit): 10  value of item[11]: 134518560  index of item to fill: (-1 to quit): 11  value of item[11]: 134518560  index of item to fill: (-1 to quit): 12  value of item[12]:134518560  index of item to fill: (-1 to quit): -1  $ ps    PID TTY          TIME CMD  15035 pts/4    00:00:00 bash  29757 pts/4    00:00:00 sh  29858 pts/4    00:00:00 ps  

程序被成功溢出轉而執行 shellcode 獲得了一個 shell。由於沒有開啟堆棧保護,溢出得以成功。

然後開啟堆棧保護,再次編譯並運行程序。

aktoon@aktoon-thinkpad:~/SCAD/overflow_test$ gcc –fno-stack-protector -o   overflow_test ./overflow_test.c  

通過 gdb 反彙編,不難算出,開啟堆棧保護后,返回地址位於 stack[17] 的位置,而 canary 位於 stack[16] 處。在 stack[10],stack[11]…stack[17] 處填入 shellcode 的地址來模擬溢出攻擊:

aktoon@aktoon-thinkpad:~/SCAD/overflow_test$ ./overflow_test   addr of shellcode in decimal: 134518688  index of item to fill: (-1 to quit): 10  value of item[11]: 134518688  index of item to fill: (-1 to quit): 11  value of item[11]: 134518688  index of item to fill: (-1 to quit): 12  value of item[11]: 134518688  index of item to fill: (-1 to quit): 13  value of item[11]: 134518688  index of item to fill: (-1 to quit): 14  value of item[11]: 134518688  index of item to fill: (-1 to quit): 15  value of item[11]: 134518688  index of item to fill: (-1 to quit): 16  value of item[12]: 134518688  index of item to fill: (-1 to quit): 17  value of item[12]: 134518688  index of item to fill: (-1 to quit): -1  Overflow Failed  *** stack smashing detected ***: ./overflow_test terminated  Aborted  

這次溢出攻擊失敗了,提示 ”stack smashing detected”,表明溢出被檢測到了。按照之前對 GCC 生成的堆棧保護代碼的分析,失敗應該是由於 canary 被改變引起的。通過反彙編和計算我們已經知道返回地址位於 stack[17],而 canary 位於 stack[16]。接下來嘗試繞過 canary,只對返回地址進行修改。

aktoon@aktoon-thinkpad:~/SCAD/overflow_test$ ./overflow_test   addr of shellcode in decimal: 134518688  index of item to fill: (-1 to quit): 17  value of item[17]:134518688  index of item to fill: (-1 to quit): -1  $ ls *.c  bypass.c  exe.c  exp1.c  of_demo.c  overflow.c  overflow_test.c  toto.c  vul1.c  $  

這次只把 stack[17] 用 shellcode 的地址覆蓋了,由於沒有修改 canary,返回地址的修改沒有被檢測到,shellcode 被成功執行了。同樣的道理,即使我們沒有修改函數的返回地址,只要 canary 被修改了(stack[16]),程序就會被保護代碼中止。

aktoon@aktoon-thinkpad:~/SCAD/overflow_test$ ./overflow_test   addr of shellcode in decimal: 134518688  index of item to fill: (-1 to quit): 16  value of item[16]:134518688  index of item to fill: (-1 to quit): -1  Overflow Failed  *** stack smashing detected ***: ./overflow_test terminated  Aborted  

在上面的測試中,我們看到編譯器的插入的保護代碼阻止了通常的溢出攻擊。實際上,現在編譯器堆棧保護技術確實使堆棧溢出攻擊變得困難了。





GCC 堆棧保護的局限

在上面的例子中,我們發現假如攻擊者能夠購繞過 canary,仍然有成功實施溢出攻擊的可能。除此以外,還有一些其他的方法能夠突破編譯器的保護,當然這些方法需要更多的技巧,應用起來也較為困難。下面對突破編譯器堆棧保護的方法做一簡介。

Canary 探測方法僅對函數堆中的控制信息 (canary word, EBP) 和返回地址進行了保護,沒有對局部變數進行保護。通過溢出覆蓋某些局部變數也可能實施溢出攻擊。此外,Stack Guard 和 SSP 都只提供了針對棧的溢出保護,不能防禦堆中的溢出攻擊。

在某些情況下,攻擊者還可以利用函數參數來實現溢出攻擊。我們用下面的例子來說明這種攻擊的原理。


清單 6. 漏洞代碼 vul.c
int func(char *msg) {      char buf[80];      strcpy(buf,msg);      strcpy(msg,buf);  }  int main(int argv, char** argc) {      func(argc[1]);  }  

運行時,func 函數的棧框如下圖所示。


圖 4. func 的函數棧

通過 strcpy(buf,msg),我們可以將 buf 數組溢出,直至將參數 msg 覆蓋。接下來的 strcpy(msg,buf) 會向 msg 所指向的內存中寫入 buf 中的內容。由於第一步的溢出中,我們已經控制了 msg 的內容,所以實際上通過上面兩步我們可以向任何不受保護的內存中寫入任何數據。雖然在以上兩步中,canaries 已經被破壞,但是這並不影響我們完成溢出攻擊,因為針對 canaries 的檢查只在函數返回前才進行。通過構造合適的溢出字元串,我們可以修改內存中程序 ELF 映像的 GOT(Global Offset Table)。假如我們通過溢出字元串修改了 GOT 中 _exit() 的入口,使其指向我們的 shellcode,當函數返回前檢查到 canary 被修改後,會提示出錯並調用 _exit() 中止程序。而此時的的 _exit() 已經指向了我們的 shellcode,所以程序不會退出,並且 shellcode 會被執行,這樣就達到了溢出攻擊的目的。

上面的例子展示了利用參數避開保護進行溢出攻擊的原理。此外,由於返回地址是根據 EBP 來定位的,即使我們不能修改返回地址,假如我們能夠修改 EBP 的值,那麼就修改了存放返回地址的位置,相當於間接的修改了返回地址。可見,GCC 的堆棧保護並不是萬能的,它仍有一定的局限性,並不能完全杜絕堆棧溢出攻擊。雖然面對編譯器的堆棧保護,我們仍可能有一些技巧來突破這些保護,但是這些技巧通常受到很多條件的制約,實際應用起來有一定的難度。





結束語

本文介紹了編譯器所採用的以 Canaries 探測為主的堆棧保護技術,並且以 GCC 為例展示了 SSP 的實現方式和實際效果。最後又簡單介紹了突破編譯器保護的一些方法。儘管攻擊者仍能通過一些技巧來突破編譯器的保護,但編譯器加入的堆棧保護機制確實給溢出攻擊造成了很大的困難。本文僅從編譯器的角度討論了防禦溢出攻擊的方法。要真正防止堆棧溢出攻擊,單從編譯器入手還是不夠的,完善的系統安全策略也相當重要,此外,良好的編程習慣,使用帶有數組越界檢查的 libc 也會對防止溢出攻擊起到重要作用。(責任編輯:A6)






[火星人 via ] GCC 中的編譯器堆棧保護技術已經有848次圍觀

http://www.coctec.com/docs/linux/show-post-68870.html