SEH分析筆記(X64篇)
阿新 • • 發佈:2018-11-25
v1.0.0
boxcounter
歷史:
v1.0.0, 2011-11-4:最初版本。
[不介意轉載,但請註明出處 www.boxcounter.com
附件裡有本文的原始稿,一樣的內容,更好的高亮和排版。
本文的部分程式碼可能會因為論壇的自動換行變得很亂,需要的朋友手動複製到自己的程式碼編輯器就可以正常顯示了]
在之前的《SEH分析筆記(X86篇)》中,我藉助 wrk1.2 介紹了 x86 下 windows 系統核心中的 SEH 實現。這次我們來看看 x64 位 windows 系統核心中 SEH 的實現。
本文需要大家熟悉 x64 位系統的一些特性,比如呼叫約定、Prolog 和 Epilog。可以通過這幾篇文章熟悉一下:
The history of calling conventions, part 5: amd64 , The Old New Thing
Everything You Need To Know To Start Programming 64-Bit Windows Systems, Matt Pietrek
首先回顧一下前一篇文章。
在 x86 windows 中,函式通過以下幾個步驟來參與 SEH :
1. 在自身的棧空間中分配並初始化一個 EXCEPTION_REGISTRATION(_RECORD) 結構體。
2. 將該 EXCEPTION_REGISTRATION(_RECORD) 掛入當前執行緒的異常連結串列。
當某函式觸發異常時,系統首先會通過呼叫 KiDispatchException 來給核心偵錯程式一個機會,如果核心偵錯程式沒有處理該異常,則該機會被轉給 RtlDispatchException,這個函式就開始分發該異常。分發過程為:
從當前執行緒的異常連結串列頭開始遍歷,對於每一個 SEH 註冊資訊(即 EXCEPTION_REGISTRATION(_RECORD)),呼叫其 Handler。根據 Handler 的返回值做相應的後續處理:
1. 返回 ExceptionContinueExecution,表示 Handler 已經修復了異常觸發點,從異常觸發點繼續執行。
2. 返回 ExceptionContinueSearch,表示該 Handler 沒有處理該異常,繼續遍歷異常連結串列。
3. Handler 沒有修復異常觸發點,但是卻能處理該異常(某個 __except 過濾程式碼返回 EXCEPTION_EXECUTE_HANDLER)。這種情況下,處理完該異常後就從異常解決程式碼(__except 程式碼塊)繼續執行,Handler 不會返回。
以上是簡略的 x86 SEH 流程,其中省略了很多細節,比如展開、錯誤處理、ExceptionNestedException 和 ExceptionCollidedUnwind 等等。
之所以在這裡重溫這個流程,是因為 x64 中 SEH 的流程總體思路也是如此,只是細節上做了一些修改。但這並不表示熟悉 x86 SEH 就能很輕鬆的掌握 x64 SEH。
本文分為四個部分:“異常註冊”、“異常分發”、“展開、解決”和“ExceptionNestedException 和 ExceptionCollidedUnwind”。依然以 MSC 的增強版為分析物件。分析環境為:WDK 7600.16385.1,內建的 cl 的版本是15.00.30729.207,link 的版本是9.00.30729.207,測試虛擬機器系統為 amd64 WinXP + wrk1.2。
在講述之前,需要先定義幾個名詞,以簡化後續的講述。
RVA —— 熟悉 PE 格式的朋友都懂的,表示某個絕對地址相對於所在模組的基地址的偏移。
EXCEPT_POINT —— 異常觸發點。
EXCEPT_FILTER —— __except 小括號內的異常過濾程式碼。
EXCEPT_HANDLER —— __except 大括號內的異常解決程式碼。
FINALLY_HANDLER —— __finally 大括號內的程式碼。
以下面的偽碼為例,
Code:
1 __try
2 {
3 __try
4 {
5 *((ULONG*)NULL) = 0;
6 }
7 __except((STATUS_INVALID_PARAMETER == GetExceptionCode()) ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER)
8 {
9 ...
10 }
11 }
12 __finally
13 {
14 ...
15 {
EXCEPT_POINT 指的是行5中的程式碼。EXCEPT_FILTER 指的是行7中的“(STATUS_INVALID_PARAMETER == GetExceptionCode()) ? EXCEPTION_CONTINUE_SEARCH : EXCEPTION_EXECUTE_HANDLER”。
EXCEPT_HANDLER 指的是行8到行10中所有的程式碼。
FINALLY_HANDLER 指的是行13到行15中所有的程式碼。
一、異常註冊
在 x64 windows 中,異常註冊資訊發生了巨大的改變。x86 中異常註冊資訊是在函式執行過程中在棧中分配並初始化的。x64 中變成這樣:
異常註冊資訊不再是動態建立,而是編譯過程中生成,連結時寫入 PE+ 頭中的 ExceptionDirectory(參考 winnt.h 中 IMAGE_RUNTIME_FUNCTION_ENTRY 的定義)。ExceptionDirectory 裡包含幾乎所有函式的棧操作、異常處理等資訊。
來看看新異常註冊資訊的資料結構:
Code:
typedef struct _RUNTIME_FUNCTION {
ULONG BeginAddress;
ULONG EndAddress;
ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
typedef enum _UNWIND_OP_CODES {
UWOP_PUSH_NONVOL = 0,
UWOP_ALLOC_LARGE, // 1
UWOP_ALLOC_SMALL, // 2
UWOP_SET_FPREG, // 3
UWOP_SAVE_NONVOL, // 4
UWOP_SAVE_NONVOL_FAR, // 5
UWOP_SPARE_CODE1, // 6
UWOP_SPARE_CODE2, // 7
UWOP_SAVE_XMM128, // 8
UWOP_SAVE_XMM128_FAR, // 9
UWOP_PUSH_MACHFRAME // 10
} UNWIND_OP_CODES, *PUNWIND_OP_CODES;
typedef union _UNWIND_CODE {
struct {
UCHAR CodeOffset;
UCHAR UnwindOp : 4;
UCHAR OpInfo : 4;
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;
#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4
typedef struct _UNWIND_INFO {
UCHAR Version : 3;
UCHAR Flags : 5;
UCHAR SizeOfProlog;
UCHAR CountOfCodes;
UCHAR FrameRegister : 4;
UCHAR FrameOffset : 4;
UNWIND_CODE UnwindCode[1];
//
// The unwind codes are followed by an optional DWORD aligned field that
// contains the exception handler address or a function table entry if
// chained unwind information is specified. If an exception handler address
// is specified, then it is followed by the language specified exception
// handler data.
//
// union {
// struct {
// ULONG ExceptionHandler;
// ULONG ExceptionData[];
// };
//
// RUNTIME_FUNCTION FunctionEntry;
// };
//
} UNWIND_INFO, *PUNWIND_INFO;
typedef struct _SCOPE_TABLE {
ULONG Count;
struct
{
ULONG BeginAddress;
ULONG EndAddress;
ULONG HandlerAddress;
ULONG JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;
x64 中,MSC 為幾乎所有的函式都登記了完備的資訊,用來在展開過程中完整的回滾函式所做的棧、暫存器操作。登記的資訊包括:函式是否使用了 SEH、
函式使用的是什麼組合的 SEH(__try/__except?__try/__finally?)、
函式申請了多少棧空間、
函式儲存了哪些暫存器、
函式是否建立了棧幀,
等等,
同時也記錄了這些操作的順序(以保證回滾的時候不會亂套)。
這些資訊就儲存在 UNWIND_INFO 之中。
UNWIND_INFO 相當於 x86 下的 EXCEPTION_REGISTRATION。它的成員分別是:
Version —— 結構體的版本。
Flags —— 標誌位,可以有這麼幾種取值:
UNW_FLAG_NHANDLER (0x0): 表示既沒有 EXCEPT_FILTER 也沒有 EXCEPT_HANDLER。
UNW_FLAG_EHANDLER (0x1): 表示該函式有 EXCEPT_FILTER & EXCEPT_HANDLER。
UNW_FLAG_UHANDLER (0x2): 表示該函式有 FINALLY_HANDLER。
UNW_FLAG_CHAININFO (0x4): 表示該函式有多個 UNWIND_INFO,它們串接在一起(所謂的 chain)。
SizeOfProlog —— 表示該函式的 Prolog 指令的大小,單位是 byte。
CountOfCodes —— 表示當前 UNWIND_INFO 包含多少個 UNWIND_CODE 結構。
FrameRegister —— 如果函式建立了棧幀,它表示棧幀的索引(相對於 CONTEXT::RAX 的偏移,詳情參考 RtlVirtualUnwind 原始碼)。否則該成員的值為0。
FrameOffset —— 表示 FrameRegister 距離函式最初棧頂(剛進入函式,還沒有執行任何指令時的棧頂)的偏移,單位也是 byte。
UnwindCode —— 是一個 UNWIND_CODE 型別的陣列。元素數量由 CountOfCodes 決定。
需要說明幾點:
1. 如果 Flags 設定了 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER,那麼在最後一個 UNWIND_CODE 之後存放著 ExceptionHandler(相當於 x86 EXCEPTION_REGISTRATION::handler)和 ExceptionData(相當於 x86 EXCEPTION_REGISTRATION::scopetable)。
2. UnwindCode 陣列詳細記錄了函式修改棧、儲存非易失性暫存器的指令。
3. MSDN 中有 UNWIND_INFO 和 UNWIND_CODE 的詳細說明,推薦閱讀。
那 UNWIND_INFO 是如何與其描述的函式關聯起來的呢?答案是:通過一個 RUNTIME_FUNCTION 結構體。
RUNTIME_FUNCTION::BeginAddress 同 RUNTIME_FUNCTION::EndAddress 一起以 RVA 形式描述了函式的範圍。
RUNTIME_FUNCTION::UnwindData 就是 UNWIND_INFO 了,它也是一個 RVA 值。
PE+ 中的 ExceptionDirectory 中存放著所有函式的 RUNTIME_FUNCTION,按 RUNTIME_FUNCTION::BeginAddress 升序排列。一旦觸發異常,系統可以通過 EXCEPT_POINT 的 RVA 在 ExceptionDirectory 中二分查詢到 RUNTIME_FUNCTION,進而找到 UNWIND_INFO。
前面有提到,MSC 為幾乎所有的函式都登記了完畢的資訊,那是不是有一些特殊函式沒有登記資訊呢?
是的。x64 新增了一個概念,叫做“葉函式”。熟悉資料結構的朋友可能第一時間就聯想到“葉節點”。沒錯,“葉函式”的含義跟“葉節點”很類似,葉函式不會有子函式,也就是說它不會再呼叫任何函式。另外 x64 對這個概念額外加了一些要求:不修改棧指標(比如分配棧空間)、沒有使用 SEH。總結下來就是:既不呼叫函式、又沒有修改棧指標,也沒有使用 SEH 的函式就叫做“葉函式”。
葉函式可以沒有登記資訊,原因很簡單,它根本就沒資訊需要登記~
還有一個 SCOPE_TABLE 結構,熟悉 x86 SEH 的朋友應該很眼熟 :-),它等同於 x86 SEH 中的 REGISTRATIOIN_RECORD::scopetable 的型別。其成員有:
Count —— 表示 ScopeRecord 陣列的大小。
ScopeRecord —— 等同於 x86 中的 scopetable_entry 成員。其中,
BeginAddress 和 EndAddress 表示某個 __try 保護域的範圍。
HandlerAddress 和 JumpTarget 表示 EXCEPTION_FILTER、EXCEPT_HANDLER 和 FINALLY_HANDLER。具體對應情況為:
對於 __try/__except 組合,HandlerAddress 代表 EXCEPT_FILTER,JumpTarget 代表 EXCEPT_HANDLER。
對於 __try/__finally 組合,HandlerAddress 代表 FINALLY_HANDLER,JumpTarget 等於 0。
這四個域通常都是 RVA,但當 EXCEPT_FILTER 簡單地返回或等於 EXCEPTION_EXECUTE_HANDLER 時,HandlerAddress 可能直接等於 EXCEPTION_EXECUTE_HANDLER,而不再是一個 RVA。
我們可以通過 windbg 中的 .fnent 命令來檢視某個函式的異常註冊資訊。比如,
1 kd> .fnent passThrough!SehTest
2 Debugger function entry 00000000`00778210 for:
3 d:\workspace\code\mycode\r0\passthrough\passthrough.c(51)
4 (fffffadf`f140f020) PassThrough!SehTest | (fffffadf`f140f0c0) PassThrough!Caller2
5 Exact matches:
6 PassThrough!SehTest (void)
7
8 BeginAddress = 00000000`00001020
9 EndAddress = 00000000`000010b2
10 UnwindInfoAddress = 00000000`00002668
11
12 Unwind info at fffffadf`f1410668, 10 bytes
13 version 1, flags 1, prolog 4, codes 1
14 handler routine: PassThrough!_C_specific_handler (fffffadf`f140f4ce), data 3
15 00: offs 4, unwind op 2, op info 4 UWOP_ALLOC_SMALL.
行8到行10描述的是 RUNTIME_FUNCTION。
行12到行15描述的是 UNWIND_INFO。
對於葉函式,輸出是這樣的,
kd> .fnent passthrough!LeafTest
No function entry for fffffadf`f240c080
到這裡,異常註冊就講完了,我們認識了相關的資料結構和定位方法。下面我們進入異常分發流程。
二、異常分發
x64 異常分發過程使用的仍然是 KiDispatchException、RtlDispatchException、RtlpExecuteHandlerForException 等函式。
其中,KiDispatchException 中有關核心異常部分的程式碼完全沒有變化,這裡我偷懶直接拷貝《SEH分析筆記(X86篇)》中的部分描述,
原型: Code:
VOID
KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
);
對於核心異常,它的處理步驟如下:如果 FirstChance 為 TRUE,那麼,
1. 首先將該異常傳達給核心偵錯程式(KD),如果 KD 處理了該異常,那麼函式返回。
2. KD 沒有處理,呼叫 RtlDispatchException 進行異常分發。如果分發成功,RtlDispatchExcetpion 返回 TRUE,或者根本不返回。
3. RtlDispatchException 分發失敗,那麼再給 KD 一次處理機會,如果還是沒有處理,那麼 BUGCHECK。
如果 FirstChance 為 FALSE,那麼將該異常傳達給 KD,如果 KD 沒有處理,那麼 BUGCHECK。
它的原始碼實現位於 $WRK-v1.2\base\ntos\ke\amd64\exceptn.c:430。
RtlDispatchException 發生了一些改變。
從之前描述的異常註冊資訊的資料結構可以發現,x64 中已經不存在異常註冊鏈這個概念了(雖然 _NT_TIB 結構體中還保留了 ExceptionList 域)。那 x64 中 RtlDispatchException 是如何遍歷異常註冊資訊的呢?前面雖然有提到如何通過 EXCEPT_POINT 找到 UNWIND_INFO 結構,但是假如這個 UNWIND_INFO 沒有處理該異常,如何繼續遍歷呢?
在解答這個問題之前,我們先來認識一個新函式和相關的資料結構,
Code:
#define UNWIND_HISTORY_TABLE_SIZE 12
typedef struct _UNWIND_HISTORY_TABLE_ENTRY {
ULONG64 ImageBase;
PRUNTIME_FUNCTION FunctionEntry;
} UNWIND_HISTORY_TABLE_ENTRY, *PUNWIND_HISTORY_TABLE_ENTRY;
#define UNWIND_HISTORY_TABLE_NONE 0
#define UNWIND_HISTORY_TABLE_GLOBAL 1
#define UNWIND_HISTORY_TABLE_LOCAL 2
typedef struct _UNWIND_HISTORY_TABLE {
ULONG Count;
UCHAR Search;
ULONG64 LowAddress;
ULONG64 HighAddress;
UNWIND_HISTORY_TABLE_ENTRY Entry[UNWIND_HISTORY_TABLE_SIZE];
} UNWIND_HISTORY_TABLE, *PUNWIND_HISTORY_TABLE;
PRUNTIME_FUNCTION
RtlLookupFunctionEntry (
IN ULONG64 ControlPc,
OUT PULONG64 ImageBase,
IN OUT PUNWIND_HISTORY_TABLE HistoryTable OPTIONAL
);
RtlLookupFunctionEntry 的功能是查詢指定地址所在函式的 RUNTIME_FUNCTION 和所在模組的基地址。它的引數分為為,ControlPc —— 需要查詢的指令地址,
ImageBase —— 返回的模組基地址,
HistoryTable —— 用於加速查詢。
工作流程:
RtlLookupFunctionEntry 在搜尋的過程會根據是否傳入 HistoryTable 而採取不同的搜尋方法:
如果傳入 HistoryTable,則根據 HistoryTable->Search 表示的搜尋方式在表中進行搜尋:
如果搜尋方式為 UNWIND_HISTORY_TABLE_NONE,那麼不在 HistoryTable 中進行搜尋。
如果搜尋方式為 UNWIND_HISTORY_TABLE_GLOBAL,則首先在全域性表 RtlpUnwindHistoryTable 開始搜尋,如果搜尋到,則結束搜尋,函式返回。否則再在 HistoryTable 中搜索。
如果搜尋方式為 UNWIND_HISTORY_TABLE_LOCAL,那麼在 HistoryTable 中搜索。
如果上述過程中沒有搜尋到需要的結果,那麼找到模組基地址,從模組的 PE+ 頭結構中解析出 RUNTIME_FUNCTION。如果搜尋方式為 UNWIND_HISTORY_TABLE_NONE,還會將解決加入到 HistoryTable。
之前的描述中 RtlDispatchException 定位 EXCEPT_POINT 所對應的 RUNTIME_FUNCTION 就是通過呼叫 RtlLookupFunctionEntry 實現的。
回到剛才的問題,如何推動遍歷呢?為了解決這個問題,x64 又引進了一個新函式。現在我們有請 x64 SEH 核心成員 RtlVirtualUnwind 登場~
先來看看它的原型: Code:
PEXCEPTION_ROUTINE
RtlVirtualUnwind (
IN ULONG HandlerType,
IN ULONG64 ImageBase,
IN ULONG64 ControlPc,
IN PRUNTIME_FUNCTION FunctionEntry,
IN OUT PCONTEXT ContextRecord,
OUT PVOID *HandlerData,
OUT PULONG64 EstablisherFrame,
IN OUT PKNONVOLATILE_CONTEXT_POINTERS ContextPointers OPTIONAL
);
它的主要功能是:根據傳入的 ControlPc 和 ContextRecord 等引數虛擬(模擬)展開該函式,並返回該函式的一些資訊,比如 HandlerData(SCOPE_TABLE)、EstablisherFrame(rsp 或 棧幀)。
流程是:
1. 通過 FunctionEntry 和 ImageBase 找到 UNWIND_INFO。根據 UNWIND_INFO 中記錄的資訊,查詢 EstablisherFrame(即棧幀或者 rsp)。
2. 根據 ControlPc 分如下兩種情況展開:
a. ControlPc >= EpilogOffset,即 ControlPc 在 Epilog 之中。那麼把剩餘的 Epilog 指令模擬執行完畢即可。
所謂模擬是指,如果下一條 EpiLog 指令是“sub rsp, 0x32”,那麼將 ContextRecord->rsp 減去0x32。並不是真正執行 sub 指令。
b. ControlPc < EpilogOffset,那麼把 Prolog 反向模擬回滾一遍即可。
所謂反向回滾是指,如果 Prolog 的指令是
mov [RSP + 8], RCX
push R15
push R14
push R13
那麼反向回滾就是
pop ContextRecord->R13 (實際上是從 ContextRecord->Rsp 指向的記憶體中取出值存入 ContextRecord->R13,然後 ContextRecord->Rsp 加上8。並不是真正執行 pop)
pop ContextRecord->R14
pop ContextRecord->R15
mov ContextRecord->RCX, [ContextRecord->RSP+8]
然後把 ContextRecord->Rip 修改為 ControlPc 所在函式的返回地址,即父函式中的某一處 call 的下一條指令。
這樣,ContextRecord 就被恢復成父函式在呼叫 ControlPc 所在函式之後的狀態了。
3. 如果 HandlerType 包含 UNW_FLAG_EHANDLER 或 UNW_FLAG_UHANDLER,那麼將 UNWIND_INFO::ExceptionData 賦給傳出引數 HandlerData,並返回 UNWIND_INFO::ExceptionRoutine。
對於 MSC 編譯器生成的模組,UNWIND_INFO::ExceptionRoutine 一般指向 nt!__C_specific_handler。UNWIND_INFO::ExceptionData 指向 ControlPc 所在函式的 SCOPE_TABLE。
RtlVirtualUnwind 的實現原始碼位於 $WRK-v1.2\base\ntos\rtl\amd64\exdsptch.c:1202。
RtlVirtualUnwind 返回後,RtlDispatchException 就可以根據 ContextRecord->Rip 找到父函式對應的 RUNTIME_FUNCTION,進而找到 UNWIND_INFO。就這樣推動整個遍歷過程。
這是一般情況,對於沒有 UNWIND_INFO 的葉函式呢?
對於葉函式,RtlLookupFunctionEntry 返回 NULL,於是 RtlDispatchException 知道這是個葉函式,就找到該葉函式的父函式,從父函式繼續遍歷。也就是完全無視葉函式,因為葉函式對整個異常處理過程沒有任何影響。
RtlDispatchException 呼叫 UNWIND_INFO::ExceptionHandler 依然是通過 RtlpExecuteHandlerForException,其函式原型沒有變化:
Code:
EXCEPTION_DISPOSITION
RtlpExecuteHandlerForException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PVOID EstablisherFrame,
IN OUT PCONTEXT ContextRecord,
IN OUT PVOID DispatcherContext
);
該函式的實現原始碼位於 $\WRK-v1.2\base\ntos\rtl\amd64\xcptmisc.asm:84。RtlpExecuteHandlerForException 的邏輯較 x86 版本沒什麼大變化,內部註冊了一個異常處理函式 RtlpExceptionHandler。RtlpExceptionHandler 相當於 x86 中的 nt!ExecuteHandler2,其內部會返回 ExceptionNestedException 或 ExceptionContinueSearch。它的實現原始碼位於 $\WRK-v1.2\base\ntos\rtl\amd64\xcptmisc.asm:26。
需要一提的是,最後一個引數 DispatchContext 的型別是 DISPATCHER_CONTEXT,相對於 x86 版本,它擴充了很多,
Code:
typedef struct _DISPATCHER_CONTEXT {
ULONG64 ControlPc;
ULONG64 ImageBase;
PRUNTIME_FUNCTION FunctionEntry;
ULONG64 EstablisherFrame;
ULONG64 TargetIp;
PCONTEXT ContextRecord;
PEXCEPTION_ROUTINE LanguageHandler;
PVOID HandlerData;
PUNWIND_HISTORY_TABLE HistoryTable;
ULONG ScopeIndex;
ULONG Fill0;
} DISPATCHER_CONTEXT, *PDISPATCHER_CONTEXT;
成員分別為:ControlPc —— 異常觸發點。
ImagePase —— ControlPc 所在模組的基地址。
FunctionEntry —— ControlPc 所在函式的 RUNTIME_FUNCTION。
EstablisherFrame —— ControlPc 所在函式的棧幀(如果建立了棧幀)或 RSP。
TargetIp —— 解決異常的 EXCEPT_HANDLER 地址,該成員只在展開的過程中被使用。RtlpExecuteHandlerForException 沒有使用它。
ContextRecord —— 供展開過程中使用,只有當展開過程中觸發新異常(返回 ExceptionCollidedUnwind)時,才會被 RtlDispatchException 真正的使用到(參考 RtlDispatchException 處理 ExceptionCollidedUnwind 的程式碼)。
LanguageHandler —— ControlPc 所在函式的 UNWIND_INFO::ExceptionRoutine。
HandlerData —— ControlPc 所在函式的 UNWIND_INFO::ExceptionData。
ScopeIndex —— UNWIND_INFO::ExceptionData 中 SCOPE_TABLE::ScopeRecord 的索引,通常設定為0(注:請不要與 x86 中執行時不斷改變的 EXCEPTION_REGISTRATION::trylevel 相混淆,ScopeIndex 不會在在函式執行過程中改變)
Fill0 —— 未用。
再看一下它的 .fnent 輸出,
1 kd> .fnent nt!RtlpExecuteHandlerForException
2 Debugger function entry 00000000`01458210 for:
3 (fffff800`008bd950) nt!RtlpExecuteHandlerForException | (fffff800`008bd970) nt!RtlpUnwindHandler
4 Exact matches:
5 nt!RtlpExecuteHandlerForException (void)
6
7 BeginAddress = 00000000`000bd950
8 EndAddress = 00000000`000bd963
9 UnwindInfoAddress = 00000000`000dfeb8
10
11 Unwind info at fffff800`008dfeb8, 10 bytes
12 version 1, flags 3, prolog 4, codes 1
13 handler routine: nt!RtlpExceptionHandler (fffff800`008bd920), data 0
14 00: offs 4, unwind op 2, op info 4 UWOP_ALLOC_SMALL.
行12中顯示 flags 等於3,即 UNW_FLAG_EHANDLER (0x1) | UNW_FLAG_UHANDLER (0x2),說明行13中顯示的異常處理函式 nt!RtlpExceptionHandler 既負責解決異常,也負責展開。
RtlpExecuteHandlerForException 會呼叫 DISPATCHER_CONTEXT::LanguageHandler。對於 MSC 編譯得到的模組,它是 nt!__C_specific_handler,我們來看看這個函式,
原型: Code:
EXCEPTION_DISPOSITION
__C_specific_handler (
IN PEXCEPTION_RECORD pExceptionRecord,
IN PVOID pEstablisherFrame,
IN OUT PCONTEXT pContext,
IN OUT PVOID pDispatcherContext
);
反彙編碼:
Code:
kd> uf nt!__C_specific_handler
nt!__C_specific_handler:
fffff800`008a42d0 mov qword ptr [rsp+10h],rdx ; 在棧上儲存 pEstablisherFrame
fffff800`008a42d5 mov rax,rsp
fffff800`008a42d8 sub rsp,88h
fffff800`008a42df mov qword ptr [rax-8],rbx
fffff800`008a42e3 mov qword ptr [rax-10h],rbp
fffff800`008a42e7 mov rbp,qword ptr [r9] ; rbp = pDispatcherContext->ControlPc
fffff800`008a42ea mov qword ptr [rax-18h],rsi
fffff800`008a42ee mov qword ptr [rax-20h],rdi
fffff800`008a42f2 mov qword ptr [rax-28h],r12
fffff800`008a42f6 mov r12,qword ptr [r9+38h] ; r12 = pDispatcherContext->HandlerData
fffff800`008a42fa mov qword ptr [rax-30h],r13
fffff800`008a42fe mov qword ptr [rax-38h],r14
fffff800`008a4302 mov r14,qword ptr [r9+8] ; r14 = pDispatcherContext->ImageBase
fffff800`008a4306 mov qword ptr [rax-40h],r15
fffff800`008a430a mov r13,r9 ; r13 = pDispatcherContext
fffff800`008a430d sub rbp,r14 ; l_OffsetInFunc = pDispatcherContext->ControlPc - pDispatcherContext->ImageBase
fffff800`008a4310 test byte ptr [rcx+4],66h ; pExceptionRecord->ExceptionFlags, EXCEPTION_UNWIND (0x66)
fffff800`008a4314 mov rsi,rdx ; rsi = pEstablisherFrame
fffff800`008a4317 mov r15,rcx ; r15 = pExceptionRecord
< fffff800`008a431a jne nt!__C_specific_handler+0xf5 (fffff800`008a43c5)
:
: -------------------------------------------------------------------
: nt!__C_specific_handler+0x50:
: fffff800`008a4320 movsxd rdi,dword ptr [r9+48h] ; l_ScopeIndex (rdi) = pDispatcherContext->ScopeIndex
: fffff800`008a4324 mov qword ptr [rax-58h],rcx ; [rax-58h] = pExceptionRecord,供給 GetExceptionCode(Information) 使用
: fffff800`008a4328 mov qword ptr [rax-50h],r8 ; [rax-50h] = pContext,供給 GetExceptionCode(Information) 使用
: fffff800`008a432c cmp edi,dword ptr [r12] ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
: fffff800`008a4330 mov rax,rdi ; rax = l_ScopeIndex
:< fffff800`008a4333 jae nt!__C_specific_handler+0x166 (fffff800`008a4436)
::
:: nt!__C_specific_handler+0x69:
:: fffff800`008a4339 add rax,rax ; 這裡 *2,下面緊接著 *8,目的是跳過指定數目的 ScopeRecord(大小為16位元組)
:: fffff800`008a433c lea rbx,[r12+rax*8+0Ch] ; rbx = &(pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress)
::
:: nt!__C_specific_handler+0x71:
:: ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟一
:: > fffff800`008a4341 mov eax,dword ptr [rbx-8] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
:: : fffff800`008a4344 cmp rbp,rax ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::< : fffff800`008a4347 jb nt!__C_specific_handler+0xdd (fffff800`008a43ad)
::: :
::: : nt!__C_specific_handler+0x79:
::: : ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟二
::: : fffff800`008a4349 mov eax,dword ptr [rbx-4] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
::: : fffff800`008a434c cmp rbp,rax ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::< : fffff800`008a434f jae nt!__C_specific_handler+0xdd (fffff800`008a43ad)
:::: :
:::: : nt!__C_specific_handler+0x81:
:::: : ; 判斷是否是 __try/__finally(JumpTarget 為 NULL)。如果是,那麼跳轉到下一個 ScopeRecord 繼續遍歷。
:::: : fffff800`008a4351 cmp dword ptr [rbx+4],0 ; cmp pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget, NULL
::::< : fffff800`008a4355 je nt!__C_specific_handler+0xdd (fffff800`008a43ad)
::::: :
::::: : nt!__C_specific_handler+0x87:
::::: : ; 到這裡,已經找到與異常地址最匹配的 __try/__except
::::: : fffff800`008a4357 mov eax,dword ptr [rbx] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress
::::: : fffff800`008a4359 cmp eax,1 ; cmp pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress, EXCEPTION_EXECUTE_HANDLER (0x1)
:::::< : fffff800`008a435c je nt!__C_specific_handler+0xa3 (fffff800`008a4373) ; 如果返回 EXCEPTION_EXECUTE_HANDLER 則跳轉
:::::: :
:::::: : nt!__C_specific_handler+0x8e:
:::::: : ; 是 __try/__except,且過濾域並不是 EXCEPTION_EXECUTE_HANDLER,執行 HandlerAddress
:::::: : ; (注:HandlerAddress 指向的函式仍有可能會返回 EXCEPTION_EXECUTE_HANDLER)
:::::: : fffff800`008a435e lea rcx,[rsp+30h]
:::::: : fffff800`008a4363 add rax,r14 ; rax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].HandlerAddress + pDispatcherContext->ImageBase
:::::: : fffff800`008a4366 mov rdx,rsi ; rdx = pEstablisherFrame
:::::: : fffff800`008a4369 call rax ; 呼叫 EXCEPT_FILTER
:::::: : fffff800`008a436b test eax,eax
::::::< : fffff800`008a436d js nt!__C_specific_handler+0xee (fffff800`008a43be) ; 返回 EXCEPTION_CONTINUE_EXECUTION (-1) 則跳轉
::::::: :
::::::: : nt!__C_specific_handler+0x9f:
::::::: : fffff800`008a436f test eax,eax
:::::::<: fffff800`008a4371 jle nt!__C_specific_handler+0xdd (fffff800`008a43ad) ; 返回 EXCEPTION_CONTINUE_SEARCH (0) 則跳轉
:::::::::
::::::::: nt!__C_specific_handler+0xa3:
::::::::: ; 返回的是 EXCEPTION_EXECUTE_HANDLER
:::::>::: fffff800`008a4373 mov ecx,dword ptr [rbx+4] ; ecx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
::::: ::: fffff800`008a4376 mov r8d,1
::::: ::: fffff800`008a437c mov rdx,rsi ; rdx = pEstablisherFrame
::::: ::: fffff800`008a437f add rcx,r14 ; rcx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget + pDispatcherContext->ImageBase
::::: ::: fffff800`008a4382 call nt!_NLG_Notify (fffff800`008b1460)
::::: ::: fffff800`008a4387 mov rax,qword ptr [r13+40h] ; rax = pDispatcherContext->HistoryTable
::::: ::: fffff800`008a438b mov edx,dword ptr [rbx+4] ; edx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
::::: ::: fffff800`008a438e movsxd r9,dword ptr [r15] ; r9 = pExceptionRecord->ExceptionCode
::::: ::: fffff800`008a4391 mov qword ptr [rsp+28h],rax ; _ARG_6 = pDispatcherContext->HistoryTable
::::: ::: fffff800`008a4396 mov rax,qword ptr [r13+28h] ; rax = pDispatcherContext->ContextRecord
::::: ::: fffff800`008a439a add rdx,r14 ; rdx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget + pDispatcherContext->ImageBase
::::: ::: fffff800`008a439d mov r8,r15 ; r8 = pExceptionRecord
::::: ::: fffff800`008a43a0 mov rcx,rsi ; rcx = pEstablisherFrame
::::: ::: fffff800`008a43a3 mov qword ptr [rsp+20h],rax ; _ARG_5 = pDispatcherContext->ContextRecord
::::: ::: fffff800`008a43a8 call nt!RtlUnwindEx (fffff800`00891e80) ; 這裡不會返回
::::: ::: ; RtlUnwindEx(pEstablisherFrame,
::::: ::: ; pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget + pDispatcherContext->ImageBase
::::: ::: ; pExceptionRecord,
::::: ::: ; pExceptionRecord->ExceptionCode
::::: ::: ; pDispatcherContext->ContextRecord,
::::: ::: ; pDispatcherContext->HistoryTable)
::::: :::
::::: ::: nt!__C_specific_handler+0xdd:
::>>> :>: fffff800`008a43ad inc edi ; l_ScopeIndex += 1
:: : : fffff800`008a43af add rbx,10h ; 調整到下一個 ScopeRecord::HandlerAddress
:: : : fffff800`008a43b3 cmp edi,dword ptr [r12] ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
:: : < fffff800`008a43b7 jb nt!__C_specific_handler+0x71 (fffff800`008a4341)
:: :
:: : nt!__C_specific_handler+0xe9:
:: : ; pDispatcherContext->HandlerData 遍歷完畢
::< : fffff800`008a43b9 jmp nt!__C_specific_handler+0x166 (fffff800`008a4436)
::: :
::: : nt!__C_specific_handler+0xee:
::: > fffff800`008a43be xor eax,eax ; eax = ExceptionContinueExecution
:::< fffff800`008a43c0 jmp nt!__C_specific_handler+0x16b (fffff800`008a443b)
::::
:::: -------------------------------------------------------------------------------------
:::: nt!__C_specific_handler+0xf5:
:::: ; 設定了 EXCEPTION_UNWIND,當前是展開過程
>::: fffff800`008a43c5 movsxd rdi,dword ptr [r9+48h] ; l_ScopeIndex (rdi) = pDispatcherContext->ScopeIndex
::: fffff800`008a43c9 mov rsi,qword ptr [r9+20h] ; rsi = pDispatcherContext->TargetIp
::: fffff800`008a43cd sub rsi,r14 ; rsi = pDispatcherContext->TargetIp - pDispatcherContext->ImageBase
::: fffff800`008a43d0 cmp edi,dword ptr [r12] ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
::: fffff800`008a43d4 mov rax,rdi ; rax = l_ScopeIndex
:::< fffff800`008a43d7 jae nt!__C_specific_handler+0x166 (fffff800`008a4436)
::::
:::: nt!__C_specific_handler+0x109:
:::: fffff800`008a43d9 add rax,rax ;
:::: fffff800`008a43dc lea rbx,[r12+rax*8+8] ; rbx = &(pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress)
::::
:::: nt!__C_specific_handler+0x111:
:::: ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟一
:::: > fffff800`008a43e1 mov eax,dword ptr [rbx-4] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
:::: : fffff800`008a43e4 cmp rbp,rax ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::::< : fffff800`008a43e7 jb nt!__C_specific_handler+0x15a (fffff800`008a442a)
::::: :
::::: : nt!__C_specific_handler+0x119:
::::: : ; 檢查 ControlPc 處於哪個 __try 保護域,之步驟二
::::: : fffff800`008a43e9 mov ecx,dword ptr [rbx] ; ecx = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
::::: : fffff800`008a43eb cmp rbp,rcx ; cmp l_OffsetInFunc, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::::< : fffff800`008a43ee jae nt!__C_specific_handler+0x15a (fffff800`008a442a)
:::::: :
:::::: : nt!__C_specific_handler+0x120:
:::::: : ; 到這裡,已經找到與異常地址匹配的最內層(如果有多層) __try/__except
:::::: : fffff800`008a43f0 cmp rsi,rax ; cmp pDispatcherContext->TargetIp - pDispatcherContext->ImageBase, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].BeginAddress
::::::< : fffff800`008a43f3 jb nt!__C_specific_handler+0x131 (fffff800`008a4401)
::::::: :
::::::: : nt!__C_specific_handler+0x125:
::::::: : fffff800`008a43f5 cmp rsi,rcx ; cmp pDispatcherContext->TargetIp - pDispatcherContext->ImageBase, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].EndAddress
:::::::< : fffff800`008a43f8 ja nt!__C_specific_handler+0x131 (fffff800`008a4401)
:::::::: :
:::::::: : nt!__C_specific_handler+0x12a:
:::::::: : ; 如果標記了 EXCEPTION_TARGET_UNWIND,說明是最後一個需要區域性展開的函式。但是該次區域性展開只展開到 EXCEPT_HANDLER(不包含 EXCEPT_HANDLER),所以需要判斷 TargetIp
:::::::: : fffff800`008a43fa test byte ptr [r15+4],20h ; test pExceptionRecord->ExceptionFlags, EXCEPTION_TARGET_UNWIND (0x20)
::::::::< : fffff800`008a43ff jne nt!__C_specific_handler+0x166 (fffff800`008a4436)
::::::::: :
::::::::: : nt!__C_specific_handler+0x131:
::::::>>: : fffff800`008a4401 mov eax,dword ptr [rbx+8] ; eax = pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
:::::: : : fffff800`008a4404 test eax,eax ; 判斷 pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget 是否為 NULL,即是否是 __try/__finally
:::::: :< : fffff800`008a4406 je nt!__C_specific_handler+0x13f (fffff800`008a440f) ; 如果是 __try/__finally 則跳轉
:::::: :: :
:::::: :: : nt!__C_specific_handler+0x138:
:::::: :: : fffff800`008a4408 cmp rsi,rax ; cmp pDispatcherContext->TargetIp, pDispatcherContext->HandlerData->ScopeRecord[l_ScopeIndex].JumpTarget
:::::: ::< : fffff800`008a440b je nt!__C_specific_handler+0x166 (fffff800`008a4436)
:::::: ::: :
:::::: ::: : nt!__C_specific_handler+0x13d:
:::::: :::<: fffff800`008a440d jmp nt!__C_specific_handler+0x15a (fffff800`008a442a)
:::::: :::::
:::::: ::::: nt!__C_specific_handler+0x13f:
:::::: ::::: ; 注意這裡是先修改 pDispatcherContext->ScopeIndex,然後呼叫 EXCEPT_HANDLER。這樣如果 EXCEPT_HANDLER 觸發異常,後續展開就會跳過這個 EXCEPT_HANDLER。
:::::: :>::: fffff800`008a440f mov rdx,qword ptr [rsp+98h]
:::::: : ::: fffff800`008a4417 lea eax,[rdi+1] ; eax = l_ScopeIndex + 1
:::::: : ::: fffff800`008a441a mov cl,1
:::::: : ::: fffff800`008a441c mov dword ptr [r13+48h],eax ; pDispatcherContext->ScopeIndex = eax
:::::: : ::: fffff800`008a4420 mov r8d,dword ptr [rbx+4] ; r8d = pDispatcherContext->HandlerData->ScopeRecord[i].HandlerAddress
:::::: : ::: fffff800`008a4424 add r8,r14 ; r8 = pDispatcherContext->HandlerData->ScopeRecord[i].HandlerAddress + pDispatcherContext->ImageBase
:::::: : ::: fffff800`008a4427 call r8 ; 呼叫 __finally 處理塊,會返回(注:對於 __try/__finally,HandlerAddress 儲存的是 __finally 程式碼塊的 RVA)
:::::: : :::
:::::: : ::: nt!__C_specific_handler+0x15a:
::::>> : :>: fffff800`008a442a inc edi ; l_ScopeIndex += 1
:::: : : : fffff800`008a442c add rbx,10h ; 調整到下一個 ScopeRecord::HandlerAddress
:::: : : : fffff800`008a4430 cmp edi,dword ptr [r12] ; cmp l_ScopeIndex, pDispatcherContext->HandlerData->Count
:::: : : < fffff800`008a4434 jb nt!__C_specific_handler+0x111 (fffff800`008a43e1)
:::: : :
:::: : : nt!__C_specific_handler+0x166:
>>:> > > fffff800`008a4436 mov eax,1 ; eax = ExceptionContinueSearch (0n1)
:
: nt!__C_specific_handler+0x16b:
> fffff800`008a443b mov r15,qword ptr [rsp+48h]
fffff800`008a4440 mov r14,qword ptr [rsp+50h]
fffff800`008a4445 mov r13,qword ptr [rsp+58h]
fffff800`008a444a mov r12,qword ptr [rsp+60h]
fffff800`008a444f mov rdi,qword ptr [rsp+68h]
fffff800`008a4454 mov rsi,qword ptr [rsp+70h]
fffff800`008a4459 mov rbp,qword ptr [rsp+78h]
fffff800`008a445e mov rbx,qword ptr [rsp+80h]
fffff800`008a4466 add rsp,88h
fffff800`008a446d ret
nt!__C_specific_handler 相當於 x86 中的 nt!_except_handler3。從上面的反彙編程式碼也可以看出它的邏輯跟 nt!_except_handler3 基本上一致。函式程式碼不長。主要分為兩個大分支,一個分支處理異常,一個分支處理展開(我用橫線分隔開了)。
異常解決的程式碼負責遍歷 SCOPE_TABLE,依次呼叫 SCOPE_TABLE::ScopeRecord.HandlerAddress 代表的 EXCEPT_FILTER,並針對返回值做出相應的處理:
1. 返回 EXCEPTION_CONTINUE_EXECUTION,說明異常已經被 EXCEPT_FILTER 修復。返回 ExceptionContinueExecution。
2. 返回 EXCEPTION_CONTINUE_SEARCH,繼續遍歷下一個 ScopeRecord。
3. 返回 EXCEPTION_EXECUTE_HANDLER,說明當前 ScopeRecord.JumpTarget 代表的 EXCEPT_HANDLER 可以處理該異常。那麼呼叫 RtlUnwindEx 進行展開。
熟悉 x86 的朋友可能會疑惑:在 x86 中 nt!_except_handler3 先進行全域性展開,然後對本函式自身進行不完全的區域性展開,最後執行 EXCEPT_HANDLER。而在 nt!__C_specific_handler 中卻找不到執行 EXCEPT_HANDLER 的指令,這是怎麼回事?
實際上,x64 對這個流程做了一些調整,EXCEPT_HANDLER 不是由 nt!__C_specific_handler 直接呼叫,而是作為引數傳給 RtlUnwindEx,RtlUnwindEx 處理完展開之後才執行 EXCEPT_HANDLER。後續我們在講展開的時候會看到具體的方法。
__C_specific_handler 的展開分支,是對 SCOPE_TABLE 進行展開,邏輯很簡單,不多講了。
更詳細的資訊,請參考上面反彙編程式碼中我附的註釋。
另外還需要說一下 SCOPE_TABLE。
在 x86 中,遍歷 scopetable 時是通過執行時動態改變的 EXCEPTION_REGISTRATION::trylevel 來確定應該首先遍歷哪一個 scopetable_entry。而 x64 中沒有等同於 trylevel 的資料,有的朋友可能會說“SCOPE_TABLE 中不是有每個 __try 保護域的範圍 RVA 嗎?通過範圍不就可以確定在哪個 __try 中觸發了異常嗎?”。
我們可以先試試這種方法,以下面這段偽碼為例,
Code:
1 VOID SehTest()
2 {
3 __try // 1
4 {
5 }
6 __except()
7 {
8 }
9
10 __try // 2
11 {
12 __try // 3
13 {
14 ...
15 }
16 __except()
17 {
18 }
19 }
20 __except()
21 {
22 }
23
24 __try // 4
25 {
26 }
27 __finally()
28 {
29 }
30}
上述偽碼中總共有4個 __try,按照 x86 中的方法,SCOPE_TABLE 的內容應該是順序排列的,像這樣:SCOPE_TABLE::Count 等於4,