逆向基礎 OS-specific (四)

左懶 · 2015/08/16 18:06c++

68章 Windows Nt


68.1 CRT(win32)


程序一開始就從main()函數執行的?事實並不是如此。若是咱們用IDA或者HIEW打開一個可執行文件,咱們能夠看到OEP(Original Entry Point)指向了其它代碼塊。這些代碼作了一些維護和準備工做以後再把控制流交給咱們的代碼。這就是所謂的startup-code或叫CRT code(C RunTime)。程序員

main()函數經過一個數組接收命令行傳遞過來的參數,環境變量與此相似。一般狀況下,傳遞一個字符串到程序以後,CRT code會用空格來分割它們。CRT code一樣也準備了一個envp來存放環境變量。若是是GUI版本的win32程序,入口函數須要使用WinMain()來代替main()函數,它也有本身的參數。shell

#!c
int CALLBACK WinMain(
    _In_ HINSTANCE hInstance,
    _In_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nCmdShow
);
複製代碼

CRT code一樣會準備好它所須要的全部參數。windows

此外,main()函數的返回值是它的退出碼。CRT code將它做爲ExitProcess()的參數。數組

一般,每一個編譯器都有它本身的CRT code。bash

下面是MSVC 2008特有的CRT code。cookie

#!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()函數。dom

有些函數調用了與自身相似的函數,如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上運行(當你運行它的時候會將先是一條錯誤信息)。更多的優化可能能夠提升執行效率,但如你所見,很快就出現了兼容問題。

68.2 Win32 PE


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程序塊將被執行打印。

68.2.1 術語

  • Module(模塊) - 一個exe/dll文件。
  • Process(進程) - 加載到內存中並正在運行的程序,一般由一個exe文件和多個dll文件組成。
  • Process memory(進程內存) - 進程所在容所。每一個進程都擁有本身的內存。一般是加載的模塊,棧內存,堆內存等等。
  • VA(虛擬地址) - 能夠被程序所使用的地址。
  • Base address(基地址) - 模塊被加載到進程內存後的地址。
  • RVA(相對虛擬地址) - VA地址減去基地址後的地址。PE文件中有許多地址使用RVA地址。
  • IAT(導入地址表)- 一個導入符號地址的數組。一般由一個IMAGE_DIRECTORY_ENTRY_IAT數據目錄指向IAT。值得注意的是,IDA可會給IAT分配一個名爲.idata的pseudo-section,即便IAT是其它section的一部分。
  • INT(導入名稱表) - 一個導入符號名的數組。

68.2.2 Base address

問題是,模塊(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。

68.2.3 Subsystem

還有一個subsystem字段, 一般是: - native (sys驅動程序) - console (控制檯程序) - GUI (圖形程序)

68.2.4 OS version

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。

68.2.5 Sections

一部分section彷佛存在於全部可執行文件格式裏面。

下面的標誌位用於區分代碼和常量數據:

  • 當IMAGE_SCN_CNT_CODE或IMAGE_SCN_MEM_EXECUTE被置位,表示該section是一個可執行代碼。
  • 在數據section中,IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE被置位。
  • 在未初始化section和空section中,IMAGE_SCN_CNT_UNINITIALIZED_DATA, IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE被置位。
  • 在常量數據section(寫保護)中,IMAGE_SCN_CNT_INITIALIZED_DATA和IMAGE_SCN_MEM_READ被置位,但不能夠置位 IMAGE_SCN_MEM_WRITE。當一個進程嘗試在這個section寫數據時,進程會崩潰掉。

每一個section在PE文件可能有一個名字,可是它並非很重要。一般(但不老是)代碼section的名字是.text,數據section是.data,常量數據section是.rdata(readable data)。其它流行的名字還有:

  • .idata—imports section(導入section)。IDA可能會建立一個相似(68.2.1)的pseudo-section。
  • .edata—exports section(導出section)。
  • .pdata—在Windows NT(MIPS,IA64,x64)包含了全部異常信息。
  • .reloc—relocs section(重定位section)
  • .bss—uninitialized data(未初始化數據(BSS))
  • .tls—thread local storage(線程局部存儲(TLS))
  • .rsrc—resources(資源)
  • .CRT—可能存在古老的MSVC版本編譯出來的二進制文件裏面。

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。

68.2.6 Relocations (relocs)

也稱爲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)。

68.2.7 Exports and imports

衆所周知,任何可執行文件都必須使用操做系統提供的服務和其它一些動態連接庫。

能夠說,一個模塊(一般是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而不是函數名。

Imports section

編譯器一般會給導入表及其相關內容分配一個單獨的section(名字相似.idata),但這不是一個強制規定。

由於術語混亂,導入表是一個比較使人困惑的地方。讓咱們嘗試一下整理這些信息。

Figure 68.1: A scheme that unites all PE-file structures related to imports

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文件不是所有加載到內存,它是「映射」和加載部分須要被訪問到的內存。可能微軟的開發者認爲這樣加載比較快。

68.2.8 Resources

資源在PE文件只是一組圖標,圖片,文本字符串,對話框描述。由於把它們從主代碼分離了出來,因此多國語言程序很容易實現,只須要根據操做系統設置的語言去選擇文本或圖片的語言。

做爲一個反作用,經過使用諸如ResHack的編輯器,即便在沒有專業知識的狀況下,也能夠輕鬆地編輯和保存可執行文件的資源。

68.2.9 .NET

.NET的程序並不編譯成機器碼,而是編譯成字節碼。嚴格地說,是在.exe文件裏面使用字節碼代替x86機器。然而,進入入口點(OEP)仍是須要一小段x86機器碼:

jmp mscoree.dll!_CorExeMain
複製代碼

.NET的加載器位於mscoree.dll,由它來處理PE文件。它存在於以前的全部Windows XP操做系統。從XP啓動的時候,OS的加載器可以探測.NET文件並經過JMP指令執行。

68.2.10 TLS

這個section包含了初始化TLS的數據(65章)(若是須要的話)。當一個新線程啓動的時候,它的TLS數據使用這個section的數據進行初始化。

除此以外,PE文件規範還提供了TLS的初始化!當section,TLS callbacks存在,它們會在傳遞控制權到主入口點(OEP)以前被調用。這個功能普遍用於PE文件的打包和加密。

68.2.11 工具

  • objdump - cygwin版本能夠反彙編PE文件
  • Hiew - (參考73章)
  • pefile - 一個處理PE文件的Python庫
  • ResHack AKA Resource Hacker — 資源編輯器
  • PE_add_import — 添加符號到導入表的簡易工具
  • PE_patcher — 修補PE文件的簡易工具
  • PE_search_str_refs — 查找函數在PE文件裏對應的字符串的簡易工具

68.2.12 擴展閱讀

Daniel Pistelli — The .NET File Format

68.3 Windows SEH


68.3.1 讓咱們先忘了MSVC

在Windows,SEH(Structured Exception Handling(結構化異常處理))是異常處理的一種機制。然而,它是語言無關的,無論是C++或者其它OOP語言。咱們能夠看到SEH(從C++和MSVC擴展)是獨立實現的。

每一個運行的進程都有一個SEH處理鏈,TIB有它最後的處理程序的地址。當異常發生時(除零,錯誤的地址訪問,用戶經過調用RaiseException()函數引起異常),操做系統在TIB找到最後的處理程序並調用它,獲取異常時CPU的狀態信息(如寄存器的值等等)。處理程序當前的異常可否修復,若是能,則修復該異常。若是不能,它通知操做系統沒法處理它並由操做系統調用異常處理鏈中的下一個處理程序,直處處理程序可以處理的異常被發現。

在異常處理鏈的結尾處有一個標準的處理程序,它顯示一個對話框用於通知用戶進程崩潰,而後把一些崩潰時CPU的狀態信息,收集起來並將其發送給微軟開發商。

Figure 68.2: Windows XP

Figure 68.2: Windows XP

Figure 68.3: Windows XP

Figure 68.3: Windows XP

Figure 68.4: Windows 7

Figure 68.4: Windows 7

Figure 68.5: Windows 8.1

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)。

Figure 68.5: Windows 8.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()。

68.3.2 如今讓咱們回到MSVC

聽說,微軟的程序員須要在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

SEH3有一個_except_handler3處理函數,並且擴展了_EXCEPTION_REGISTRATION表,並添加了一個指向scope table和previous try level變量。SEH4擴展了scope table緩衝溢出保護。

scope table是一個表,包含了指向filter和handler code的塊和每一個try/except嵌套。

scopetable

再者,操做系統只關心prev/handle字段。_except_handler3函數的工做是讀取其餘字段和scope table,並決定由哪些處理程序來執行。

_except_handler3函數的源代碼是閉源的。然而,Sanos操做系統的win32兼容性層從新實現相同的功能。其它相似的實現有Wine和ReactOS。

若是filter指針爲NULL,handler指針則指向finally代碼塊。

執行期間,棧中的previous try level變量發生變化,因此_except_handler3能夠獲取當前嵌套級的信息,才知道要使用scope table哪一表項。

SEH3: 一個try/except塊例子

#!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。

SEH3: 兩個try/except塊例子

#!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])
複製代碼

SEH4

在緩衝區攻擊期間(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。

seh4

這裏有兩個使用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。

68.3.3 Windows x64

正如你所認爲的,每一個函數序言在設置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還包含了幾乎全部函數的開始和結束地址,所以它可能對於自動化分析工具備用。

68.3.4 更多關於SEH的信息


Matt Pietrek. 「A Crash Course on the Depths of Win32™ Structured Exception Handling」. In: MSDN magazine (). URL: 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.

68.4 Windows NT: Critical section


臨界區在任何操做系統多線程環境中都是很是重要的,它保證一個線程在某一時刻訪問一些數據的時候,阻塞其它正要訪問這些數據的線程。

下面是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並修改內存數據,這種行爲將致使不肯定的後果。

相關文章
相關標籤/搜索