异常学习
一丶CPU异常
1.1 异常处理简介
此为Windows异常处理。学习异常可以帮助我们更好的调试程序。或者更好的寻找异常点。
先说一下异常产生之后要进行的操作
-
异常记录
-
异常分发
-
异常处理
异常产生后第一步就是要进行异常记录
原因是首先要记录哪里产生了异常。 异常的类型是什么。 第二步就是要分发异常
分发异常指的就是去寻找异常处理函数
找到函数之后就会进行调用,调用的函数我们就称为异常处理
。
1.2 异常的分类之CPU异常
异常分类有两种,第一种就是CPU产生的异常。 第二种就是软件模拟的产生的异常。
CPU异常如下:
C++代码示例
- CPU异常示例
int main(int argc, char **argv)
{
int x = 10;
int y = 0;
int c = x / y;
return 0;
}
提示我们除零异常
这个是CPU可以检测出来的。
- 软件模拟的异常
void fun()
{
throw 1;
}
int main(int argc, char **argv)
{
fun();
return 0;
}
当我们使用高级语言编写代码的时候。多多少少高级语言都会提供异常处理机制。
比如C++ 我们可以使用 throw
关键字来抛出一个异常。
这个就是软件模拟的异常。
1.3 CPU异常的 记录
CPU 检测到异常之后会进行如下操作。
XP下
WIN10 64位下
这里说下64位下的把。
其实最重要的就是 KiExceptionDisPatch
此函数会先 进行异常记录
此函数结构大概为:
__int64 __fastcall KiExceptionDispatch(
int arg_ExceptionCode,
unsigned int arg_ExceptionNumberParameters,
void *arg_ExceptionAddress,
unsigned __int64 arg_ExceptionInformation,
char a5)
参数 | 说明 |
---|---|
arg_ExceptionCode | 异常记录的代码 |
arg_ExceptionNumberParameters | 异常记录的附加参数大小 |
arg_ExceptionAddress | 异常记录出错的地址 |
arg_ExceptionInformation | 异常记录附加参数数组信息 |
a5 | 暂时未知 |
在调用函数之前代码汇编代码如下:
.text:00000001401C72F3 B9 03 00 00 10 mov ecx, 10000003h
.text:00000001401C72F8 33 D2 xor edx, edx
.text:00000001401C72FA 4C 8B 85 E8 00+ mov r8, [rbp+0E8h]
.text:00000001401C72FA 00 00
.text:00000001401C7301 E8 7A 71 00 00 call KiExceptionDispatch
传递了一个 10000003
值。此值就是我们的除零异常的的SWITCH值
注意 除零异常的值 应该是 0xC0000094
这个后面说。
下图为 KiExceptionDisPatch
异常记录区域。可以看到会把我们的 switch 值
进行记录 并且会记录我们出现异常的地址
异常记录结构体
为如下:
#defien EXCEPTION_MAXIMUM_PARAMETERS 15
typedef struct _EXCEPTION_RECORD32 {
DWORD ExceptionCode;
DWORD ExceptionFlags;
DWORD ExceptionRecord;
DWORD ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD32, *PEXCEPTION_RECORD32;
typedef struct _EXCEPTION_RECORD64 {
DWORD ExceptionCode;
DWORD ExceptionFlags;
DWORD64 ExceptionRecord;
DWORD64 ExceptionAddress;
DWORD NumberParameters;
DWORD __unusedAlignment;
DWORD64 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD64, *PEXCEPTION_RECORD64;
含义如下:
参数 | 说明 |
---|---|
ExceptionCode |
记录异常的代码 |
ExceptionFlags |
记录异常的状态 确定我们是CPU异常还是 软件模拟的异常 还是嵌套异常。根据不同场合值也不一样。 |
ExceptionRecord |
指向下一个异常记录。当出现嵌套异常的时候会记录下一个异常。 |
ExceptionAddress |
记录异常发生的地址 |
NumberParameters | 附加参数 |
ExceptionInformation | 附加参数指针 |
观看结构体,其中最重要的其实就是前4个成员。 此结构体 我称为异常记录结构体
,此结构体也是我所说的异常产生的时候进行的第一步。记录异常信息。此结构体中我们重点还需要知道 ExceptionCode ExceptionAddress
这两个成员分别记录的异常代码和异常地址。这在我们调试的时候很有帮助。
假设我们是 DIV 0 的异常
那么CPU 回去 IDT表中寻找 KiDivideErrorFaultShadow 此函数会最终调用到 KiDivideErrorFault
。
如果在WINDBG
中调试的时候 输入 !IDT
并且有符号的情况下则可以看到 IDT表中的符号名称。
反汇编查看 KiDivideErrorFault
则可以看到异常流程。
1.4 CPU异常的分发
异常的分发就是去寻找异常处理函数。 我们继续反汇编
KiExceptionDisPatch
可以看到异常分发函数
反汇编
再上图上我们可以看到会继续调用 KiDispatchException
此函数如下:
void KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
参数 | 说明 |
---|---|
ExceptionRecord | 异常记录结构体 |
ExceptionFrame | 异常信息框架 |
TrapFrame | 陷阱框架 |
PreviousMode | 先前模式 |
FirstChance | 机会。第几次异常 |
继续反汇编
在反汇编过程中 可以看到。 他会对比异常记录结构体中记录的switch值 然后进行修复
如果我们是除零异常
则会修复异常错误代码为 0xC0000094
;
在后面会着重说一下 异常分发和处理。 这里先抛出问题。知道我们 CPU产生异常了 如何进行 异常记录 异常分发掉哟个你的函数是哪个
1.5 CPU 异常记录小总结
CPU产生除零
异常后会调用 IDT表中 KiDivideErrorFaultShadow
然后此函数继续调用 KiDivideErrorFault
这两个函数都不会进行异常处理只是继续往内核层面下发发。
KiDivideErrorFault
继续调用 KiExceptionDispatch
然后继续调用 KiDispatchException
其中异常记录
是在 KiExceptionDispatch
中产生的。 KiExceptionDispatch
会生成一个 结构体 我们称为异常记录结构体(EXCEPTION_RECORD)
此结构体来保存上层传递过来的 异常代码 异常地址 等信息。
最中会交给 KiDispatchException
去分发。
还了解两个函数。 这两个函数可以在调试中帮助我们来快速定位问题。
void KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
__int64 __fastcall KiExceptionDispatch(
int arg_ExceptionCode,
unsigned int arg_ExceptionNumberParameters,
void *arg_ExceptionAddress,
unsigned __int64 arg_ExceptionInformation,
char a5)
二丶软件模拟异常
2.1 反汇编下追踪模拟异常
软件模拟异常 我们可以使用上面的C++ 代码。
void fun()
{
throw 1;
}
int main(int argc, char **argv)
{
fun();
return 0;
}
断点下在 throw 1
位置 然后反汇编查看代码
代码中会调用 __CxxThrowException@8
函数。并且传递了两个参数 一个参数为 _TI1H
结构 另一个为我们自定义的错误码 在C++ 如何想要专门识别异常处理得专门开一篇文章介绍。 所以在这里不在介绍。后面会单独将。 我们继续往下走。
继续往下走则会调用到Kernel32中得 RaiseException
而Kernel32中的 RaiseException
只是一个stub
函数,它是对 Kernelbase.dll中得RaiseException
做了封装。
所以我们直接IDA分析 KernelBase
主要 要拷贝32位的 Kernelbase.dll
分析
此函数结构如下:
void __stdcall RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
const ULONG_PTR *lpArguments);
反汇编如下:
可以看到 还是生成一个 异常记录结构体
并对其填充
如果是软件异常 则异常记录结构体填充为1
记录异常的地址填充为了 RaiseException
本身
记录的异常代码为上层传递的。异常由当前编译环境给填充的。
最后调用到 ntdll.dll中的RtlRaiseException
继续反汇编 RtlRaiseException
函数如下:
void __stdcall __noreturn RtlRaiseException(
PEXCEPTION_RECORD ExceptionRecord)
反汇编如下:
可以看到首先 使用 RtlCaptureContext
获取当前环境的异常上下文信息(上下文就是记录寄存器的一些信息) 然后设置 异常记录结构体中的异常地址为返回地址。 最后调用 ZwRaiseException
调用到内核。
在内核中则是KiRaiseException
处理异常。然后最终调用到 KiDispatchException
在调用KiDispatchException
的时候 会把 EXCEPTION_RECORD.ExceptionCode
最高位清零
,用来区分CPU异常
。 也就是上图所示
2.2 软件模拟异常 产生的总结图
根据2.1小节,跟出了软件模拟一个异常的时候的步骤。
那么画图进行总结一下。
其中还了解到了两个API
void __stdcall RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
const ULONG_PTR *lpArguments);
void __stdcall __noreturn RtlRaiseException(
PEXCEPTION_RECORD ExceptionRecord)
三丶CPU异常与软件模拟异常图总结
3.1 总结图如下
其中 ring3的 记录异常 会将记录的异常地址设置为 RaiseException的地址
异常代码 由当前编译环境决定
KiRaiseException
会为了区分是CPU一行还是软件异常来设置 异常记录.异常代码 的最高位为0
CPU的异常记录函数为 KiExceptionDisPatch
最后都会发送给 KiDispatchException
进行异常分发
四丶内核层下的异常分发 KiDispatchException的详解
4.1 内核异常分发 学习
-
简单了解
我们知道 不管是 ring3 还是 ring0 出了异常都会走到
KiDispatchException
函数进行异常分发(也就是找异常处理函数) 但是此函数很复杂。但是学好此函数也就是学明白异常的关键。 这里 在内核下发生的异常我们称为内核异常
反之就是用户模式异常
函数结构如下:
void KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
内核层面的流程
内核层面其实很简单
-
保存TrapFrame(一些ring3的上下文信息) 返回ring3做准备
-
判断先前模式 0 = 内核 1= ring3
-
判断是否是第一次调试机会
-
判断是否有内核调试器
-
如果没有或者内核调试器不处理
-
调用
RtlDispatchException
7.如果返回值是FALSE 会继续判断是否有内核调试器
8 根据7 决定发送到内核调试器 还是蓝屏。
所以重点就是新函数 RtlDispatchException
此函数就是专门调用异常处理函数的
此函数结构如下:
BOOLEAN __stdcall RtlDispatchException(
PEXCEPTION_RECORD ExceptionRecord,
PCONTEXT Context)
{
//xxxx
}
参数1 是记录的异常信息
参数2 是记录的上下文信息。 比如记录了 RIP RAX 等寄存器
此函数本质上就是 遍历异常处理链表
来进行异常处理
4.2 异常信息记录链表结构体
异常信息结构体存储在 FS:[0] 在ring3下也就是 TEB的环境块
TEB结构块如下:
nt!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
+0x020 ClientId : _CLIENT_ID
+0x028 ActiveRpcHandle : Ptr32 Void
+0x02c ThreadLocalStoragePointer : Ptr32 Void
+0x030 ProcessEnvironmentBlock : Ptr32 _PEB
其中第一个成员结构如下:
typedef struct _NT_TIB
{
PEXCEPTION_REGISTRATION_RECORD ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union
{
PVOID FiberData;
ULONG Version;
};
PVOID ArbitraryUserPointer;
PNT_TIB Self;
} NT_TIB, *PNT_TIB;
NT_TIB结构的第一个成员记录的就是异常记录链表
。 异常记录链表
中记录的就是异常处理函数
结构如下:
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
RtlDispatchException
本质上就是遍历此结构体来进行异常函数寻找调用处理的。
此结构如上 第一个成员是一个NEXT域 指向下一个 异常处理信息结构体的
第二个成员如下指向一个异常处理函数。异常处理函数结构如下
NTAPI
EXCEPTION_ROUTINE (
_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT *ContextRecord,
_In_ PVOID DispatcherContext
);
异常处理函数 有四个参数 其中第一个参数我们都应该很熟悉了 ,就是异常信息记录结构
参数3 是一个上下文 记录着 寄存器的信息 也是很重要的参数。
异常处理函数 可以进行异常处理。 如果处理了那么 返回值就是1 那么就不会继续寻找异常处理函数了。
4.3 内核异常处理总体流程总结图
其中我们要知道 FS:[0]中指向的是一个异常记录链表
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
异常记录链表中的第二个成员是异常处理函数
NTAPI
EXCEPTION_ROUTINE (
_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT *ContextRecord,
_In_ PVOID DispatcherContext
);
RtlDispatchException
会循环遍历链表进行调用 异常处理函数
五丶用户模式下的异常分发(内核) KiDispatchException的详解
5.1 反汇编内核中的KiDispatchException 查看用户异常异常流程
观看反汇编总结如下:
-
KeContextFromKframes
备份Trap_frame 到context中 -
判断先前模式 内核调用还是用户调用 0 = 内核 1 = 用户
-
是否有第一次机会
-
是否有内核调试器
-
发送给ring3调试器
DbgkForwardException
-
如果3环调试器没有处理异常那么修正EIP为
KiUserExceptionDispatcher
-
如果是CPU异常那么通过 IRETD返回3环 如果是模拟异常 那么直接通过系统调用返回3环
-
无论哪种方式当线程再次回到3环的时候都会调用
KiUserExceptionDispatcher
5.2 流程图
其中需要注意的是最终由 ring3层的 KiUserExceptionDispatcher
开始分发异常
5.3 3环下的异常处理
ntdll.KiUserExceptionDispatcher
是操作系统返回到3环所进行调用的函数。
所以直接分析此函数即可。
此函数结构如下:
VOID KiUserExceptionDispatcher(
__in PEXCEPTION_RECORD ExceptionRecord,
__in PCONTEXT ContextRecord)
反汇编代码如下:
int __stdcall KiUserExceptionDispatcher(PEXCEPTION_RECORD a1, PCONTEXT a2)
{
_EXCEPTION_RECORD *pRecord; // ebx
int v4; // eax
_DWORD v5[3]; // [esp-8h] [ebp-14h] BYREF
_EXCEPTION_RECORD *pV6Record; // [esp+4h] [ebp-8h]
_CONTEXT *v7; // [esp+8h] [ebp-4h]
if ( !LdrDelegatedKiUserExceptionDispatcher )
{
pRecord = pV6Record;
if ( RtlDispatchException(pV6Record, v7) )
v4 = ZwContinue(v7, 0);
else
v4 = ZwRaiseException((int)pV6Record, (int)v7, 0);
v5[0] = v4;
v5[1] = 1;
v5[2] = pRecord;
v7 = 0;
RtlRaiseException((PEXCEPTION_RECORD)v5);
}
return ((int (__stdcall *)(PEXCEPTION_RECORD, PCONTEXT))LdrDelegatedKiUserExceptionDispatcher)(a1, a2);
}
此结构进行的流程如下
-
调用
RtlDispatchException
查找并执行异常处理函数 -
如果Rtlxx返回真 那么则调用
ZwContinue
再次进入0环。但是再次返回3环的时候 会从修正的地址
开始执行 -
如果Rtlxxx返回假 那么会调用
ZwRaiseException
进行第二轮的异常分发
六丶用户模式下的VEH
6.1 3环下的RtlDispatchException 分析
在3环下我们分析下RtlDispatchException
在代码中可以看到API调用
RtlpCallVectoredHandlers
这个函数的主要作用就是调用一个链表
函数原型如下:
BOOLEAN
NTAPI
RtlpCallVectoredHandlers(IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT Context,
IN PLIST_ENTRY VectoredHandlerList)
{
...
}
它总共三个三次
-
参数1
异常记录结构体
-
参数2
上下文信息保存了寄存器的信息
-
参数3
链表
此链表则是全局链表
我们也称为VEH。 如果它找到了则返回 则 RtlDispatchException
就不再往下执行了。
ReactOS: sdk/lib/rtl/vectoreh.c Source File 此链接有人将这个函数逆向出来了。有兴趣可以看一下。
6.1.2 VEH的全局链表
VEH是全局的,是因为当出现异常的时候首先就是先调用它来进行处理。 假设我们有自己异常要进行处理。我们则可以加入到全局链表中。让其帮我们处理异常。
下面演示如何添加VEH 让其帮我们处理
首先是VEH 处理函数
//#define EXCEPTION_EXECUTE_HANDLER 1 VEH不使用
//#define EXCEPTION_CONTINUE_SEARCH 0 未处理
//#define EXCEPTION_CONTINUE_EXECUTION (-1) 已处理
LONG NTAPI VectExceptionRoutine(PEXCEPTION_POINTERS pExceptionInfo)
{
::MessageBoxA(NULL, "Veh Execute", "VehTitle", 0);
if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
{
pExceptionInfo->ContextRecord->Eip = pExceptionInfo->ContextRecord->Eip + 2;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
VEH处理函数的参数是一个 PEXCEPTION_POINTERS
结构,此结构如下:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
此结构的第一个成员是 异常记录结构体
。 此结构体上面说过很多次了。记录的就是当前异常的一些信息。(异常代码 异常地址 异常标志等等)
参数2 是一个Context
上下文信息 Context上下文信息里面就保存了我们发生异常时候的寄存器。这里面最重要的就是 EIP/RIP
VEH的异常处理函数只能返回两个值。 要么是 已经处理
要么是 未处理
下面就是插入 异常处理函数到VEH链表中了。
这里我们需要使用Ntdll
中的函数 RtlAddVectoredExceptionHandler
在之前是使用
Kernel32
中的 AddVectoredExceptionHandler
但是在我逆向的时候只发现了Ntdll用有此函数了。
所以我们动态获取来进行使用。
完整代码:
typedef PVOID (NTAPI * PfnRtlAddVectoredExceptionHandler)(int isInsertListHeader, PEXCEPTION_POINTERS arg_poniter);
PfnRtlAddVectoredExceptionHandler RtlAddVectoredExceptionHandler = NULL;
//#define EXCEPTION_EXECUTE_HANDLER 1 VEH不使用
//#define EXCEPTION_CONTINUE_SEARCH 0 未处理
//#define EXCEPTION_CONTINUE_EXECUTION (-1) 已处理
LONG NTAPI VectExceptionRoutine(PEXCEPTION_POINTERS pExceptionInfo)
{
::MessageBoxA(NULL, "Veh Execute", "VehTitle", 0);
if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
{
//注释掉的为第一种修复方式,修复EIP + 2 跳过除0的硬编码。
//pExceptionInfo->ContextRecord->Eip = pExceptionInfo->ContextRecord->Eip + 2;
pExceptionInfo->ContextRecord->Ecx = 1; //第二种方式 修复除数为1 那么则不会触发异常
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
int main(int argc, char **argv)
{
HMODULE hModule = NULL;
hModule = GetModuleHandle("ntdll.dll");
if (hModule == NULL)
{
return 0;
}
RtlAddVectoredExceptionHandler =
reinterpret_cast<PfnRtlAddVectoredExceptionHandler>
(::GetProcAddress(hModule, "RtlAddVectoredExceptionHandler"));
if (RtlAddVectoredExceptionHandler == NULL)
{
return 0;
}
RtlAddVectoredExceptionHandler(0, (PEXCEPTION_POINTERS)&VectExceptionRoutine);
//构造异常
__asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx
}
printf("异常处理成功\n");
system("pause");
return 0;
}
实现效果如下: