异常篇——|异常篇—— VEH 与 SEH
写在前面
??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。??看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。
?
华丽的分割线
?
概述 ??当用户异常产生后,内核函数
KiDispatchException
并不是像处理内核异常那样在0环直接进行处理 ,而是修正3环EIP为KiUserExceptionDispatcher
函数后就结束了。这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher
函数开始执行,这个函数就是我们重点关注对象,我们先看一下它的流程:- 调用
RtlDispatchException
,查找并执行异常处理函数。 - 如果
RtlDispatchException
返回真,调用ZwContinue
再次进入0环,但线程再次返回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
,如果有就执行,它是在堆栈中的。??有了这些铺垫后,我们来介绍
VEH
和SEH
。VEH ??对于
VEH
,这个是XP
及其之后才有的,中文为向量化异常结构处理。我们先看看它的处理流程:CPU
捕获异常信息;- 通过
KiDispatchException
进行分发; KiUserExceptionDispatcher
调用RtlDispatchException
;RtlDispatchException
查找VEH
处理函数链表 并调用相关处理函数;- 代码返回到
KiUserExceptionDispatcher
; - 调用
ZwContinue
再次进入0环(ZwContinue
调用NtContinue
,主要作用就是恢复_TRAP_FRAME
然后通过KiServiceExit
返回到3环); - 线程再次返回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
意为结构化异常处理,它的结构如下图所示:文章图片
??也就是说包装的异常处理项目是以单向链表的形式管理的。必须具有两个如上图所示的成员,也就是说,这个结构是可以扩展的,有关扩展的将会在后续介绍,下面我们来看实验代码:
#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(/*过滤表达式*/) //异常过滤
{
//异常处理程序
}
??对于过滤表达式的结果值,只能是
-1
、0
、1
,它们表示的含义如下:EXCEPTION_EXECUTE_HANDLER
(1) 执行except
里面的代码EXCEPTION_CONTINUE_SEARCH
(0) 寻找下一个异常处理函数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;
};
??然后我们所谓的结构就成立这样子:
文章图片
??图中的
_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
??看不懂吗?我们来画个堆栈图,如下所示:
文章图片
??标注
*
的表示原来的值,是不是和结构体的成员对应起来了?注意不要以为只有黄色的区域,由于通常的函数采用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】??异常篇——总结与提升
推荐阅读
- 『无为则无心』Python面向对象|『无为则无心』Python面向对象 — 58、类方法和静态方法
- hadoop|13、Hive数据仓库——结合shell脚本企业实战用法,定时调度
- hadoop|5、Hive数据仓库——Hive分区及动态分区
- 产品经理(「点这里,我要跳到任何我想跳的页面」—— 解耦提效神器「统跳路由」)
- C语言入门篇--定义宏#define的概述
- 一篇文章带你了解Maven的生命周期
- python|python api测试框架 github_GitHub - tilics/jiekou-python3: 接口自动化测试框架——python版,支持HTTP,dubbo协议接口...
- Java每日一题|【蓝桥Java每日一题】——14.球会落何处(有趣模拟题)
- 『无为则无心』Python面向对象|『无为则无心』Python面向对象 — 57、类属性和实例属性
- 算法|开学季——经典计算机教材带你起飞!