异常篇——|异常篇—— VEH 与 SEH

写在前面 ??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
??看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。
?
华丽的分割线
?
概述 ??当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接进行处理 ,而是修正3环EIP为KiUserExceptionDispatcher函数后就结束了。这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行,这个函数就是我们重点关注对象,我们先看一下它的流程:
  1. 调用RtlDispatchException,查找并执行异常处理函数。
  2. 如果RtlDispatchException返回真,调用ZwContinue再次进入0环,但线程再次返回3环时,会从修正后的位置开始执行。
  3. 如果RtlDispatchException返回假,调用ZwRaiseException进行第二轮异常分发。
??看完上面的流程之后,我们看看其反汇编:
; void __stdcall __noreturn KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextFrame) public _KiUserExceptionDispatcher@8 _KiUserExceptionDispatcher@8 proc near; DATA XREF: .text:off_7C923428↑ovar_C= dword ptr -0Ch var_8= dword ptr -8 var_4= dword ptr -4 ExceptionRecord = dword ptr4 ContextFrame= dword ptr8movecx, [esp+ExceptionRecord] movebx, [esp] pushecx; ContextRecord pushebx; ExceptionRecord call_RtlDispatchException@8 ; RtlDispatchException(x,x) oral, al jzshort loc_7C92E47A popebx popecx push0 pushecx call_ZwContinue@8; ZwContinue(x,x) jmpshort loc_7C92E485 ; ---------------------------------------------------------------------------loc_7C92E47A:; CODE XREF: KiUserExceptionDispatcher(x,x)+10↑j popebx popecx push0; FirstChance pushecx; ContextRecord pushebx; ExceptionRecord call_ZwRaiseException@12 ; ZwRaiseException(x,x,x)loc_7C92E485:; CODE XREF: KiUserExceptionDispatcher(x,x)+1C↑j addesp, -14h mov[esp+EXCEPTION_RECORD.ExceptionCode], eax mov[esp+EXCEPTION_RECORD.ExceptionFlags], 1 mov[esp+EXCEPTION_RECORD.ExceptionRecord], ebx mov[esp+EXCEPTION_RECORD.NumberParameters], 0 pushesp; ExceptionRecord call_RtlRaiseException@4 ; RtlRaiseException(x) _KiUserExceptionDispatcher@8 endp ; sp-analysis failed

??可以看出该函数会调用RtlDispatchException,为了节省篇幅用伪代码如下:
BOOLEAN __stdcall RtlDispatchException(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]result = 0; if ( RtlCallVectoredExceptionHandlers(ExceptionRecord, ContextRecord) ) return 1; RtlpGetStackLimits(&LowLimit, &HighLimit); ExceptionRecorda = 0; exRecord = RtlpGetRegistrationHead(); // ExceptionList if ( exRecord != -1 ) { while ( 1 ) { if ( exRecord < LowLimit || &exRecord[1] > HighLimit || (exRecord & 3) != 0 || (handler = exRecord->Handler, handler >= LowLimit) && handler < HighLimit || !RtlIsValidHandler(exRecord->Handler) ) { ExceptionRecord->ExceptionFlags |= EXCEPTION_STACK_INVALID; return result; } if ( byte_7C99B3FA < 0 ) v11 = RtlpLogExceptionHandler(ExceptionRecord, ContextRecord, 0, exRecord, 0x10u); RtlpExecuteHandlerForException(ExceptionRecord, exRecord, ContextRecord, &a4, exRecord->Handler); v6 = v5; if ( byte_7C99B3FA < 0 ) RtlpLogLastExceptionDisposition(v11, v5); if ( ExceptionRecorda == exRecord ) { ExceptionRecord->ExceptionFlags &= 0xFFFFFFEF; ExceptionRecorda = 0; } if ( !v6 ) break; if ( v6 == 1 ) { if ( (ExceptionRecord->ExceptionFlags & 8) != 0 ) return result; } else { if ( v6 != 2 ) { e.ExceptionCode = EXCEPTION_INVALID_DISPOSITION; e.ExceptionFlags = 1; e.ExceptionRecord = ExceptionRecord; e.NumberParameters = 0; RtlRaiseException(&e); } v8 = a4; ExceptionRecord->ExceptionFlags |= EXCEPTION_NESTED_CALL; if ( v8 > ExceptionRecorda ) ExceptionRecorda = v8; } exRecord = exRecord->Next; if ( exRecord == -1 ) return result; } if ( (ExceptionRecord->ExceptionFlags & 1) != 0 ) { e.ExceptionCode = EXCEPTION_NONCONTINUABLE_EXCEPTION; e.ExceptionFlags = EXCEPTION_NONCONTINUABLE; e.ExceptionRecord = ExceptionRecord; e.NumberParameters = 0; RtlRaiseException(&e); } result = 1; } return result; }

??RtlCallVectoredExceptionHandlers这个函数就是用来执行VEH的。如果返回假,则说明没有,后面的RtlpGetRegistrationHead就会获取SEH,如果有就执行,它是在堆栈中的。
??有了这些铺垫后,我们来介绍VEHSEH
VEH ??对于VEH,这个是XP及其之后才有的,中文为向量化异常结构处理。我们先看看它的处理流程:
  1. CPU捕获异常信息;
  2. 通过KiDispatchException进行分发;
  3. KiUserExceptionDispatcher调用RtlDispatchException
  4. RtlDispatchException查找VEH处理函数链表 并调用相关处理函数;
  5. 代码返回到KiUserExceptionDispatcher
  6. 调用ZwContinue再次进入0环(ZwContinue调用NtContinue,主要作用就是恢复_TRAP_FRAME然后通过KiServiceExit返回到3环);
  7. 线程再次返回3环后,从修正后的位置开始执行;
??如下是执行VEH的伪代码:
BOOLEAN __stdcall RtlCallVectoredExceptionHandlers(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord) { PRTL_VECTORED_HANDLER_ENTRY p; // esi int (__stdcall *VectoredHandler)(EXCEPTION_POINTERS *); // eax EXCEPTION_POINTERS ExceptionInfo; // [esp+4h] [ebp-8h] BYREF BOOLEAN v6; // [esp+17h] [ebp+Bh]if ( IsListEmpty(&RtlpCalloutEntryList) ) return 0; ExceptionInfo.ExceptionRecord = ExceptionRecord; ExceptionInfo.ContextRecord = ContextRecord; RtlEnterCriticalSection(&RtlpCalloutEntryLock); for ( p = RtlpCalloutEntryList.Flink; ; p = p->ListEntry.Flink ) { if ( p == &RtlpCalloutEntryList ) { v6 = 0; goto EndProc; } VectoredHandler = RtlDecodePointer(p->VectoredHandler); if ( VectoredHandler(&ExceptionInfo) == -1 ) break; } v6 = 1; EndProc: RtlLeaveCriticalSection(&RtlpCalloutEntryLock); return v6; }

??剩余的细节将会在总结与提升进行讲解,下面我们来看看如何使用VEH,如下是实验代码:
#include "stdafx.h" #include #include typedef PVOID (NTAPI *VectoredExceptionHandler)(ULONG,_EXCEPTION_POINTERS*); LONG NTAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { puts("进入异常处理函数……"); if (pExceptionInfo->ExceptionRecord->ExceptionCode==0xC0000094) { puts("异常函数处理了……"); pExceptionInfo->ContextRecord->Ecx = 1; return EXCEPTION_CONTINUE_EXECUTION; }return EXCEPTION_CONTINUE_SEARCH; }int main(int argc, char* argv[]) { HMODULE lib = LoadLibrary("kernel32.dll"); VectoredExceptionHandler AddVectoredExceptionHandler = (VectoredExceptionHandler)GetProcAddress(lib,"AddVectoredExceptionHandler"); AddVectoredExceptionHandler(1,(_EXCEPTION_POINTERS*)&MyVectoredExceptionHandler); _asm { xor edx,edx; xor ecx,ecx; mov eax,0x10; idiv ecx; } puts("继续执行……"); system("pause"); return 0; }

??执行后会正常执行,并显示异常处理信息。
SEH ??SEH意为结构化异常处理,它的结构如下图所示:
异常篇——|异常篇—— VEH 与 SEH
文章图片

??也就是说包装的异常处理项目是以单向链表的形式管理的。必须具有两个如上图所示的成员,也就是说,这个结构是可以扩展的,有关扩展的将会在后续介绍,下面我们来看实验代码:
#include "stdafx.h" #include #include struct MyException { MyException* prev; DWORD handle; }; EXCEPTION_DISPOSITION MyExceptionHandler(_EXCEPTION_RECORD* ExceptionRecord,void* Establisherframe,CONTEXT* context,void* DispatcherContext) { puts("进入异常处理……"); if (ExceptionRecord->ExceptionCode==0xC0000094) { puts("开始处理异常……"); context->Eip+=2; return ExceptionContinueExecution; } return ExceptionContinueSearch; }int main(int argc, char* argv[]) {DWORD tmp; //初始化异常结构 MyException ex={(MyException*)tmp,(DWORD)MyExceptionHandler}; //加入 SEH _asm { mov eax,fs:[0]; mov tmp,eax; lea ecx,ex; mov fs:[0],ecx; }//制造异常 _asm { xor edx,edx; xor ecx,ecx; mov eax,0x10; idiv ecx; }//撤掉 SEH _asm { mov eax,tmp; mov fs:[0],eax; }puts("正常运行……"); system("pause"); return 0; }

??该程序正常执行,并打印异常处理结果。
编译器扩展 SEH 初识
??前面我们用自己的方式实现了SEH的使用。异常处理很重要,但是,这个对于开发者很不友好。每次都要构造SEH,退出函数要撤掉。编译器提供了关键字,并对SEH进行了扩充,使用如下图所示:
_try// 挂入 SEH 链表 {} _except(/*过滤表达式*/) //异常过滤 { //异常处理程序 }

??对于过滤表达式的结果值,只能是-101,它们表示的含义如下:
  1. EXCEPTION_EXECUTE_HANDLER (1) 执行except里面的代码
  2. EXCEPTION_CONTINUE_SEARCH (0) 寻找下一个异常处理函数
  3. EXCEPTION_CONTINUE_EXECUTION (-1) 返回出错位置重新执行
??我说只能是这三值,并没有说只能写这三个数字,你可以写入表达式或者函数,使其得到的结果或者返回值是这仨值其中之一就可以,如下是我们的实验程序:
#include "stdafx.h" #include int main(int argc, char* argv[]) { _try { _asm { xor edx,edx; xor ecx,ecx; mov eax,0x10; idiv ecx; } puts("继续跑……"); }_except(1) { puts("异常处理……"); } system("pause"); return 0; }

??运行该程序,只打印了except里面的,得到正确结果。
初步深入
??我们接下来在汇编层面查看它是如何实现的,首先我们查看一下编译器为我们扩展的结构,否则看代码是看不懂的。
struct _EXCEPTION_REGISTRATION { struct _EXCEPTION_REGISTRATION *prev; void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD); struct scopetable_entry *scopetable; int trylevel; int _ebp; };

??然后我们所谓的结构就成立这样子:
异常篇——|异常篇—— VEH 与 SEH
文章图片

??图中的_except_handler3是啥我们看它的反汇编是什么就知道了:
#include "stdafx.h" #include int main(int argc, char* argv[]) { 00401010pushebp 00401011movebp,esp 00401013push0FFh 00401015pushoffset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed"+0Ch (00424030) 0040101Apushoffset __except_handler3 (00401400) 0040101Fmoveax,fs:[00000000] 00401025pusheax 00401026movdword ptr fs:[0],esp 0040102Daddesp,0B8h 00401030pushebx 00401031pushesi 00401032pushedi 00401033movdword ptr [ebp-18h],esp 00401036leaedi,[ebp-58h] 00401039movecx,10h 0040103Emoveax,0CCCCCCCCh 00401043rep stosdword ptr [edi] _try 00401045movdword ptr [ebp-4],0 { _asm { xor edx,edx; 0040104Cxoredx,edx xor ecx,ecx; 0040104Exorecx,ecx mov eax,0x10; 00401050moveax,10h idiv ecx; 00401055idiveax,ecx } puts("继续跑……"); 00401057pushoffset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed" (00424024) 0040105Ccallputs (004011e0) 00401061addesp,4 }_except(1) 00401064movdword ptr [ebp-4],0FFFFFFFFh 0040106Bjmp$L865+17h (0040108a) $L864: 0040106Dmoveax,1 $L866: 00401072ret $L865: 00401073movesp,dword ptr [ebp-18h] { puts("异常处理……"); 00401076pushoffset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00425140) 0040107Bcallputs (004011e0) 00401080addesp,4 } 00401083movdword ptr [ebp-4],0FFFFFFFFh system("pause"); 0040108Apushoffset string "pause" (0042401c) 0040108Fcallsystem (004010d0) 00401094addesp,4 return 0; 00401097xoreax,eax } 00401099movecx,dword ptr [ebp-10h] 0040109Cmovdword ptr fs:[0],ecx 004010A3popedi 004010A4popesi 004010A5popebx 004010A6addesp,58h 004010A9cmpebp,esp 004010ABcall__chkesp (004012d0) 004010B0movesp,ebp 004010B2popebp 004010B3ret

??看不懂吗?我们来画个堆栈图,如下所示:
异常篇——|异常篇—— VEH 与 SEH
文章图片

??标注*的表示原来的值,是不是和结构体的成员对应起来了?注意不要以为只有黄色的区域,由于通常的函数采用ebp寻址,所以我没有把ebp*打上黄色底色。
??下面我们来看看scopetable成员,它的结构如下:
struct scopetable_entry { DWORD previousTryLevel; //上一个try{}结构编号 PDWRD lpfnFilter; //过滤函数的起始地址 PDWRD lpfnHandler; //异常处理程序的地址 }

??我们来看看这个结构的内容是啥,最终它的成员如下:
scopetable.previousTryLevel = -1; scopetable.lpfnFilter = 0x40106D; scopetable.lpfnHandler = 0x401073;

??正好把代码指令和地址逐个对应起来了。
继续深入
??如果异常处理有嵌套调用的情况会是怎么样呢?如下是测试代码:
#include "stdafx.h" #include int main(int argc, char* argv[]) { _try { _try { _asm { xor edx,edx; xor ecx,ecx; mov eax,0x10; idiv ecx; } }_except(1) { puts("测试"); } puts("继续跑……"); }_except(1) { puts("异常处理……"); } system("pause"); return 0; }

??然后查看反汇编结果:
#include "stdafx.h" #include int main(int argc, char* argv[]) { 00401010pushebp 00401011movebp,esp 00401013push0FFh 00401015pushoffset string "\xb2\xe2\xca\xd4"+0Ch (00424050) 0040101Apushoffset __except_handler3 (00401450) 0040101Fmoveax,fs:[00000000] 00401025pusheax 00401026movdword ptr fs:[0],esp 0040102Daddesp,0B8h 00401030pushebx 00401031pushesi 00401032pushedi 00401033movdword ptr [ebp-18h],esp 00401036leaedi,[ebp-58h] 00401039movecx,10h 0040103Emoveax,0CCCCCCCCh 00401043rep stosdword ptr [edi] _try 00401045movdword ptr [ebp-4],0 { _try 0040104Cmovdword ptr [ebp-4],1 { _asm { xor edx,edx; 00401053xoredx,edx xor ecx,ecx; 00401055xorecx,ecx mov eax,0x10; 00401057moveax,10h idiv ecx; 0040105Cidiveax,ecx } }_except(1) 0040105Emovdword ptr [ebp-4],0 00401065jmp$L872+17h (0040f5d4) $L871: 00401067moveax,1 $L873: 0040106Cret $L872: 0040106Dmovesp,dword ptr [ebp-18h] { puts("测试"); 00401070pushoffset string "\xb2\xe2\xca\xd4" (00424044) 00401075callputs (00401230) 0040107Aaddesp,4 } 0040107Dmovdword ptr [ebp-4],0 puts("继续跑……"); 00401084pushoffset string "\xbc\xcc\xd0\xf8\xc5\xdc\xa1\xad\xa1\xad" (00424034) 00401089callputs (00401230) 0040108Eaddesp,4 }_except(1) 00401091movdword ptr [ebp-4],0FFFFFFFFh 00401098jmp$L868+17h (004010b7) $L867: 0040109Amoveax,1 $L869: 0040109Fret $L868: 004010A0movesp,dword ptr [ebp-18h] { puts("异常处理……"); 004010A3pushoffset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00424024) 004010A8callputs (00401230) 004010ADaddesp,4 } 004010B0movdword ptr [ebp-4],0FFFFFFFFh system("pause"); 004010B7pushoffset string "pause" (0042401c) 004010BCcallsystem (00401120) 004010C1addesp,4 return 0; 004010C4xoreax,eax } 004010C6movecx,dword ptr [ebp-10h] 004010C9movdword ptr fs:[0],ecx 004010D0popedi 004010D1popesi 004010D2popebx 004010D3addesp,58h 004010D6cmpebp,esp 004010D8call__chkesp (00401320) 004010DDmovesp,ebp 004010DFpopebp 004010E0ret

??看代码发现还是只是挂了一次,我们得看看scopetable的内容是啥了:
00425168FFFFFFFF0040109A004010A0 0042517400000000004010670040106D 00425180000000000000000000000000 0042518C000000000000000000000000

??可以看到,这里有两个成员了。
finally 关键字
??当然不仅仅有try_except,还可以使用finally,该关键字的作用就是只要退出try就执行里面的函数,无论通过那种方式,如下是我们的实验代码:
#include "stdafx.h" #include int main(int argc, char* argv[]) { _try { return 0; }__finally { puts("异常处理……"); system("pause"); } return 0; }

??执行结果如下:
异常处理…… 请按任意键继续. . .

??然后我们看看它在汇编层面是如何实现的,其反汇编如下:
#include "stdafx.h" #include int main(int argc, char* argv[]) { 00401010pushebp 00401011movebp,esp 00401013push0FFh 00401015pushoffset string "stream != NULL"+10h (00425168) 0040101Apushoffset __except_handler3 (00401450) 0040101Fmoveax,fs:[00000000] 00401025pusheax 00401026movdword ptr fs:[0],esp 0040102Daddesp,0B4h 00401030pushebx 00401031pushesi 00401032pushedi 00401033leaedi,[ebp-5Ch] 00401036movecx,11h 0040103Bmoveax,0CCCCCCCCh 00401040rep stosdword ptr [edi] _try 00401042movdword ptr [ebp-4],0 00401049push0FFh 0040104Bmovdword ptr [ebp-1Ch],0 { 00401052leaeax,[ebp-10h] 00401055pusheax 00401056call__local_unwind2 (0040139a) 0040105Baddesp,8 return 0; 0040105Emoveax,dword ptr [ebp-1Ch] 00401061jmp$L865+2 (00401080) }__finally { puts("异常处理……"); 00401063pushoffset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xa1\xad\xa1\xad" (00424024) 00401068callputs (00401230) 0040106Daddesp,4 system("pause"); 00401070pushoffset string "pause" (0042401c) 00401075callsystem (00401120) 0040107Aaddesp,4 $L863: 0040107Dret } 17:return 0; 0040107Exoreax,eax } 00401080movecx,dword ptr [ebp-10h] 00401083movdword ptr fs:[0],ecx 0040108Apopedi 0040108Bpopesi 0040108Cpopebx 0040108Daddesp,5Ch 00401090cmpebp,esp 00401092call__chkesp (00401320) 00401097movesp,ebp 00401099popebp 0040109Aret

??可以看到在调用return 0; 之前,被插入了调用__local_unwind2函数,正是这个函数能够调用finally里面的代码的:
__local_unwind2: 0040139Apushebx 0040139Bpushesi 0040139Cpushedi 0040139Dmoveax,dword ptr [esp+10h] 004013A1pusheax 004013A2push0FEh 004013A4pushoffset __global_unwind2+20h (00401378) 004013A9pushdword ptr fs:[0] 004013B0movdword ptr fs:[0],esp 004013B7moveax,dword ptr [esp+20h] 004013BBmovebx,dword ptr [eax+8] 004013BEmovesi,dword ptr [eax+0Ch] 004013C1cmpesi,0FFh 004013C4je__NLG_Return2+2 (004013f4) 004013C6cmpesi,dword ptr [esp+24h] 004013CAje__NLG_Return2+2 (004013f4) 004013CCleaesi,[esi+esi*2] 004013CFmovecx,dword ptr [ebx+esi*4] 004013D2movdword ptr [esp+8],ecx 004013D6movdword ptr [eax+0Ch],ecx 004013D9cmpdword ptr [ebx+esi*4+4],0 004013DEjne__NLG_Return2 (004013f2) 004013E0push101h 004013E5moveax,dword ptr [ebx+esi*4+8] 004013E9call__NLG_Notify (0040142e) 004013EEcalldword ptr [ebx+esi*4+8] __NLG_Return2: 004013F2jmp__local_unwind2+1Dh (004013b7) 004013F4popdword ptr fs:[0] 004013FBaddesp,0Ch 004013FEpopedi 004013FFpopesi 00401400popebx 00401401ret

??关键调用在call dword ptr [ebx+esi*4+8],执行这个就会调用finally里的代码。具体详细的其他细节将会在总结与提升进行介绍。
下一篇 【异常篇——|异常篇—— VEH 与 SEH】??异常篇——总结与提升

    推荐阅读