我知道作出这个决定后肯定要经历一个痛苦的过程,但我仍然义无反顾地开了 VC,然后一头扎进机器语言的茫茫大雾中……
想看到 _except_handler3,就要先抓住它;想抓住它,就要先引发一个异常。这个好办,几行程序就可以把它引出来:
1: __try { 2: int *p = 0; 3: *p = 0; 4: } __except(EXCEPTION_EXECUTE_HANDLER) { 5: }
在第 3 行设断点,然后切换到反汇编,就看到了这样的景象:
01: _try { 02: 00411A4B mov dword ptr [ebp-4],0 03: int *p = 0; 04: 00411A52 mov dword ptr [p],0 05: *p = 0; 06: 00411A59 mov eax,dword ptr [p] 07: 00411A5C mov dword ptr [eax],0 08: 00411A62 mov dword ptr [ebp-4],0FFFFFFFFh 09: 00411A69 jmp $L28580+0Ah (411A7Bh) 10: } _except(EXCEPTION_EXECUTE_HANDLER) { 11: 00411A6B mov eax,1 12: $L28581: 13: 00411A70 ret 14: $L28580: 15: 00411A71 mov esp,dword ptr [ebp-18h] 16: 00411A74 mov dword ptr [ebp-4],0FFFFFFFFh 17: }
啊,在明白了大部分事情之后,一切显得都是那么的自然:第 2 行的指令不就是在设置那个“传说中的”trylevel 么?呵呵,基址后的第一个
DWORD 就是,果然不错。AV 异常显然应该在第 7 行发生,step into 那一行,却发现:VC 在输出窗口中显示有异常发生,然后直接停在了 15 行,也就是 handler 代码开始的地方。这不是我想要的结果,因为据我所知,异常发生后,会产生一大堆系统调用,最后由 _except_handler3 把控制权交回我写的 handler。换句话说,当进入我的 handler 代码时,这一切都已经结束了…… 既然 VC 不愿意让我这么容易地看到 _except_handler3 的代码,那么我也就不得不耍点手段了,于是我盯上了
11、13 行的 filter 指令。是的,这应该就是 filter 的代码,如果有人 CALL 到 11 行,那么这行指令会将 eax 置为 1,然后在第 13 行返回,也就是返回 1,根据 EXCPT.H 中的宏定义,1 就是 EXCEPTION_EXECUTE_HANDLER 的值,所以这正是我的 filter-expression 的行为,这就是我的 filter 代码。那么,如果是 _except_handler3 调用了 filter,那么我在 filter 返回之前中断,是不是就能跟回到我梦寐以求的 _except_handler3 中去了呢?是的,当我在第 13 行设断点、step over 之后,VC 终于老老实实地把我带回了 _except_handler3 家。 好在 _except_handler3 的代码不多,更何况我之前已经看过了伪码,所以想弄懂这些指令在做什么并不是件很难的事。首先我意识到必须先弄到它的定义,否则看那一大堆相对于
ebp 寄存器的偏移肯定不是件多么舒服的事。好在 Matt Pietrek 已经在他的文章中提到了,EXCPT.H 中包含了这个函数定义: 1: EXCEPTION_DISPOSITION 2: __cdecl _except_handler( 3: struct _EXCEPTION_RECORD *ExceptionRecord, 4: void * EstablisherFrame, 5: struct _CONTEXT *ContextRecord, 6: void * DispatcherContext 7: );
虽然这个定义中的函数名是 _except_handler 而非 _except_handler3,但估计也就是一个 Place
Holder。因为我已经尝试过直接在代码中显示调用这个函数名了,但是 Link 不上,所以名字不一样也无所谓了。根据这个定义,可以得出结论:这是一个 __cdecl 调用约定的函数,4 个参数从右至左入栈,调用者负责清理堆栈。因此:指令中出现的 [ebp+8] 引用的是 ExceptionRecord、[ebp+0Ch] 引用的是 EstablisherFrame、[ebp+10h] 引用的是 ContextRecord、[ebp+14h] 引用的是 DispatcherContext,函数返回使用 ret 而非 __stdcall 的函数常用的“ret N”。好了,有了这些信息,分析起来就容易多了: 001: _except_handler3: 002: 004141A0 push ebp 003: 004141A1 mov ebp,esp 004: ; // EXCEPTION_POINTERS exceptPtrs; 005: 004141A3 sub esp,8 006: 004141A6 push ebx 007: 004141A7 push esi 008: 004141A8 push edi 009: 004141A9 push ebp 010: 004141AA cld 011: ; // EstablisherFrame => ebx 012: 004141AB mov ebx,dword ptr [ebp+0Ch] 013: ; // ExceptionRecord => eax 014: 004141AE mov eax,dword ptr [ebp+8] 015: ; // if (ExceptionRecord->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT) 016: ; // goto _lh_unwinding; 017: 004141B1 test dword ptr [eax+4],6 018: 004141B8 jne _lh_unwinding (414269h) 019: ; // exceptPtrs.ExceptionRecord = ExceptionRecord 020: 004141BE mov dword ptr [ebp-8],eax 021: ; // exceptPtrs.ContextRecord = ContextRecord; 022: 004141C1 mov eax,dword ptr [ebp+10h] 023: 004141C4 mov dword ptr [ebp-4],eax 024: ; // *(PDWORD)((PBYTE)EstablisherFrame - 4) = &exceptPtrs 025: 004141C7 lea eax,[ebp-8] 026: 004141CA mov dword ptr [ebx-4],eax 027: ; // EstablisherFrame->trylevel => esi 028: 004141CD mov esi,dword ptr [ebx+0Ch] 029: ; // EstablisherFrame->scopetable => edi 030: 004141D0 mov edi,dword ptr [ebx+8] 031: ; // if (_ValidateEH3RN(EstablisherFrame) == 0) 032: ; // goto _lh_abort; 033: 004141D3 push ebx 034: 004141D4 call @ILT+775(__ValidateEH3RN) (41130Ch) 035: 004141D9 add esp,4 036: 004141DC or eax,eax 037: 004141DE je _lh_abort (41425Bh) 038: _lh_top: 039: ; // if (trylevel == TRYLEVEL_NONE) 040: ; // goto _lh_bagit; 041: 004141E0 cmp esi,0FFFFFFFFh 042: 004141E3 je _lh_bagit (414262h) 043: ; // EstablisherFrame->scopetable[trylevel].lpfnFilter => eax 044: 004141E5 lea ecx,[esi+esi*2] 045: 004141E8 mov eax,dword ptr [edi+ecx*4+4] 046: ; // if (EstablisherFrame->scopetable[trylevel].lpfnFilter == NULL) 047: ; // goto _lh_continue; 048: 004141EC or eax,eax 049: 004141EE je _lh_continue (414249h) 050: ; // PUSH EBP 051: 004141F0 push esi 052: 004141F1 push ebp 053: ; // EBP = &EstablisherFrame->_ebp 054: 004141F2 lea ebp,[ebx+10h] 055: ; // ret = EstablisherFrame->scopetable[trylevel].lpfnFilter(); 056: 004141F5 xor ebx,ebx 057: 004141F7 xor ecx,ecx 058: 004141F9 xor edx,edx 059: 004141FB xor esi,esi 060: 004141FD xor edi,edi 061: 004141FF call eax 062: ; // POP EBP 063: 00414201 pop ebp 064: 00414202 pop esi 065: ; // EstablisherFrame => ebx 066: 00414203 mov ebx,dword ptr [ebp+0Ch] 067: ; // if (ret == EXCEPTION_CONTINUE_SEARCH) 068: ; // goto _lh_continue; 069: ; // else if (ret < 0) 070: ; // goto _lh_dismiss; 071: 00414206 or eax,eax 072: 00414208 je _lh_continue (414249h) 073: 0041420A js _lh_dismiss (414254h) 074: ; // __global_unwind2(EstablisherFrame); 075: 0041420C mov edi,dword ptr [ebx+8] 076: 0041420F push ebx 077: 00414210 call @ILT+700(__global_unwind2) (4112C1h) 078: 00414215 add esp,4 079: ; // EBP = &EstablisherFrame->_ebp 080: 00414218 lea ebp,[ebx+10h] 081: ; // __local_unwind2(EstablisherFrame, trylevel); 082: 0041421B push esi 083: 0041421C push ebx 084: 0041421D call @ILT+385(__local_unwind2) (411186h) 085: 00414222 add esp,8 086: ; // __NLG_Notify(1); 087: 00414225 lea ecx,[esi+esi*2] 088: 00414228 push 1 089: 0041422A mov eax,dword ptr [edi+ecx*4+8] 090: 0041422E call @ILT+1045(__NLG_Notify) (41141Ah) 091: ; // EstablisherFrame->trylevel = 092: ; // EstablisherFrame->scopetable[trylevel].previousTryLevel 093: 00414233 mov eax,dword ptr [edi+ecx*4] 094: 00414236 mov dword ptr [ebx+0Ch],eax 095: ; // EstablisherFrame->scopetable[trylevel].lpfnHandler(); 096: 00414239 mov eax,dword ptr [edi+ecx*4+8] 097: 0041423D xor ebx,ebx 098: 0041423F xor ecx,ecx 099: 00414241 xor edx,edx 100: 00414243 xor esi,esi 101: 00414245 xor edi,edi 102: 00414247 call eax 103: _lh_continue: 104: ; // EstablisherFrame->scopetable[trylevel].previousTryLevel => esi 105: 00414249 mov edi,dword ptr [ebx+8] 106: 0041424C lea ecx,[esi+esi*2] 107: 0041424F mov esi,dword ptr [edi+ecx*4] 108: 00414252 jmp _lh_top (4141E0h) 109: _lh_dismiss: 110: ; // return ExceptionContinueExecution; 111: 00414254 mov eax,0 112: 00414259 jmp _lh_return (41427Eh) 113: _lh_abort: 114: ; // ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID; 115: 0041425B mov eax,dword ptr [ebp+8] 116: 0041425E or dword ptr [eax+4],8 117: _lh_bagit: 118: ; // return ExceptionContinueSearch; 119: 00414262 mov eax,1 120: 00414267 jmp _lh_return (41427Eh) 121: _lh_unwinding: 122: ; // PUSH EBP 123: 00414269 push ebp 124: ; // EBP = &EstablisherFrame->_ebp 125: 0041426A lea ebp,[ebx+10h] 126: ; // __local_unwind2(EstablisherFrame, TRYLEVEL_NONE); 127: 0041426D push 0FFFFFFFFh 128: 0041426F push ebx 129: 00414270 call @ILT+385(__local_unwind2) (411186h) 130: 00414275 add esp,8 131: ; // POP EBP 132: 00414278 pop ebp 133: ; // return ExceptionContinueSearch; 134: 00414279 mov eax,1 135: _lh_return: 136: 0041427E pop ebp 137: 0041427F pop edi 138: 00414280 pop esi 139: 00414281 pop ebx 140: 00414282 mov esp,ebp 141: 00414284 pop ebp 142: 00414285 ret
好了,我已经在指令前插入了 C 语句,现在 _except_handler3 对于我来说已经没有任何神秘之处了。说点题外话:我发现如果把这些语句提取出来、组成伪码的话,与
Matt Pietrek 的伪码将会非常的像,如果说代码结构方面有相似性也就罢了——毕竟牛人写出来的东西一般都很靠谱的,但是像变量的赋值顺序、指令流的走向、甚至 CLD 指令这样的小地方都一样。不知道他是不是也是用跟踪反汇编的方法写出的那些伪代码?真想问问他本人…… 不难发现,Matt Pietrek 没有在他的文章中提到第 31、32 行的代码(也就是反汇编第 33 至 37 行间的指令),这段代码调用了另一个函数并检查返回值,如果返回
0,handler 的指令流就会跳转到 _lh_abort 处:给异常打上一个“EXCEPTION_STACK_INVALID”的标志位(or 上了一个 8,也就是 EXSUP.INC 中定义的 EXCEPTION_STACK_INVALID 的值)然后立即返回。根据这个函数符号名中“Validate”的含义、以及 _except_handler3 发现其返回 0 后神经质般的举动可以判断——这个函数执行的是对栈帧指针的合法性检查。这种检查可以说在整个异常处理过程中并不鲜见,Rtl 函数里经常进行这样的检查,什么是否上下越界、是否 DWORD 对齐什么的……在这里出现也并不稀奇。我也没有对这个函数做深入研究,只是跟进去看了一眼,但是却有了意外的发现。