Windows 10 x64 异常分发
系统版本:
这个版本吾爱破解论坛有人分享了,也可以下载我提供的ntdll.dll 和 ntoskrnl.exe (包括pdb符号文件)。配合IDA使用。
异常 —> KiDispatchException
当出现异常时,会触发中断,CPU会通过对应的中断在IDT(中断描述符表)中寻找对应的处理函数。
在内核调试下输入 !idt,可查看各个中断的对应处理函数,例(节选):
1: kd> !idt
Dumping IDT: ffff8700676f7000
00: fffff8042b7bdb00 nt!KiDivideErrorFault
01: fffff8042b7bde00 nt!KiDebugTrapOrFault Stack = 0xFFFF8700676F4180
02: fffff8042b7be2c0 nt!KiNmiInterrupt Stack = 0xFFFF8700676F6180
03: fffff8042b7be780 nt!KiBreakpointTrap
04: fffff8042b7bea80 nt!KiOverflowTrap
05: fffff8042b7bed80 nt!KiBoundFault
06: fffff8042b7bf280 nt!KiInvalidOpcodeFault
07: fffff8042b7bf740 nt!KiNpxNotAvailableFault
08: fffff8042b7bfa00 nt!KiDoubleFaultAbort Stack = 0xFFFF8700676F0180
09: fffff8042b7bfcc0 nt!KiNpxSegmentOverrunAbort
0a: fffff8042b7bff80 nt!KiInvalidTssFault
0b: fffff8042b7c0240 nt!KiSegmentNotPresentFault
0c: fffff8042b7c05c0 nt!KiStackFault
0d: fffff8042b7c0900 nt!KiGeneralProtectionFault
0e: fffff8042b7c0c40 nt!KiPageFault
像我们常使用的 int 3断点 ,由上可知,就是调用了3号处理函数 nt!KiBreakpointTrap
,然后再进行下一步处理(见下文)。当某个中断函数被调用时,此函数做了一些处理后(例如将异常信息打包),最终发往 KiDispatchException
进行异常分发。例:
我们在KiDispatchException
用户态异常处理部分处下断点后,然后运行一个 触发 int 3 断点的程序。那么内核调试器就会断下来。查看调用栈:
KiDispatchException
简介
此函数是Windows内核中分发异常的枢纽,不论是内核态的异常,用户态的异常,都需要此函数进行分发。
参数(选读)
VOID KiDispatchException(
ExceptionRecord, //异常信息
ExceptionFrame,//异常帧
TrapFrame,//陷阱帧
PreviousMode,//内核态异常 OR 用户态异常
FirstChance);//是否为第一轮分发
EXCEPTION_RECORD:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;//发生异常的原因。(异常状态码)
DWORD ExceptionFlags;//该成员包含零个或多个异常标志。
struct _EXCEPTION_RECORD *ExceptionRecord;//指向关联的 EXCEPTION_RECORD结构的指针。
PVOID ExceptionAddress;//异常发生的地址
DWORD NumberParameters;//与异常关联的参数数量。
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];//与异常关联的参数
} EXCEPTION_RECORD;
ExceptionFrame:(指向异常帧)
kd> dt _KEXCEPTION_FRAME
nt!_KEXCEPTION_FRAME
+0x000 P1Home : Uint8B
+0x008 P2Home : Uint8B
+0x010 P3Home : Uint8B
+0x018 P4Home : Uint8B
+0x020 P5 : Uint8B
+0x028 Spare1 : Uint8B
+0x030 Xmm6 : _M128A
+0x040 Xmm7 : _M128A
+0x050 Xmm8 : _M128A
+0x060 Xmm9 : _M128A
+0x070 Xmm10 : _M128A
+0x080 Xmm11 : _M128A
+0x090 Xmm12 : _M128A
+0x0a0 Xmm13 : _M128A
+0x0b0 Xmm14 : _M128A
+0x0c0 Xmm15 : _M128A
+0x0d0 TrapFrame : Uint8B
+0x0d8 OutputBuffer : Uint8B
+0x0e0 OutputLength : Uint8B
+0x0e8 Spare2 : Uint8B
+0x0f0 MxCsr : Uint8B
+0x0f8 Rbp : Uint8B
+0x100 Rbx : Uint8B
+0x108 Rdi : Uint8B
+0x110 Rsi : Uint8B
+0x118 R12 : Uint8B
+0x120 R13 : Uint8B
+0x128 R14 : Uint8B
+0x130 R15 : Uint8B
+0x138 Return : Uint8B
TrapFrame:(指向陷阱帧)
kd> dt _KTRAP_FRAME
nt!_KTRAP_FRAME
+0x000 P1Home : Uint8B
+0x008 P2Home : Uint8B
+0x010 P3Home : Uint8B
+0x018 P4Home : Uint8B
+0x020 P5 : Uint8B
+0x028 PreviousMode : Char
+0x028 InterruptRetpolineState : UChar
+0x029 PreviousIrql : UChar
+0x02a FaultIndicator : UChar
+0x02a NmiMsrIbrs : UChar
+0x02b ExceptionActive : UChar
+0x02c MxCsr : Uint4B
+0x030 Rax : Uint8B
+0x038 Rcx : Uint8B
+0x040 Rdx : Uint8B
+0x048 R8 : Uint8B
+0x050 R9 : Uint8B
+0x058 R10 : Uint8B
+0x060 R11 : Uint8B
+0x068 GsBase : Uint8B
+0x068 GsSwap : Uint8B
+0x070 Xmm0 : _M128A
+0x080 Xmm1 : _M128A
+0x090 Xmm2 : _M128A
+0x0a0 Xmm3 : _M128A
+0x0b0 Xmm4 : _M128A
+0x0c0 Xmm5 : _M128A
+0x0d0 FaultAddress : Uint8B
+0x0d0 ContextRecord : Uint8B
+0x0d8 Dr0 : Uint8B
+0x0e0 Dr1 : Uint8B
+0x0e8 Dr2 : Uint8B
+0x0f0 Dr3 : Uint8B
+0x0f8 Dr6 : Uint8B
+0x100 Dr7 : Uint8B
+0x108 DebugControl : Uint8B
+0x110 LastBranchToRip : Uint8B
+0x118 LastBranchFromRip : Uint8B
+0x120 LastExceptionToRip : Uint8B
+0x128 LastExceptionFromRip : Uint8B
+0x130 SegDs : Uint2B
+0x132 SegEs : Uint2B
+0x134 SegFs : Uint2B
+0x136 SegGs : Uint2B
+0x138 TrapFrame : Uint8B
+0x140 Rbx : Uint8B
+0x148 Rdi : Uint8B
+0x150 Rsi : Uint8B
+0x158 Rbp : Uint8B
+0x160 ErrorCode : Uint8B
+0x160 ExceptionFrame : Uint8B
+0x168 Rip : Uint8B
+0x170 SegCs : Uint2B
+0x172 Fill0 : UChar
+0x173 Logging : UChar
+0x174 Fill1 : [2] Uint2B
+0x178 EFlags : Uint4B
+0x17c Fill2 : Uint4B
+0x180 Rsp : Uint8B
+0x188 SegSs : Uint2B
+0x18a Fill3 : Uint2B
+0x18c Fill4 : Uint4B
该函数先会对异常信息进行处理,然后通过 PreviousMode 区分该异常是内核态下触发的还是用户态下触发的。然后做出不同的处理。
初始工作这里就只讲一个比较重要的 : KeContextFromKframes。
一些初始工作
这里只介绍KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame);
通过 TrapFrame 和 ExceptionFrame 构建contextFrame,供调试器和异常处理函数报告异常时使用。
当内核态异常处理和用户态异常经内核调试器处理完毕以后需要调用KeContextToKframes
将context结构转回 TrapFrame 和 ExceptionFrame。
Context结构:(构建这个结构时就是从 TrapFrame 和 ExceptionFrame 中提取值)
typedef struct DECLSPEC_ALIGN(16) DECLSPEC_NOINITALL _CONTEXT {
//
// Register parameter home addresses.
//
// N.B. These fields are for convience - they could be used to extend the
// context record in the future.
//
DWORD64 P1Home;
DWORD64 P2Home;
DWORD64 P3Home;
DWORD64 P4Home;
DWORD64 P5Home;
DWORD64 P6Home;
//
// Control flags.
//
DWORD ContextFlags;
DWORD MxCsr;
//
// Segment Registers and processor flags.
//
WORD SegCs;
WORD SegDs;
WORD SegEs;
WORD SegFs;
WORD SegGs;
WORD SegSs;
DWORD EFlags;
//
// Debug registers
//
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7;
//
// Integer registers.
//
DWORD64 Rax;
DWORD64 Rcx;
DWORD64 Rdx;
DWORD64 Rbx;
DWORD64 Rsp;
DWORD64 Rbp;
DWORD64 Rsi;
DWORD64 Rdi;
DWORD64 R8;
DWORD64 R9;
DWORD64 R10;
DWORD64 R11;
DWORD64 R12;
DWORD64 R13;
DWORD64 R14;
DWORD64 R15;
//
// Program counter.
//
DWORD64 Rip;
//
// Floating point state.
//
union {
XMM_SAVE_AREA32 FltSave;
struct {
M128A Header[2];
M128A Legacy[8];
M128A Xmm0;
M128A Xmm1;
M128A Xmm2;
M128A Xmm3;
M128A Xmm4;
M128A Xmm5;
M128A Xmm6;
M128A Xmm7;
M128A Xmm8;
M128A Xmm9;
M128A Xmm10;
M128A Xmm11;
M128A Xmm12;
M128A Xmm13;
M128A Xmm14;
M128A Xmm15;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
//
// Vector registers.
//
M128A VectorRegister[26];
DWORD64 VectorControl;
//
// Special debug control registers.
//
DWORD64 DebugControl;
DWORD64 LastBranchToRip;
DWORD64 LastBranchFromRip;
DWORD64 LastExceptionToRip;
DWORD64 LastExceptionFromRip;
} CONTEXT, *PCONTEXT;
内核态异常
IDA生成的伪代码好像有逻辑错误,因此我这里展示注释后的汇编代码(部分),还做成了流程图,其他地方补充说明,节省文字。
汇编代码:
流程图:
注意:
1.内核态异常用常规手段是不能调试的。因为内核和内核调试器之间也是通过上述部分进行分发。调试时会引起死锁!虚拟机和内核调试器都会卡死。
2.内核的RtlDispatchException
内只有SEH(结构化异常处理,无向量化异常处理..)。
用户态异常
相比于内核态的异常分发,用户态的异常分发就显得十分复杂了。先展示流程图:
第一轮分发
因为转伪代码后各个模块太过分散,都是用goto连接的,比较混乱,所以我这里依旧使用注释后的汇编展示,其他地方补充说明。
汇编代码(部分):少了调用 KiuserExceptionDispatcher
部分 、 内核调试器处理后返回的部分、用户调试器成功处理返回的部分。
例:
demo 1 无异常处理.exe 代码:
#include<iostream>
int main()
{
int* p = (int*)0x8888;
system("pause");
*p = 0x9999;//触发 无权限写异常
return 0;
}
我们在上图 call DbgkForwardException 处下一个断点。
(可以使用内核调试器调试KiDispatchException
函数中处理用户态异常的部分)
虚拟机中使用调试器运行该程序:(触发异常)
虚拟机卡死,内核调试器中断在发往用户调试器(call DbgkForwardException)处。
我们单步步过(kd>p)一定要是步过,不是运行!
此时我们可以看到虚拟机中的windbg收到了 提示 (first chance)
我们在虚拟机的windbg中输入g,回车!
虚拟机立马卡死,内核调试器正好到下一行(因为我们刚刚是单步步过,不是运行)。
我们查看 寄存器al,发现是 0 ,说明第一次发往用户调试器,用户调试器没有解决这个异常。
看流程图,下一步就是要调用 用户态的 KiUserExceptionDispatcher
,将此异常发送给触发异常的程序,进行 VEH(向量化异常处理)和SEH(结构化异常处理)
调用用户态的KiUserExceptionDispatcher
此处的
KeUserExceptionDispatcher
,存放的就是用户态下
KiUserExceptionDispatcher
,上图的注释打错了。例:
1: kd> dq keuserexceptiondispatcher l1
fffff804`2bb43900 00007ffd`332633d0
通过修改 KTRAP_FRAME 中的栈地址、段寄存器。将rip定位到
KiUserExceptionDispatcher
,再通过
KiSetuoForInstrumentationReturn
返回到用户态。这个修改rip的手段类似于 VEH Hook的手段。
KiUserExceptionDispatcher
汇编代码:
注意:
1.此处的RtlDispatchException
处于 ntdll.dll 模块,和内核态的同名函数(见上文)是不同的。
2.只有VEH(向量化异常处理)成功才会执行到 RtlGuardRestoreContext
,SEH(结构化异常处理)若返回EXCEPTION_EXECUTE_HANDLER 是直接跳走的。(下文细说,很重要!)
3.当VEH(向量化异常处理)和SEH(结构化异常处理)都不能成功的时候,调用ZwRaiseException
以第二轮分发为目的(FirstChance = 0)再次进入KiDispatchException
。
第二轮分发
伪代码:
说明:
DbgkForwardException
函数的第二个参数:置1时,发往用户调试器。置0时,发往“监控程序”
第二次发往调试器:
我们同样在第二次发往调试器的汇编代码处下断点。运行!断下!单步(kd>p)!
用户调试器收到消息 (second chance)
用户调试器输入 g 继续运行。
虚拟机卡死,内核调试器断在调用用户调试器的下一行。查看寄存器,发现返回 1 。
说明用户调试器第二次收到异常消息时,默认返回 “已处理”。
注意:
此时继续运行程序,会重新进入 first chance,因为虽然它返回了“已处理”,但实际上异常还在,返回去执行时,依旧会触发异常。会重新进入第一轮异常分发,重新开始! 但是这种情况只会出现在 有用户调试器的情况下。
例:
发往“监控程序”:
我们可以查看 进程 _EPROCESS结构中的ExceptionPort的值。
例:
2: kd> .thread
Implicit thread is now ffff9d0e`595db080
2: kd> dt _kthread ffff9d0e`595db080 -y process
nt!_KTHREAD
+0x074 ProcessDetachActive : 0y0
+0x078 ProcessStackCountDecremented : 0y0
+0x220 Process : 0xffff9d0e`56c52080 _KPROCESS
2: kd> dt _eprocess 0xffff9d0e`56c52080 -y ExceptionPort
nt!_EPROCESS
+0x350 ExceptionPortData : 0xffff9d0e`56eefce0 Void
+0x350 ExceptionPortValue : 0xffff9d0e`56eefce0
+0x350 ExceptionPortState : 0y000
2: kd> !findhandle ffff9d0e56eefce0
[ffff9d0e56f23140 csrss.exe]
78: Entry ffffc40faa79a1e0 Granted Access 1f0001 (Protected) (Inherit)
通过命令我们可以得知这个 ExceptionPort 最终连接到的是 csrss.exe 。就是说如果第二次发往用户调试器不处理,且这个进程也不能也不能解决这个异常的话,程序就会被终止。(一般不会出现这种情况)
总结
关于用户态异常处理的更多细节(选读)
是否交由内核调试器处理
KdIgnoreUmExceptions
当没有用户调试器时,这个符号将决定是否将异常发往内核调试器处理。置 1 时表明需要内核调试器忽略此异常(不发往内核调试器)。置0时说明不忽略该异常,需要先把该异常先发往内核调试器。例:
demo:
#include<iostream>
#include<Windows.h>
int main()
{
system("pause");
DebugBreak();
printf("Get Exception!\n");
system("pause");
}
内核调试器输入命令:
0: kd> eb KdIgnoreUmExceptions 0
运行demo(直接运行或 反反调试 调试器运行):
内核调试器断在了用户态。这时候就可以用内核调试器调试Ring 3进程了。
KdIsThisAKdTrap
此函数返回值确定了是否强制先将异常发往内核调试器处理。(不论Debugport是否有值,KdIgnoreUmExceptions是否为0)
该函数只有一个参数,即为 EXCEPTION_RECORD 详情见上文。
先判断EXCEPTION_RECORD.ExceptionCode是否为满足某些要求,可以在msdn上查询,或者在 minwinbase.h上查询。
再判断参数的数量是否满足要求。才确定该异常是否发往内核调试器处理。
个人认为此处应该是微软开发Windows时为了方便调试而留下来的。
因为我找了许多地方都没有找到EXCEPTION_RECORD.ExceptionCode = 0x4000001F 是什么意思。而且ExceptionInformation的值和对应ExceptionCode也只公开了两个,可以在 此处 查看msdn。
ntdll!RtlDispatchException说明
处于ntdll模块的 RtlDispatchException
负责对第一次发到用户调试器未处理的异常进行向量化、结构化异常处理。
图示:
基础:
返回 |
值 |
意义 |
EXCEPTION_EXECUTE_HANDLER |
1 |
系统将控制权转移给异常处理程序,并在找到处理程序的堆栈帧中继续执行。 |
EXCEPTION_CONTINUE_SEARCH |
0 |
系统继续搜索处理程序。 |
EXCEPTION_CONTINUE_EXECUTION |
-1 |
系统停止搜索处理程序并将控制权返回到发生异常的点。 |
向量化异常处理:
RtlpCallVectoredHandlers
demo:
#include<iostream>
#include<Windows.h>
LONG NTAPI ExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
printf("Get Exception!\n");
system("pause");
}
return EXCEPTION_CONTINUE_EXECUTION;
}
int main()
{
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)ExceptionFilter);//添加VEH异常处理
int* p = (int*)0x8888;
system("pause");
*p = 0x9999;
return 0;
}
由于我们自定义的向量化异常处理函数(ExceptionFilter
)默认返回-1,结合流程,则将该异常视为已解决或已忽略。RtlDispatchException
直接返回 1。回到异常处继续执行,而因为异常为真正解决,因此会再次触发异常。表现为:
一直是First Chance。
返回 1 和 0 的情况和注册多个向量化异常处理的情况 略。
结构化异常处理:
RtlpExecuteHandlerForException
这个部分也许是和编译器有关的,我是使用VS2019进行编译。
demo:
#include<iostream>
#include<Windows.h>
int main()
{
int* p = (int*)0x8888;
system("pause");
__try {
*p = 0x9999;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
printf("Get Exception!\n");
system("pause");
}
return 0;
}
我们在上述 RtlpExecuteHandlerForException
处下断点。单步步进到 call rax
步入。
执行到这:
00000001`40001c90 ff2502040000 jmp qword ptr [__+0x2098 (00000001`40002098)] ds:00000001`40002098={VCRUNTIME140!__C_specific_handler (00007ffd`2384eb20)}
windbg看的不明显,我们用x64dbg查看:
跟进去,
调用了
vcruntime140 ! __c_specific_handler
。这个函数就是进行结构化异常处理。不论你的
__try{....}
__except(EXCEPTION_EXECUTE_HANDLER){....}
有多少层,它都是在这个函数里面进行处理。一旦寻找到对应的处理方式,就会调用 RtlUnwindEx
进行跳转。例:
第二个参数,rdx = 0x140001091 就是接下来要执行的地址,我们跟过去,下个断点,继续执行 或 单步步过:
此时的调用栈:
0:000> k
# Child-SP RetAddr Call Site
00 00000000`0014fec0 00000001`400012e0 __+0x1091
01 00000000`0014fef0 00007ffd`32c081f4 __+0x12e0
02 00000000`0014ff30 00007ffd`3322a251 KERNEL32!BaseThreadInitThunk+0x14
03 00000000`0014ff60 00000000`00000000 ntdll!RtlUserThreadStart+0x21
注意!RtlUnwindEx
是“飞”去执行 0x140001091 的。执行后并不会回到下一行。没有返回值。说明这个异常就已经结束了。
补充:except(EXCEPTION_EXECUTE_HANDLER){....}可以改为 except(seh()){ ..do something } ,那么就可以通过 seh() 返回的值确定是否 通过RtlUnwindEx 跳到 花括号内的指令去执行,例如:
先执行 seh() 通过 seh() 的返回值判断,是否调用RtlUnwindEx飞到花括号里面执行(返回EXCEPTION_EXECUTE_HANDLER时)。还是按照调用栈返回。
如果没有结构化异常处理的代码:
demo:
#include<iostream>
int main()
{
int* p = (int*)0x8888;
system("pause");
*p = 0x9999;//触发 无权限写异常
return 0;
}
依旧会执行到vcruntime140 ! __c_specific_handler
但是由于没有结构化异常处理的代码。因此第一次RtlpExecuteHandlerForException
返回1。
寻找第二个 处理手段,上述(图)说了第二个处理手段就是 ntdll ! __c_specific_handler
和第一个重名且代码基本一致,但是属于不同的模块。
这个函数很重要!说明了为何出现异常的进程会被终止:该函数先会查询该进程是否被调试(可绕过),若正在被调试,就返回 1 。若没有被调试,则通过RtlUnwindEx
去执行ZwTerminateProcess
。例:
开启反反调试的x64dbg,在ntdll ! __c_specific_handler
中的RtlUnwindEx
下断点。执行!
下一步将会 执行到 rdx = 0x7ffd3322a267,跟过去下断点,执行或单步步过。
可以发现接下来就会调用
ZwTerminateProcess
终止进程。
UEH(顶层异常处理)
UEH也属于SEH,是SEH的最后一个。是ntdll ! c_specific_handler负责派发的(函数同vcruntime140 ! c_specific_handler 看上图)。当返回EXCEPTION_EXECUTE_HANDLER时,通过RtlUnwindEx 进入上图的0x7ffd3322a267处执行ZwTerminateProcess终止进程。
当返回其他值时按照调用栈返回。例如:
VCH异常处理
大家注意到了流程图中的 “(假)向量化异常处理”。其实是VCH异常处理。感谢 Qfrost 纠错。图我就不改了 -w-
RtlpCallVectoredHandlers
有三个参数,第三个参数表明我们注册的向量化异常处理类型。
0:VEH 1:VCH
在RtlDispatchException
中,最后返回之前,总要调用一次RtlpCallVectoredHandlers
,并且第三个参数置1。
。