程序一開始就從main()函數執行的?事實并非如此。如果我們用IDA或者HIEW打開一個可執行文件,我們可以看到OEP(Original Entry Point)指向了其它代碼塊。這些代碼做了一些維護和準備工作之后再把控制流交給我們的代碼。這就是所謂的startup-code或叫CRT code(C RunTime)。
main()函數通過一個數組接收命令行傳遞過來的參數,環境變量與此類似。通常情況下,傳遞一個字符串到程序之后,CRT code會用空格來分割它們。CRT code同樣也準備了一個envp來存放環境變量。如果是GUI版本的win32程序,入口函數需要使用WinMain()來代替main()函數,它也有自己的參數。
#!c
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
);
CRT code同樣會準備好它所需要的所有參數。
此外,main()函數的返回值是它的退出碼。CRT code將它作為ExitProcess()的參數。
通常,每個編譯器都有它自己的CRT code。
下面是MSVC 2008特有的CRT code。
#!c
___tmainCRTStartup proc near
var_24 = dword ptr -24h
var_20 = dword ptr -20h
var_1C = dword ptr -1Ch
ms_exc = CPPEH_RECORD ptr -18h
push 14h
push offset stru_4092D0
call __SEH_prolog4
mov eax, 5A4Dh
cmp ds:400000h, ax
jnz short loc_401096
mov eax, ds:40003Ch
cmp dword ptr [eax+400000h], 4550h
jnz short loc_401096
mov ecx, 10Bh
cmp [eax+400018h], cx
jnz short loc_401096
cmp dword ptr [eax+400074h], 0Eh
jbe short loc_401096
xor ecx, ecx
cmp [eax+4000E8h], ecx
setnz cl
mov [ebp+var_1C], ecx
jmp short loc_40109A
loc_401096: ; CODE XREF: ___tmainCRTStartup+18
; ___tmainCRTStartup+29 ...
and [ebp+var_1C], 0
loc_40109A: ; CODE XREF: ___tmainCRTStartup+50
push 1
call __heap_init
pop ecx
test eax, eax
jnz short loc_4010AE
push 1Ch
call _fast_error_exit
pop ecx
loc_4010AE: ; CODE XREF: ___tmainCRTStartup+60
call __mtinit
test eax, eax
jnz short loc_4010BF
push 10h
call _fast_error_exit
pop ecx
loc_4010BF: ; CODE XREF: ___tmainCRTStartup+71
call sub_401F2B
and [ebp+ms_exc.disabled], 0
call __ioinit
test eax, eax
jge short loc_4010D9
push 1Bh
call __amsg_exit
pop ecx
loc_4010D9: ; CODE XREF: ___tmainCRTStartup+8B
call ds:GetCommandLineA
mov dword_40B7F8, eax
call ___crtGetEnvironmentStringsA
mov dword_40AC60, eax
call __setargv
test eax, eax
jge short loc_4010FF
push 8
call __amsg_exit
pop ecx
loc_4010FF: ; CODE XREF: ___tmainCRTStartup+B1
call __setenvp
test eax, eax
jge short loc_401110
push 9
call __amsg_exit
pop ecx
loc_401110: ; CODE XREF: ___tmainCRTStartup+C2
push 1
call __cinit
pop ecx
test eax, eax
jz short loc_401123
push eax
call __amsg_exit
pop ecx
loc_401123: ; CODE XREF: ___tmainCRTStartup+D6
mov eax, envp
mov dword_40AC80, eax
push eax ; envp
push argv ; argv
push argc ; argc
call _main
add esp, 0Ch
mov [ebp+var_20], eax
cmp [ebp+var_1C], 0
jnz short $LN28
push eax ; uExitCode
call $LN32
$LN28: ; CODE XREF: ___tmainCRTStartup+105
call __cexit
jmp short loc_401186
$LN27: ; DATA XREF: .rdata:stru_4092D0
mov eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 401044
mov ecx, [eax]
mov ecx, [ecx]
mov [ebp+var_24], ecx
push eax
push ecx
call __XcptFilter
pop ecx
pop ecx
$LN24:
retn
$LN14: ; DATA XREF: .rdata:stru_4092D0
mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401044
mov eax, [ebp+var_24]
mov [ebp+var_20], eax
cmp [ebp+var_1C], 0
jnz short $LN29
push eax ; int
call __exit
$LN29: ; CODE XREF: ___tmainCRTStartup+135
call __c_exit
loc_401186: ; CODE XREF: ___tmainCRTStartup+112
mov [ebp+ms_exc.disabled], 0FFFFFFFEh
mov eax, [ebp+var_20]
call __SEH_epilog4
retn
在這里我們看到代碼調用了GetCommandLineA(),setargv()和setenvp()去填充argc,argv,envp全局變量。
最后,使用這些參數去調用main()函數。
有些函數調用了與自身類似的函數,如heap_init(),ioinit()。
如果你嘗試在CRT code代碼中使用malloc(),它將異常退出下面的錯誤:
runtime error R6030
- CRT not initialized
在C++中,全局對象的初始化也同樣發生在main()函數執行之前的CRT:51.4.1。
main()函數的返回值傳給cexit()或$LN32,后者調用doexit()。
能否擺脫CRT?這個當然,如果你知道你在做什么的話。
MSVC的鏈接器可以通過/ENTRY選項設置入口函數。
#!c++
#include <windows.h>
int main()
{
MessageBox (NULL, "hello, world", "caption", MB_OK);
};
讓我們用MSVC 2008來編譯它。
#!bash
cl no_crt.c user32.lib /link /entry:main
我們可以獲得一個大小為2560字節的runnable.exe。它有一個PE頭,調用MessageBox的指令,數據段中有兩串字符串,而MessageBox函數導入自user32.DLL。
這個程序能夠正常運行,但你不能在main()函數里面使用WinMain()的四個參數。準確點來說你能,但是這些參數并沒有在執行的時候準備好。
#!bash
cl no_crt.c user32.lib /link /entry:main /align:16
它會報一個鏈接警告:
LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run
我們可以獲得一個720字節的exe文件。它可以在Windows 7 x86上正常運行,但是沒辦法在x64上運行(當你運行它的時候會將先是一條錯誤信息)。更多的優化可能可以提高執行效率,但如你所見,很快就出現了兼容問題。
PE是Windows下的可執行文件格式。
.exe,.dll,.sys文件它們之間的區別是,.exe和.sys文件通常沒有導出表,只有導入表。
DLL文件和其它PE文件類似,有一個入口點(OEP)(DllMain()函數),但一般情況下很少DLL帶有這個函數。
.sys通常是一個設備驅動程序。
作為驅動程序,Windows需要檢驗它的PE文件并保證它是正確的。
從Windows Vista開始,一個驅動程序文件必須擁有數字簽名,否則它會被拒絕加載。
每個PE文件都由一段打印“This program cannot be run in DOS mode.”的DOS程序塊開始。如果你的程序運行于DOS或者Windows 3.1(這些OS并不識別PE文件格式),這個DOS程序塊將被執行打印。
問題是,模塊(DLL)的開發者不可能事先知道哪些地址分配給哪些模塊使用的。
這就是為什么兩個具有相同基地址的DLL需要一個加載到這個基地址而另外一個加載到進程的其它空閑內存處并調整第二個DLL的虛擬地址。
通常情況下,MSVC鏈接器生成.exe文件的基地址是0x400000,并把代碼段安排在0x401000。這意味著該代碼段的RVA地址是0x1000。DLL的基地址通常被MSVC鏈接器安排在0x10000000。
還有一種情況下加載模塊時會導致基地址浮動。
這就是ASLR(Address Space Layout Randomization(地址空間布局隨機化))。
一個shellcode想要執行必須調用到系統的函數。
在老的操作系統當中(如果是WindowsNT,則在Windows Vista之前),系統的DLL(如kernel32.dll,user32.dll)總是加載到已知的地址。如果我們還記得的話,它們的版本是很少有變動的。因為函數的地址是固定的,shellcode可以直接調用它們。
為了避免這種情況,ASLR每次在加載模塊的時候都會隨機安排它們的基地址。
支持ASLR的程序在PE頭中會設置IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE標識表明其支持ASLR。
還有一個subsystem字段, 通常是: - native (sys驅動程序) - console (控制臺程序) - GUI (圖形程序)
PE文件還規定了可以加載它的最小Windows版本號。有一個表保存了PE的版本號和相應的Windows開發代號。
舉個例子,MSVC 2005編譯的.exe文件運行在Windows NT4(version 4.00)。但MSVC 2008不是(生成文件的版本是5.00,至少運行于Windows 2000)。
MSVC 2012生成的.exe文件默認是6.00版本,最低平臺要求至少是Windows Vista。但可以通過更改編譯選項,強制編譯器支持Windows XP。
一部分section似乎存在于所有可執行文件格式里面。
下面的標志位用于區分代碼和常量數據:
每個section在PE文件可能有一個名字,但是它并不是很重要。通常(但不總是)代碼section的名字是.text,數據section是.data,常量數據section是.rdata(readable data)。其它流行的名字還有:
PE文件的打包器/加密器經常打亂section名字或者把名字替換為自己的。
MSVC允許你任意命名section。
一些編譯器和鏈接器可以添加一個用于調試符號和其他調試信息的section(例如MinGW)。但不包括MSVC現在的版本(提供單獨的PDB文件用于這個目的)。
這是PE文件的section結構體定義:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
一些相關的字段的解釋:PointerToRawData是在磁盤文件中的偏移,VirtualAddress在Hiew中是裝載到內存中的RVA。
也稱為FIXUP-s(在Hiew)。
他們也存在于幾乎所有的可執行文件格式。
顯然,模塊可以被加載到各種基地地址,但如何處理全局變量?一個解決方案是使用位置無關代碼(67.1章),但它并不是總是有用的。
這就是重定位表存在的理由:當模塊加載到不同的基地址的時候,它們的入口地址都需要修正。
舉個例子,有一個全局變量的地址是0x410000,它是這樣訪問的:
A1 00 00 41 00 mov eax, [000410000]
模塊的基地址是0x400000,全局變量的RVA地址是0x10000。
如果模塊加載到0x500000這個基地址,那么全局變量實際的地址必須是0x510000。
我們可以看到,在0xA1字節之后,變量的地址編碼到MOV指令中的。
這就是為什么0xA1字節之后的4個字節地址寫在了重定位表。
如果模塊加載到不同的基地址,操作系統加載器枚舉重定位表中所有地址,查找每個32位的地址,減去原來的基地址(我們這里得到了RVA),并添加新的基地址。
如果模塊加載到原來的基地址,那么不做任何事情。
所有的全局變量都可以這樣處理。
重定位表可能有各種類型,但是在x86處理器的Windows中,通常是IMAGE_REL_BASED_HIGHLOW。
順便說一下,重定位表在Hiew是隱藏的。相關例子請查看(Figure 7.12)。
OllyDbg會用下劃線標識哪些使用了重定位表。相關例子請查看(Figure 13.11)。
眾所周知,任何可執行文件都必須使用操作系統提供的服務和其它一些動態鏈接庫。
可以說,一個模塊(通常是DLL)的函數通常都是導出提供給其它模塊使用(.exe文件或其它DLL)。
這種情況下,每個DLL都有一個導出(exports)表,由模塊的函數加它們的地址組成。
每個exe或dll文件也有一個導入(imports)表,里面包含了程序執行所需函數對應的DLL文件名。
在加載main.exe文件之后,操作系統加載器開始處理導入表:它加載所需的DLL文件,接著在DLL的導入表查找對應函數名字的地址,然后把它們的地址寫到main.exe模塊的IAT((Import Address Table)導入表)。
我們可以看到,加載器必須大量比較函數名,但字符串比較效率并不是很高。所以有一個支持“ordinals”或“hints”的東西,表示函數存儲在表中的序號,用于代替它們的函數名。
這使得它們可以更快地加載DLL。Ordinals在導出表中永遠都存在。
舉個例子:一個使用MFC庫的程序都是通過ordinals加載mfc*.dll,在這種程序中,INT(Import Name Table)是不存在MFC函數名字的。
使用IDA加載這類程序的時候,如果告訴它mfc*.dll文件路徑,則可以看到函數名。如果不告訴IDA這些DLL路徑,它會顯示諸如mfc80_123而不是函數名。
編譯器通常會給導入表及其相關內容分配一個單獨的section(名字類似.idata),但這不是一個強制規定。
因為術語混亂,導入表是一個比較令人困惑的地方。讓我們嘗試一下整理這些信息。
Figure 68.1: A scheme that unites all PE-file structures related to imports
里面主要的結構是IMAGE_IMPORT_DESCRIPTOR數組。每個被加載進來的DLL占用一個元素。
每個元素包含一個文本字符串(DLL名字)的RVA地址。
OriginalFirstThink是INT表的RVA地址。這是一個RVA地址的數組,里面每個成員都指向一個函數名的文本字符串。每個函數名的字符串之前是一個16位的("hint")-"ordinal"整數。
加載的時候,如果可以通過ordinal找到函數,那么就不需要使用字符串比較來查找函數。數組的最后一個元素是0。還有一個FirstThunk字段指向IAT表,這個地方是加載器重寫需要重新解析函數的地址的RVA地址。
需要加載器重寫地址的函數在IDA中加了諸如這種標記:__imp_CreateFileA。
加載器至少有兩種方法重寫地址:
代碼會有諸如調用__imp_CreateFileA的指令,因為導入函數的地址在某種意義上是一個全局變量,當模塊加載到不同的基地址時,call指令的地址被添加到重定位表中。 但是,顯然這種方法可能會擴大重定位表。因為有可能從這個模塊大量調用導入的函數。而且,重定位表太大的話會減慢模塊的加載速度。
每個導入函數給它分配一條jmp指令,使用jmp指令加上重定位表的地址跳轉到導入函數。這些入口點被稱之為“thunks”,所有調用導入函數僅需要調用相對應的“thunk”,這種情況下不需要額外的重定位操作,因為這些CALL都使用相對地址,不需要額外的調整操作。
這兩種方法可以組合使用。可能的話,鏈接器給那些被調用太多次的函數創建一個“thunk”,然而默認情況下不是這樣。
順便說一下,FirstThunk指向的函數地址數組不必要位于IAT section。舉個例子,我曾經寫的PE_add_import工具可以給.exe文件添加一個導入函數。在早些時候,這個工具可以讓你的函數調用其它DLL文件的函數。我的工具添加了類似下面的代碼:
MOV EAX, [yourdll.dll!function]
JMP EAX
FirstThunk指向第一條指令,換句話說,當加載yourdll.dll的時候,加載器在代碼中寫入function函數的正確地址。
還值得注意的是代碼段通常是寫保護的,因此我的工具在code section添加了一個IMAGE_SCN_MEM_WRITE標志位。否則,程序在加載的時候會爆出錯誤碼為5(訪問失敗)的異常錯誤。
有人可能會問:如果我提供一個程序與一組不變的DLL文件,是有可能加快加載過程?
是的,它可以提前把函數的地址寫入到導入表的FirstThunk數組。IMAGE_IMPORT_DESCRIPTOR結構有一個Timestamp字段。如果這個變量存在,則加載器會比較這個變量和DLL文件日期時間。如果它們相等,那么加載器不做任何事情,所以加載過程可以很快完成。這就是所謂的“old-style binding”。為了加快程序的加載,Matt Pietrek. “An In-Depth Look into the Win32 Portable Executable File Format”,建議你的程序安裝在最終用戶的計算機后不久做捆綁。
PE文件的打包器/加密器也可以壓縮/加密導入表。在這種情況下,Windows的加載器當然不會加載所有需要的DLL。因此打包器/加密器只能通過LoadLibrary()和GetProcAddress()來獲取所需函數。
安裝在Windows系統中的標準DLL文件,IAT往往是位于PE文件的開頭。據說,這是一種優化。加載時.exe文件不是全部加載到內存,它是“映射”和加載部分需要被訪問到的內存。可能微軟的開發者認為這樣加載比較快。
資源在PE文件只是一組圖標,圖片,文本字符串,對話框描述。因為把它們從主代碼分離了出來,所以多國語言程序很容易實現,只需要根據操作系統設置的語言去選擇文本或圖片的語言。
作為一個副作用,通過使用諸如ResHack的編輯器,即使在沒有專業知識的情況下,也可以輕松地編輯和保存可執行文件的資源。
.NET的程序并不編譯成機器碼,而是編譯成字節碼。嚴格地說,是在.exe文件里面使用字節碼代替x86機器。然而,進入入口點(OEP)還是需要一小段x86機器碼:
jmp mscoree.dll!_CorExeMain
.NET的加載器位于mscoree.dll,由它來處理PE文件。它存在于之前的所有Windows XP操作系統。從XP啟動的時候,OS的加載器能夠探測.NET文件并通過JMP指令執行。
這個section包含了初始化TLS的數據(65章)(如果需要的話)。當一個新線程啟動的時候,它的TLS數據使用這個section的數據進行初始化。
除此之外,PE文件規范還提供了TLS的初始化!當section,TLS callbacks存在,它們會在傳遞控制權到主入口點(OEP)之前被調用。這個功能廣泛用于PE文件的打包和加密。
Daniel Pistelli — The .NET File Format
在Windows,SEH(Structured Exception Handling(結構化異常處理))是異常處理的一種機制。然而,它是語言無關的,不管是C++
或者其它OOP語言。我們可以看到SEH(從C++
和MSVC擴展)是獨立實現的。
每個運行的進程都有一個SEH處理鏈,TIB有它最后的處理程序的地址。當異常發生時(除零,錯誤的地址訪問,用戶通過調用RaiseException()函數引發異常),操作系統在TIB找到最后的處理程序并調用它,獲取異常時CPU的狀態信息(如寄存器的值等等)。處理程序當前的異常能否修復,如果能,則修復該異常。如果不能,它通知操作系統無法處理它并由操作系統調用異常處理鏈中的下一個處理程序,直到處理程序能夠處理的異常被發現。
在異常處理鏈的結尾處有一個標準的處理程序,它顯示一個對話框用于通知用戶進程崩潰,然后把一些崩潰時CPU的狀態信息,收集起來并將其發送給微軟開發商。
Figure 68.2: Windows XP
Figure 68.3: Windows XP
Figure 68.4: Windows 7
Figure 68.5: Windows 8.1
早些時候,這個處理程序被稱為Dr.Watson。
順便說一句,有些開發人員會在自己的處理程序發送程序崩潰的信息。通過SetUnhandledExceptionFilter()函數注冊異常處理程序,如果操作系統沒有任何其它方式處理異常,則調用它。一個例子是Oracle RDBMS,它保存了CPU所有可能有用的信息和內存狀態的巨大轉儲文件。
讓我們寫一個自己的primitive exception handler:
#!c++
#include <windows.h>
#include <stdio.h>
DWORD new_value=1234;
EXCEPTION_DISPOSITION __cdecl except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
unsigned i;
printf ("%s\n", __FUNCTION__);
printf ("ExceptionRecord->ExceptionCode=0x%p\n", ExceptionRecord->ExceptionCode);
printf ("ExceptionRecord->ExceptionFlags=0x%p\n", ExceptionRecord->ExceptionFlags);
printf ("ExceptionRecord->ExceptionAddress=0x%p\n", ExceptionRecord->ExceptionAddress);
if (ExceptionRecord->ExceptionCode==0xE1223344)
{
printf ("That's for us\n");
// yes, we "handled" the exception
return ExceptionContinueExecution;
}
else if (ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION)
{
printf ("ContextRecord->Eax=0x%08X\n", ContextRecord->Eax);
// will it be possible to 'fix' it?
printf ("Trying to fix wrong pointer address\n");
ContextRecord->Eax=(DWORD)&new_value;
// yes, we "handled" the exception
return ExceptionContinueExecution;
}
else
{
printf ("We do not handle this\n");
// someone else's problem
return ExceptionContinueSearch;
};
}
int main()
{
DWORD handler = (DWORD)except_handler; // take a pointer to our handler
// install exception handler
__asm
{ // make EXCEPTION_REGISTRATION record:
push handler // address of handler function
push FS:[0] // address of previous handler
mov FS:[0],ESP // add new EXECEPTION_REGISTRATION
}
RaiseException (0xE1223344, 0, 0, NULL);
// now do something very bad
int* ptr=NULL;
int val=0;
val=*ptr;
printf ("val=%d\n", val);
// deinstall exception handler
__asm
{ // remove our EXECEPTION_REGISTRATION record
mov eax,[ESP] // get pointer to previous record
mov FS:[0], EAX // install previous record
add esp, 8 // clean our EXECEPTION_REGISTRATION off stack
}
return 0;
}
FS段寄存器:在Win32指向TIB。在TIB的第一個元素是指向異常處理指針鏈中的最后一個處理程序,我們將自己的異常處理程序的地址保存在這里。異常處理鏈的結點結構體名字是_EXCEPTION_REGISTRATION,這是一個單鏈表實現的棧容器。
Listing 68.1: MSVC/VC/crt/src/exsup.inc
\_EXCEPTION\_REGISTRATION struc
prev dd ?
handler dd ?
\_EXCEPTION\_REGISTRATION ends
每個結點的handler字段指向一個異常處理程序,每個結點的prev字段指向在棧中的上一個結點。最后一個結點的prev指向0xFFFFFFFF(-1)。
我們的處理程序安裝后,我們調用RaiseException()。這是一個用戶異常。處理程序檢查異常代碼,如果異常代碼是0xE1223344,它返回ExceptionContinueExecution。這意味著處理程序修復了CPU的狀態(通常是EIP/ESP寄存器),操作系統可以恢復運行。如果你稍微修改一下代碼,處理程序返回ExceptionContinueSearch,那么操作系統將調用下一個處理程序,如果沒有找到處理程序(因為沒人捕獲該異常),你會看到標準的Windows進程崩潰對話框。
系統異常和用戶異常之間的區別是什么?這里有系統的:
|as defined in WinBase.h |as defined in ntstatus.h numerical| value
|--------------------------------------------------------------------------
|EXCEPTION_ACCESS_VIOLATION | STATUS_ACCESS_VIOLATION | 0xC0000005
|EXCEPTION_DATATYPE_MISALIGNMENT | STATUS_DATATYPE_MISALIGNMENT | 0x80000002
|EXCEPTION_BREAKPOINT | STATUS_BREAKPOINT | 0x80000003
|EXCEPTION_SINGLE_STEP | STATUS_SINGLE_STEP | 0x80000004
|EXCEPTION_ARRAY_BOUNDS_EXCEEDED | STATUS_ARRAY_BOUNDS_EXCEEDED | 0xC000008C
|EXCEPTION_FLT_DENORMAL_OPERAND | STATUS_FLOAT_DENORMAL_OPERAND | 0xC000008D
|EXCEPTION_FLT_DIVIDE_BY_ZERO | STATUS_FLOAT_DIVIDE_BY_ZERO | 0xC000008E
|EXCEPTION_FLT_INEXACT_RESULT | STATUS_FLOAT_INEXACT_RESULT | 0xC000008F
|EXCEPTION_FLT_INVALID_OPERATION | STATUS_FLOAT_INVALID_OPERATION | 0xC0000090
|EXCEPTION_FLT_OVERFLOW | STATUS_FLOAT_OVERFLOW | 0xC0000091
|EXCEPTION_FLT_STACK_CHECK | STATUS_FLOAT_STACK_CHECK | 0xC0000092
|EXCEPTION_FLT_UNDERFLOW | STATUS_FLOAT_UNDERFLOW | 0xC0000093
|EXCEPTION_INT_DIVIDE_BY_ZERO | STATUS_INTEGER_DIVIDE_BY_ZERO | 0xC0000094
|EXCEPTION_INT_OVERFLOW | STATUS_INTEGER_OVERFLOW | 0xC0000095
|EXCEPTION_PRIV_INSTRUCTION | STATUS_PRIVILEGED_INSTRUCTION | 0xC0000096
|EXCEPTION_IN_PAGE_ERROR | STATUS_IN_PAGE_ERROR | 0xC0000006
|EXCEPTION_ILLEGAL_INSTRUCTION | STATUS_ILLEGAL_INSTRUCTION | 0xC000001D
|EXCEPTION_NONCONTINUABLE_EXCEPTION | STATUS_NONCONTINUABLE_EXCEPTION | 0xC0000025
|EXCEPTION_STACK_OVERFLOW | STATUS_STACK_OVERFLOW | 0xC00000FD
|EXCEPTION_INVALID_DISPOSITION | STATUS_INVALID_DISPOSITION | 0xC0000026
|EXCEPTION_GUARD_PAGE | STATUS_GUARD_PAGE_VIOLATION | 0x80000001
|EXCEPTION_INVALID_HANDLE | STATUS_INVALID_HANDLE | 0xC0000008
|EXCEPTION_POSSIBLE_DEADLOCK | STATUS_POSSIBLE_DEADLOCK | 0xC0000194
|CONTROL_C_EXIT | STATUS_CONTROL_C_EXIT | 0xC000013A
這些異常碼的定義規則是:
| 31 | 29 | 28 | 27 ~ 16 | 15 ~ 0 |
|-------------------------------------------|
| S | U | 0 | Facility code | Error code |
S是一個基本代碼: 11—error; 10—warning; 01—informational; 00—success;U表示是否是用戶代碼。
這就是為什么我選擇了0xE1223344,0xE(1110b)意味著1)user exception(用戶異常);2)error(錯誤)。
當我們嘗試讀取地址為0的內存時。因為這個地址在win32中并不被使用,所以會引發一個異常。通過檢查異常碼是否等于EXCEPTION_ACCESS_VIOLATION常量。
讀0地址內存的代碼看起來像這樣:
Listing 68.2: MSVC 2010
...
xor eax, eax
mov eax, DWORD PTR [eax] ; exception will occur here
push eax
push OFFSET msg
call _printf
add esp, 8
...
能否修復“on the fly”這個錯誤然后繼續執行程序?當然,我們的異常處理程序可以修復EAX值然后讓操作系統繼續執行下去。這是我們該做的。printf()將打印1234,因為我們的處理程序執行后EAX不是0,而是全局變量new_value的地址。
若內存管理器有一個關于CPU的錯誤信號,CPU會暫停線程,在Windows內核查找異常處理程序,然后一個一個調用SEH鏈的handler。
我在這里使用MSVC 2010,當然,沒有任何保證EAX將用于這個指針。
這個地址替換的技巧非常的漂亮,我經常使用它插入到SEH內部中。不過,我忘記了在哪里用它修復“on the fly”錯誤。
為什么SHE相關的記錄存儲在棧上而不是其它地方?據說這是因為操作系統不需要在函數執行完成之后關心這些信息。但我不能100%肯定。這有點類似alloca()。
據說,微軟的程序員需要在C語言而不是C++
上使用異常,所以它們在MSVC上添加了一個非標準的C擴展。它與C++
的異常沒有任何關聯。
__try
{
...
}
__except(filter code)
{
handler code
}
“Finally”塊也許能代替handler code:
__try
{
...
}
__finally
{
...
}
filte code是一個表達式,告訴handler code是否對應引發的異常。如果你的filte code太大而無法使用一個表達式,可以定義一個單獨的filte函數。
在Windows內核有很多這樣的結構,下面是幾個例子(WRK(Windows Research Kernel)):
Listing 68.3: WRK-v1.2/base/ntos/ob/obwait.c
#!c++
try {
KeReleaseMutant( (PKMUTANT)SignalObject,
MUTANT_INCREMENT,
FALSE,
TRUE );
} except((GetExceptionCode () == STATUS_ABANDONED ||
GetExceptionCode () == STATUS_MUTANT_NOT_OWNED)?
EXCEPTION_EXECUTE_HANDLER :
EXCEPTION_CONTINUE_SEARCH) {
Status = GetExceptionCode();
goto WaitExit;
}
Listing 68.4: WRK-v1.2/base/ntos/cache/cachesub.c
#!c++
try {
RtlCopyBytes( (PVOID)((PCHAR)CacheBuffer + PageOffset),
UserBuffer,
MorePages ?
(PAGE_SIZE - PageOffset) :
(ReceivedLength - PageOffset) );
} except( CcCopyReadExceptionFilter( GetExceptionInformation(), Status ) ) {
這里是一個filter code的例子:
Listing 68.5: WRK-v1.2/base/ntos/cache/copysup.c
#!c++
LONG
CcCopyReadExceptionFilter(
IN PEXCEPTION_POINTERS ExceptionPointer,
IN PNTSTATUS ExceptionCode
)
/*++
Routine Description:
This routine serves as a exception filter and has the special job of
extracting the "real" I/O error when Mm raises STATUS_IN_PAGE_ERROR
beneath us.
Arguments:
ExceptionPointer - A pointer to the exception record that contains
the real Io Status.
ExceptionCode - A pointer to an NTSTATUS that is to receive the real
status.
Return Value:
EXCEPTION_EXECUTE_HANDLER
--*/
{
*ExceptionCode = ExceptionPointer->ExceptionRecord->ExceptionCode;
if ( (*ExceptionCode == STATUS_IN_PAGE_ERROR) &&
(ExceptionPointer->ExceptionRecord->NumberParameters >= 3) ) {
*ExceptionCode = (NTSTATUS) ExceptionPointer->ExceptionRecord->ExceptionInformation[2];
}
ASSERT( !NT_SUCCESS(*ExceptionCode) );
return EXCEPTION_EXECUTE_HANDLER;
}
在內部,SEH是操作系統支持的異常擴展。但是處理函數是_except_handler3(對于SEH3)或_except_handler4(對于SEH4)。 這個處理函數的代碼是與MSVC相關的,它位于它的庫或在msvcr*.dll文件。其他的Win32編譯器可以提供與之完全不同的機制。
SEH3有一個_except_handler3處理函數,而且擴展了_EXCEPTION_REGISTRATION表,并添加了一個指向scope table和previous try level變量。SEH4擴展了scope table緩沖溢出保護。
scope table是一個表,包含了指向filter和handler code的塊和每個try/except嵌套。
再者,操作系統只關心prev/handle字段。_except_handler3函數的工作是讀取其他字段和scope table,并決定由哪些處理程序來執行。
_except_handler3函數的源代碼是閉源的。然而,Sanos操作系統的win32兼容性層重新實現相同的功能。其它類似的實現有Wine和ReactOS。
如果filter指針為NULL,handler指針則指向finally代碼塊。
執行期間,棧中的previous try level變量發生變化,所以_except_handler3可以獲取當前嵌套級的信息,才知道要使用scope table哪一表項。
#!c
#include <stdio.h>
#include <windows.h>
#include <excpt.h>
int main()
{
int* p = NULL;
__try
{
printf("hello #1!\n");
*p = 13; // causes an access violation exception;
printf("hello #2!\n");
}
__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("access violation, can't recover\n");
}
}
Listing 68.6: MSVC 2003
$SG74605 DB 'hello #1!', 0aH, 00H
$SG74606 DB 'hello #2!', 0aH, 00H
$SG74608 DB 'access violation, can''t recover', 0aH, 00H
_DATA ENDS
; scope table
CONST SEGMENT
$T74622 DD 0ffffffffH ; previous try level
DD FLAT:$L74617 ; filter
DD FLAT:$L74618 ; handler
CONST ENDS
_TEXT SEGMENT
$T74621 = -32 ; size = 4
_p$ = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC NEAR
push ebp
mov ebp, esp
push -1 ; previous try level
push OFFSET FLAT:$T74622 ; scope table
push OFFSET FLAT:__except_handler3 ; handler
mov eax, DWORD PTR fs:__except_list
push eax ; prev
mov DWORD PTR fs:__except_list, esp
add esp, -16
push ebx ; saved 3 registers
push esi ; saved 3 registers
push edi ; saved 3 registers
mov DWORD PTR __$SEHRec$[ebp], esp
mov DWORD PTR _p$[ebp], 0
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
push OFFSET FLAT:$SG74605 ; 'hello #1!'
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
push OFFSET FLAT:$SG74606 ; 'hello #2!'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; previous try level
jmp SHORT $L74616
; filter code
$L74617:
$L74627:
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T74621[ebp], eax
mov eax, DWORD PTR $T74621[ebp]
sub eax, -1073741819; c0000005H
neg eax
sbb eax, eax
inc eax
$L74619:
$L74626:
ret 0
; handler code
$L74618:
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET FLAT:$SG74608 ; 'access violation, can''t recover'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; setting previous try level back to -1
$L74616:
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
mov DWORD PTR fs:__except_list, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
在這里我們可以看到SEH幀是如果在棧中構建出來的,scope table位于CONST segment-事實上,這些字段是不被改變的。一件有趣的事情是如何改變previous try level變量。它的初始化值是0xFFFFFFFF(-1)。當進入try語句塊的時候,變量賦值為0。當try語句塊結束的時候,寫回-1。我們還能看到filter和handler code的地址。因此,我們可以很容易在函數里看到try/except是如何構造的。
由于函數序言的SEH安裝代碼被多個函數共享,有時候編譯器會在函數序言插入調用SEH_prolog()函數,這就完成了這個任務。該SEH回收代碼是SEH_epilog()函數。
讓我們嘗試用tracer運行這個例子:
#!bash
tracer.exe -l:2.exe --dump-seh
Listing 68.7: tracer.exe output
EXCEPTION_ACCESS_VIOLATION at 2.exe!main+0x44 (0x401054) ExceptionInformation[0]=1
EAX=0x00000000 EBX=0x7efde000 ECX=0x0040cbc8 EDX=0x0008e3c8
ESI=0x00001db1 EDI=0x00000000 EBP=0x0018feac ESP=0x0018fe80
EIP=0x00401054
FLAGS=AF IF RF
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x401204 (2.exe!_except_handler
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x401070 (2.exe!main+0x60) handler=0x401088 (2.exe!main+0x78)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x401204 (2.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x401531 (2.exe!mainCRTStartup+0x18d) handler=0x401545 (2.exe!mainCRTStartup+0x1a1)
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header: GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll!___safe_se_handler_table+0x20) handler=0x771f90eb ([email protected]+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 ([email protected])
我們看到,SEH鏈包含4個handler。
前面兩個是我們的例子。兩個?但是我們只有一個?是的,一個是CRT的_mainCRTStartup()函數設置的。并至少作為FPU異常的處理。它的源碼可以在MSVC的安裝目錄找到:crt/src/winxfltr.c。
第三個是ntdll.dll的SEH4,第四個handler也位于ntdll.dll,跟MSVC沒什么關系,它有一個自描述函數名。
正如你所見,在一個鏈中有三種類型的處理函數:一個跟MSVC(最后一個)沒什么關系和兩個與MSVC關聯的:SEH3和SEH4。
#!c
#include <stdio.h>
#include <windows.h>
#include <excpt.h>
int filter_user_exceptions (unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
printf("in filter. code=0x%08X\n", code);
if (code == 0x112233)
{
printf("yes, that is our exception\n");
return EXCEPTION_EXECUTE_HANDLER;
}
else
{
printf("not our exception\n");
return EXCEPTION_CONTINUE_SEARCH;
};
}
int main()
{
int* p = NULL;
__try
{
__try
{
printf ("hello!\n");
RaiseException (0x112233, 0, 0, NULL);
printf ("0x112233 raised. now let's crash\n");
*p = 13; // causes an access violation exception;
}
__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("access violation, can't recover\n");
}
}
__except(filter_user_exceptions(GetExceptionCode(), GetExceptionInformation()))
{
// the filter_user_exceptions() function answering to the question
// "is this exception belongs to this block?"
// if yes, do the follow:
printf("user exception caught\n");
}
}
現在有兩個try塊,所以scope table現在有兩個元素,每個塊占用一個。Previous try level隨著try塊的進入或退出而改變。
Listing 68.8: MSVC 2003
$SG74606 DB 'in filter. code=0x%08X', 0aH, 00H
$SG74608 DB 'yes, that is our exception', 0aH, 00H
$SG74610 DB 'not our exception', 0aH, 00H
$SG74617 DB 'hello!', 0aH, 00H
$SG74619 DB '0x112233 raised. now let''s crash', 0aH, 00H
$SG74621 DB 'access violation, can''t recover', 0aH, 00H
$SG74623 DB 'user exception caught', 0aH, 00H
_code$ = 8 ; size = 4
_ep$ = 12 ; size = 4
_filter_user_exceptions PROC NEAR
push ebp
mov ebp, esp
mov eax, DWORD PTR _code$[ebp]
push eax
push OFFSET FLAT:$SG74606 ; 'in filter. code=0x%08X'
call _printf
add esp, 8
cmp DWORD PTR _code$[ebp], 1122867; 00112233H
jne SHORT $L74607
push OFFSET FLAT:$SG74608 ; 'yes, that is our exception'
call _printf
add esp, 4
mov eax, 1
jmp SHORT $L74605
$L74607:
push OFFSET FLAT:$SG74610 ; 'not our exception'
call _printf
add esp, 4
xor eax, eax
$L74605:
pop ebp
ret 0
_filter_user_exceptions ENDP
; scope table
CONST SEGMENT
$T74644 DD 0ffffffffH ; previous try level for outer block
DD FLAT:$L74634 ; outer block filter
DD FLAT:$L74635 ; outer block handler
DD 00H ; previous try level for inner block
DD FLAT:$L74638 ; inner block filter
DD FLAT:$L74639 ; inner block handler
CONST ENDS
$T74643 = -36 ; size = 4
$T74642 = -32 ; size = 4
_p$ = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC NEAR
push ebp
mov ebp, esp
push -1 ; previous try level
push OFFSET FLAT:$T74644
push OFFSET FLAT:__except_handler3
mov eax, DWORD PTR fs:__except_list
push eax
mov DWORD PTR fs:__except_list, esp
add esp, -20
push ebx
push esi
push edi
mov DWORD PTR __$SEHRec$[ebp], esp
mov DWORD PTR _p$[ebp], 0
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; outer try block entered. set previous try level to 0
mov DWORD PTR __$SEHRec$[ebp+20], 1 ; inner try block entered. set previous try level to 1
push OFFSET FLAT:$SG74617 ; 'hello!'
call _printf
add esp, 4
push 0
push 0
push 0
push 1122867 ; 00112233H
call DWORD PTR [email protected]
push OFFSET FLAT:$SG74619 ; '0x112233 raised. now let''s crash'
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0
jmp SHORT $L74615
; inner block filter
$L74638:
$L74650:
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T74643[ebp], eax
mov eax, DWORD PTR $T74643[ebp]
sub eax, -1073741819; c0000005H
neg eax
sbb eax, eax
inc eax
$L74640:
$L74648:
ret 0
; inner block handler
$L74639:
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET FLAT:$SG74621 ; 'access violation, can''t recover'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0
$L74615:
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; outer try block exited, set previous try level back to -1
jmp SHORT $L74633
; outer block filter
$L74634:
$L74651:
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T74642[ebp], eax
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
push ecx
mov edx, DWORD PTR $T74642[ebp]
push edx
call _filter_user_exceptions
add esp, 8
$L74636:
$L74649:
ret 0
; outer block handler
$L74635:
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET FLAT:$SG74623 ; 'user exception caught'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; both try blocks exited. set previous try level back to -1
$L74633:
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
mov DWORD PTR fs:__except_list, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
如果我們在handler中調用的printf()函數設置一個斷點,可以看到另一個SEH handler如何被添加。同樣,我們還可以看到scope table包含兩個元素。
tracer.exe -l:3.exe bpx=3.exe!printf --dump-seh
Listing 68.9: tracer.exe output
(0) 3.exe!printf
EAX=0x0000001b EBX=0x00000000 ECX=0x0040cc58 EDX=0x0008e3c8
ESI=0x00000000 EDI=0x00000000 EBP=0x0018f840 ESP=0x0018f838
EIP=0x004011b6
FLAGS=PF ZF IF
* SEH frame at 0x18f88c prev=0x18fe9c handler=0x771db4ad ([email protected]+0x3a)
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=1
scopetable entry[0]. previous try level=-1, filter=0x401120 (3.exe!main+0xb0) handler=0x40113b (3.exe!main+0xcb)
scopetable entry[1]. previous try level=0, filter=0x4010e8 (3.exe!main+0x78) handler=0x401100 (3.exe!main+0x90)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x40160d (3.exe!mainCRTStartup+0x18d) handler=0x401621 (3.exe!mainCRTStartup+0x1a1
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header: GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll!___safe_se_handler_table+0x20) handler=0x771f90eb ([email protected]+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 ([email protected])
在緩沖區攻擊期間(18.2章),scope table的地址可以被重寫。所以從MSVC 2005開始,SEH3升級到SEH4后有了緩沖區溢出保護。現在scope table指針與一個security cookie(一個隨機值)做異或運算。scope table擴展了包含兩個指向security cookie指針的頭部。每個元素都有另一個棧內偏移值:棧幀的地址(EBP)與security_cookie異或。該值將在異常處理過程中讀取并檢查其正確性。棧中的security cookie每次都是隨機的,所以遠程攻擊者無法預測到它。
SEH4的previous try level初始化值是-2而不是-1。
這里有兩個使用MSVC編譯的SEH4例子:
Listing 68.10: MSVC 2012: one try block example
$SG85485 DB 'hello #1!', 0aH, 00H
$SG85486 DB 'hello #2!', 0aH, 00H
$SG85488 DB 'access violation, can''t recover', 0aH, 00H
; scope table:
xdata$x SEGMENT
__sehtable$_main DD 0fffffffeH ; GS Cookie Offset
DD 00H ; GS Cookie XOR Offset
DD 0ffffffccH ; EH Cookie Offset
DD 00H ; EH Cookie XOR Offset
DD 0fffffffeH ; previous try level
DD FLAT:[email protected] ; filter
DD FLAT:[email protected] ; handler
xdata$x ENDS
$T2 = -36 ; size = 4
_p$ = -32 ; size = 4
tv68 = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC
push ebp
mov ebp, esp
push -2
push OFFSET __sehtable$_main
push OFFSET __except_handler4
mov eax, DWORD PTR fs:0
push eax
add esp, -20
push ebx
push esi
push edi
mov eax, DWORD PTR ___security_cookie
xor DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope table
xor eax, ebp
push eax ; ebp ^ security_cookie
lea eax, DWORD PTR __$SEHRec$[ebp+8] ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
mov DWORD PTR fs:0, eax
mov DWORD PTR __$SEHRec$[ebp], esp
mov DWORD PTR _p$[ebp], 0
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
push OFFSET $SG85485 ; 'hello #1!'
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
push OFFSET $SG85486 ; 'hello #2!'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
jmp SHORT [email protected]
; filter:
[email protected]:
[email protected]:
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T2[ebp], eax
cmp DWORD PTR $T2[ebp], -1073741819 ; c0000005H
jne SHORT [email protected]
mov DWORD PTR tv68[ebp], 1
jmp SHORT [email protected]
[email protected]:
mov DWORD PTR tv68[ebp], 0
[email protected]:
mov eax, DWORD PTR tv68[ebp]
[email protected]:
[email protected]:
ret 0
; handler:
[email protected]:
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET $SG85488 ; 'access violation, can''t recover'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
[email protected]:
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
mov DWORD PTR fs:0, ecx
pop ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
Listing 68.11: MSVC 2012: two try blocks example
$SG85486 DB 'in filter. code=0x%08X', 0aH, 00H
$SG85488 DB 'yes, that is our exception', 0aH, 00H
$SG85490 DB 'not our exception', 0aH, 00H
$SG85497 DB 'hello!', 0aH, 00H
$SG85499 DB '0x112233 raised. now let''s crash', 0aH, 00H
$SG85501 DB 'access violation, can''t recover', 0aH, 00H
$SG85503 DB 'user exception caught', 0aH, 00H
xdata$x SEGMENT
__sehtable$_main DD 0fffffffeH ; GS Cookie Offset
DD 00H ; GS Cookie XOR Offset
DD 0ffffffc8H ; EH Cookie Offset
DD 00H ; EH Cookie Offset
DD 0fffffffeH ; previous try level for outer block
DD FLAT:[email protected] ; outer block filter
DD FLAT:[email protected] ; outer block handler
DD 00H ; previous try level for inner block
DD FLAT:[email protected] ; inner block filter
DD FLAT:[email protected] ; inner block handler
xdata$x ENDS
$T2 = -40 ; size = 4
$T3 = -36 ; size = 4
_p$ = -32 ; size = 4
tv72 = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC
push ebp
mov ebp, esp
push -2 ; initial previous try level
push OFFSET __sehtable$_main
push OFFSET __except_handler4
mov eax, DWORD PTR fs:0
push eax ; prev
add esp, -24
push ebx
push esi
push edi
mov eax, DWORD PTR ___security_cookie
xor DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope table
xor eax, ebp ; ebp ^ security_cookie
push eax
lea eax, DWORD PTR __$SEHRec$[ebp+8] ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
mov DWORD PTR fs:0, eax
mov DWORD PTR __$SEHRec$[ebp], esp
mov DWORD PTR _p$[ebp], 0
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; entering outer try block, setting previous try level=0
mov DWORD PTR __$SEHRec$[ebp+20], 1 ; entering inner try block, setting previous try level=1
push OFFSET $SG85497 ; 'hello!'
call _printf
add esp, 4
push 0
push 0
push 0
push 1122867 ; 00112233H
call DWORD PTR [email protected]
push OFFSET $SG85499 ; '0x112233 raised. now let''s crash'
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, set previous try level back to 0
jmp SHORT [email protected]
; inner block filter:
[email protected]:
[email protected]:
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T3[ebp], eax
cmp DWORD PTR $T3[ebp], -1073741819 ; c0000005H
jne SHORT [email protected]
mov DWORD PTR tv72[ebp], 1
jmp SHORT [email protected]
[email protected]:
mov DWORD PTR tv72[ebp], 0
[email protected]:
mov eax, DWORD PTR tv72[ebp]
[email protected]:
[email protected]:
ret 0
; inner block handler:
[email protected]:
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET $SG85501 ; 'access violation, can''t recover'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, setting previous try level back to 0
[email protected]:
mov DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
jmp SHORT [email protected]
; outer block filter:
[email protected]:
[email protected]:
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T2[ebp], eax
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
push ecx
mov edx, DWORD PTR $T2[ebp]
push edx
call _filter_user_exceptions
add esp, 8
[email protected]:
[email protected]:
ret 0
; outer block handler:
[email protected]:
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET $SG85503 ; 'user exception caught'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
[email protected]:
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
mov DWORD PTR fs:0, ecx
pop ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
_code$ = 8 ; size = 4
_ep$ = 12 ; size = 4
_filter_user_exceptions PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _code$[ebp]
push eax
push OFFSET $SG85486 ; 'in filter. code=0x%08X'
call _printf
add esp, 8
cmp DWORD PTR _code$[ebp], 1122867 ; 00112233H
jne SHORT [email protected]_use
push OFFSET $SG85488 ; 'yes, that is our exception'
call _printf
add esp, 4
mov eax, 1
jmp SHORT [email protected]_use
jmp SHORT [email protected]_use
[email protected]_use:
push OFFSET $SG85490 ; 'not our exception'
call _printf
add esp, 4
xor eax, eax
[email protected]_use:
pop ebp
ret 0
_filter_user_exceptions ENDP
這里是cookie的含義:Cookie Offset用于區分棧中saved_EBP的地址和EBP⊕security_cookie。附加的Cookie XOR Offset用于區分EBP⊕security_cookie是否保存在棧中。如果這個等式不為true,會由于棧受到破壞而停止這個過程。
security_cookie⊕(Cookie XOR Offset+address_of_saved_EBP) == stack[address_of_saved_EBP + CookieOffset]
如果Cookie Offset為-2,這意味著它不存在。
在我的tracer工具也實現了Cookie檢查,具體請看Github。
MSVC 2005之后的編譯器開啟/GS選項仍可能會回滾到SEH3。不過,CRT的代碼總是使用SEH4。
正如你所認為的,每個函數序言在設置SEH幀效率不高。另一個性能問題是,函數執行期間多次嘗試改變previous try level。這種情況在x64完全改變了:現在所有指向try塊,filter和handler函數都保存在PE文件的.pdata段,由它提供給操作系統異常處理所需信息。
這里有兩個使用x64編譯的例子:
Listing 68.12: MSVC 2012
$SG86276 DB 'hello #1!', 0aH, 00H
$SG86277 DB 'hello #2!', 0aH, 00H
$SG86279 DB 'access violation, can''t recover', 0aH, 00H
pdata SEGMENT
$pdata$main DD imagerel $LN9
DD imagerel $LN9+61
DD imagerel $unwind$main
pdata ENDS
pdata SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
DD imagerel main$filt$0+32
DD imagerel $unwind$main$filt$0
pdata ENDS
xdata SEGMENT
$unwind$main DD 020609H
DD 030023206H
DD imagerel __C_specific_handler
DD 01H
DD imagerel $LN9+8
DD imagerel $LN9+40
DD imagerel main$filt$0
DD imagerel $LN9+40
$unwind$main$filt$0 DD 020601H
DD 050023206H
xdata ENDS
_TEXT SEGMENT
main PROC
$LN9:
push rbx
sub rsp, 32
xor ebx, ebx
lea rcx, OFFSET FLAT:$SG86276 ; 'hello #1!'
call printf
mov DWORD PTR [rbx], 13
lea rcx, OFFSET FLAT:$SG86277 ; 'hello #2!'
call printf
jmp SHORT [email protected]
[email protected]:
lea rcx, OFFSET FLAT:$SG86279 ; 'access violation, can''t recover'
call printf
npad 1 ; align next label
[email protected]:
xor eax, eax
add rsp, 32
pop rbx
ret 0
main ENDP
_TEXT ENDS
text$x SEGMENT
main$filt$0 PROC
push rbp
sub rsp, 32
mov rbp, rdx
[email protected]$filt$:
mov rax, QWORD PTR [rcx]
xor ecx, ecx
cmp DWORD PTR [rax], -1073741819; c0000005H
sete cl
mov eax, ecx
[email protected]$filt$:
add rsp, 32
pop rbp
ret 0
int 3
main$filt$0 ENDP
text$x ENDS
Listing 68.13: MSVC 2012
$SG86277 DB 'in filter. code=0x%08X', 0aH, 00H
$SG86279 DB 'yes, that is our exception', 0aH, 00H
$SG86281 DB 'not our exception', 0aH, 00H
$SG86288 DB 'hello!', 0aH, 00H
$SG86290 DB '0x112233 raised. now let''s crash', 0aH, 00H
$SG86292 DB 'access violation, can''t recover', 0aH, 00H
$SG86294 DB 'user exception caught', 0aH, 00H
pdata SEGMENT
$pdata$filter_user_exceptions DD imagerel $LN6
DD imagerel $LN6+73
DD imagerel $unwind$filter_user_exceptions
$pdata$main DD imagerel $LN14
DD imagerel $LN14+95
DD imagerel $unwind$main
pdata ENDS
pdata SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
DD imagerel main$filt$0+32
DD imagerel $unwind$main$filt$0
$pdata$main$filt$1 DD imagerel main$filt$1
DD imagerel main$filt$1+30
DD imagerel $unwind$main$filt$1
pdata ENDS
xdata SEGMENT
$unwind$filter_user_exceptions DD 020601H
DD 030023206H
$unwind$main DD 020609H
DD 030023206H
DD imagerel __C_specific_handler
DD 02H
DD imagerel $LN14+8
DD imagerel $LN14+59
DD imagerel main$filt$0
DD imagerel $LN14+59
DD imagerel $LN14+8
DD imagerel $LN14+74
DD imagerel main$filt$1
DD imagerel $LN14+74
$unwind$main$filt$0 DD 020601H
DD 050023206H
$unwind$main$filt$1 DD 020601H
DD 050023206H
xdata ENDS
_TEXT SEGMENT
main PROC
$LN14:
push rbx
sub rsp, 32
xor ebx, ebx
lea rcx, OFFSET FLAT:$SG86288 ; 'hello!'
call printf
xor r9d, r9d
xor r8d, r8d
xor edx, edx
mov ecx, 1122867 ; 00112233H
call QWORD PTR __imp_RaiseException
lea rcx, OFFSET FLAT:$SG86290 ; '0x112233 raised. now let''s crash'
call printf
mov DWORD PTR [rbx], 13
jmp SHORT [email protected]
[email protected]:
lea rcx, OFFSET FLAT:$SG86292 ; 'access violation, can''t recover'
call printf
npad 1 ; align next label
[email protected]:
jmp SHORT [email protected]
[email protected]:
lea rcx, OFFSET FLAT:$SG86294 ; 'user exception caught'
call printf
npad 1 ; align next label
[email protected]:
xor eax, eax
add rsp, 32
pop rbx
ret 0
main ENDP
text$x SEGMENT
main$filt$0 PROC
push rbp
sub rsp, 32
mov rbp, rdx
[email protected]$filt$:
mov rax, QWORD PTR [rcx]
xor ecx, ecx
cmp DWORD PTR [rax], -1073741819; c0000005H
sete cl
mov eax, ecx
[email protected]$filt$:
add rsp, 32
pop rbp
ret 0
int 3
main$filt$0 ENDP
main$filt$1 PROC
push rbp
sub rsp, 32
mov rbp, rdx
[email protected]$filt$:
mov rax, QWORD PTR [rcx]
mov rdx, rcx
mov ecx, DWORD PTR [rax]
call filter_user_exceptions
npad 1 ; align next label
[email protected]$filt$:
add rsp, 32
pop rbp
ret 0
int 3
main$filt$1 ENDP
text$x ENDS
_TEXT SEGMENT
code$ = 48
ep$ = 56
filter_user_exceptions PROC
$LN6:
push rbx
sub rsp, 32
mov ebx, ecx
mov edx, ecx
lea rcx, OFFSET FLAT:$SG86277 ; 'in filter. code=0x%08X'
call printf
cmp ebx, 1122867; 00112233H
jne SHORT [email protected]_use
lea rcx, OFFSET FLAT:$SG86279 ; 'yes, that is our exception'
call printf
mov eax, 1
add rsp, 32
pop rbx
ret 0
[email protected]_use:
lea rcx, OFFSET FLAT:$SG86281 ; 'not our exception'
call printf
xor eax, eax
add rsp, 32
pop rbx
ret 0
filter_user_exceptions ENDP
_TEXT ENDS
讀Sko12獲取更多詳細的信息。
除了異常信息,.pdata還包含了幾乎所有函數的開始和結束地址,因此它可能對于自動化分析工具有用。
Matt Pietrek. “A Crash Course on the Depths of Win32? Structured Exception Handling”. In: MSDN magazine (). URL: http://go.yurichev.com/17293.
Igor Skochinsky. Compiler Internals: Exceptions and RTTI. Also available as [http://go.yurichev.com/ 17294](http://go.yurichev.com/ 17294). 2012.
臨界區在任何操作系統多線程環境中都是非常重要的,它保證一個線程在某一時刻訪問一些數據的時候,阻塞其它正要訪問這些數據的線程。
下面是Windows NT操作系統的CRITICAL_SECTION聲明:
Listing 68.14: (Windows Research Kernel v1.2) public/sdk/inc/nturtl.h
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
下面展示了EnterCriticalSection()函數的運行過程:
Listing 68.15: Windows 2008/ntdll.dll/x86 (begin)
[email protected]
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
arg_0 = dword ptr 8
mov edi, edi
push ebp
mov ebp, esp
sub esp, 0Ch
push esi
push edi
mov edi, [ebp+arg_0]
lea esi, [edi+4] ; LockCount
mov eax, esi
lock btr dword ptr [eax], 0
jnb wait ; jump if CF=0
loc_7DE922DD:
mov eax, large fs:18h
mov ecx, [eax+24h]
mov [edi+0Ch], ecx
mov dword ptr [edi+8], 1
pop edi
xor eax, eax
pop esi
mov esp, ebp
pop ebp
retn 4
... skipped
在這段代碼中最重要的指令是BTR(帶LOCK前綴):把目的操作數中由源操作數所指定位的值送往標志位CF,并將目的操作數中的該位置0。這是一個原子操作,會阻塞掉其它同時想要訪問這段內存的CPU(參看BTR指令的LOCK前綴)。如果LockCount是1,則重置并返回:我們現在正處于臨界區。如果不是,則表示其它線程正在占用,將進入等待狀態。
使用WaitForSingleObject()進入等待狀態。
下面展示了LeaveCriticalSection()函數的運行過程:
Listing 68.16: Windows 2008/ntdll.dll/x86 (begin)
[email protected] proc near
arg_0 = dword ptr 8
mov edi, edi
push ebp
mov ebp, esp
push esi
mov esi, [ebp+arg_0]
add dword ptr [esi+8], 0FFFFFFFFh ;RecursionCount
jnz short loc_7DE922B2
push ebx
push edi
lea edi, [esi+4] ; LockCount
mov dword ptr [esi+0Ch], 0
mov ebx, 1
mov eax, edi
lock xadd [eax], ebx
inc ebx
cmp ebx, 0FFFFFFFFh
jnz loc_7DEA8EB7
loc_7DE922B0:
pop edi
pop ebx
loc_7DE922B2:
xor eax, eax
pop esi
pop ebp
retn 4
... skipped
XADD指令功能是:交換并相加。這種情況下,LockCount加1并把結果保存到EBX寄存器,同時把1賦值給LockCount。這個操作是原子的,因為它使用了LOCK前綴,這意味著系統會阻塞其它CPU或CPU核心同時訪問這塊內存。
LOCK前綴是非常重要的:如果兩個線程,每個都工作在不同的CPU或CPU核心,它們都能夠進入critical section并修改內存數據,這種行為將導致不確定的后果。