标签:需要 windows 工作 define 调试 turn padding 因此 using
Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html
异常(1)
1. 异常种类
2. CPU异常的产生
3. 用户模拟异常
4.CPU异常与用户模拟异常的总结
5. 内核层异常的分发与处理
6. 用户层的异常处理
7.VEH异常
8.SEH异常
9.当用户层异常未处理时
1. 异常种类
1)CPU处理异常 (除零异常)
2)软件模拟异常(throw 1)
2. CPU异常的产生
1)处理过程
CPU指令检测到异常->查IDT表,执行中断处理函数->CommonDIspatchException->KiDispatchException。
2)KiTrap00函数分析:
当出现除零异常时,其会走IDT[0]中断,执行KiTrap00这个函数,该函数保存CPU现场,填写KTRAP_FRAME这个结构。
之后,其会调用CommonDispatchException来分发异常。
3)DispatchException函数分析:
该函数目的就是构造_EXCEPTION_RECORD,然后传入KiDispatchException进一步处理异常。
struct _EXCEPTION_RECORD {
LONG ExceptionCode; //0x0
ULONG ExceptionFlags; //0x4
struct _EXCEPTION_RECORD* ExceptionRecord; //0x8
VOID* ExceptionAddress; //0xc ULONG NumberParameters; //0x10
ULONG ExceptionInformation[15]; //0x14
};
3. 用户模拟异常
1)throw 反汇编
在程序中使用代码 throw 1 抛出异常。
0040103F push offset __TI1H (00423580)
00401044 lea eax,[ebp-4]
00401047 push eax // &ThrowCode
00401048 call __CxxThrowException@8 (00401230)
2)在 __CxxThorException函数中调用Kernel32!RaiseException
0040125E push ecx
0040125F mov edx,dword ptr [ebp-20h]
00401262 push edx // ExceptionCode,这个编译器自己模拟的
00401263 call dword ptr [__imp__RaiseException@16 (0042a154)]
其中要注意的一点:在函数中并没有看到 [ebp-20h] 确定的值,ExceptionCode 并不是ThrowCode,而是编译器固定的值。
作为Kernel32!RaiseException第一个参数,这个值是编译器自己确定的,并不是像内核异常一样由CPU确定(比如除零异常0xc0000094)。
3)Kernel32!RaiseException函数分析
该函数包装好EXCEPTION_RECORD结构体,然后调用RtlRaiseException函数。
4)ntdll!RtlRaiseException函数分析
该函数调用_CONTEXT保存三环的工作环境,然后记录第几次发生异常,将其传入ntdll!ZwRaiseException函数中。
5)ntdll!ZwRaiseException函数分析
该函数进入零环,系统服务号0B5h.
6)nt!NtRaiseException函数分析
其本质就是调用KiRaiseException,在这之前做了些简单的处理。
当调用完成时,可以看到调用KiServiceExit来退出函数。
7)nt!KiRaiseException函数分析
其做了一些基本的判断,首先,判断先前模式三环,做了些准备工作;之后将CONTEXT转换为TRAPFRAME;最后将ExceptionCode最高位置0。
之前除零异常,c0000094,最高位为1,表示CPU产生的异常;而如果最高位为0,则表示用户模拟异常。
最后,调用KiDispatchException,同CPU异常一样,进入用户派发函数。
4.CPU异常与用户模拟异常的总结
CPU发生异常时,其直接在零环,而当用户模拟异常,其存在一个三环到零环的过程,相对比较复杂。
其中用户模拟异常从三环进入零环时,有一点特殊,其通过_CONTEXT保存现场而不是_KTRAP_FRAME。
之前我们学习用户APC的过程处理时,其通过零环返回三环处理用户APC函数通过保存在三环_CONTEXT,但是异常已经提前这么做了,我们之后会深入研究。
最后,无论CPU异常还是用户模拟异常,最后都会通过KiDispatchException函数派发的,对于操作系统来说没有区别。
唯一可能区分异常的就是看最高位,如果为1则为CPU异常,如果为0则为用户异常。
5. 内核层异常的分发与处理
1)KiDispatchException分析
无论用户层异常还是内核层异常,最终都是走KiDispatchException这个函数。
其中内核层处理比较直观,因为不用再返回用户层去处理。
2)核心逻辑判断
KeDebugRoutine是内核调试器函数,其判断是否存在内核调试器,如果有就调用内核调试器来处理异常。
RtlDispatchException是异常分发,其_KPCR+0x0处的 ExceptionList 分析。
3)RtlDispatchException函数分析
该函数在处理用户异常时会详细分析,现在先做的一个简单介绍。
_KPCR+0x0 是一个 ExceptionList,里面一个链表,串起来一个_EXCEPTION_REGISTRATION_ROCRD结构,里面存在一个异常处理函数。
我们可以向其中添加函数,来处理异常。
其中Handler异常处理函数的返回值为_EXCEPTION_DISPOSTION,来判断异常处理的结果
enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution = 0, // 异常处理成功
ExceptionContinueSearch = 1, // 异常没有处理,继续寻找
ExceptionNestedException = 2, // 二次异常,存在嵌套异常
ExceptionCollidedUnwind = 3 // 发生嵌套的展开
};
而RtlDispatchException核心就是遍历这张链表,其核心操作如下(用户层发生异常时再回来分析)
4)总结
在内核中出现异常时,执行结果相对比较简单,因为不需要返回三环来处理。
其在KiDispatchException函数中的思路比较清晰,不用太过多的来分析。
6. 用户层的异常处理
用户层出现异常时,处理用户层的函数在三环,因此我们必须返回用户层来处理。
从零环返回三环,其最关键的是堆栈切换,异常返回三环的。
我们之前从零环返回三环,分析过用户APC的执行流程,其三环异常的执行流程与用户APC的执行流程大体相同。
只不过用户APC返回三环的落脚点是 KiUserApcDispatcher,而三环异常的落脚点是KiUserDispatchDispatcher。
1)KiDispatchException函数分析
该函数在判断不是内核异常时,则默认是用户异常来执行代码,如下图。
2)ntdll!KiUserExceptionDispatcher函数分析
返回三环后,可以看到其调用一个RtlDispatchException。
注意,在处理内核异常时,也有一个同名的RtlDispatchException,那是内核模块,这是三环模块。
RtlDispatchException可以认为是异常的核心,区别是如果在内核模块,则处理零环,如果在ntdll模块,则处理三环。
这样你就能很好的区分两者的作用了。
3)ntdll!RtlDispatchException函数分析
其处理两种异常,一种VEH异常,一种SEH异常。
VEH异常相当于一个全局变量的异常链表,其通过全局变量查找该张表。
SEH异常相当于局部异常,其Try··catch··就是向这里面添加异常,TEB、KPCR第一个成员都是这个ExceptionList。
我们先分析这里,之后会分析VEH异常与SEH异常,之后再来分析这个函数。
7.VEH异常
1)VEH异常链表是一个全局链表,其模板如下:
LONG NTAPI VehFunc(struct _EXCEPTION_POINTERS* ExceptionInfo) {
return EXCEPTION_CONTINUE_SEARCH;
}
int main(int argc, char* argv[])
{
// 1-veh链头部,0-veh链尾部。
AddVectoredExceptionHandler(1,VehFunc);
return 0;
}
其中 _EXCEPTION_POINTER结构体如下:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; // 异常记录
PCONTEXT ContextRecord; // 异常发生时的各个寄存器的值
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
2)VEH异常的返回值
#define EXCEPTION_EXECUTE_HANDLER 1 // 异常被识别,_except模块中处理该异常
#define EXCEPTION_CONTINUE_SEARCH 0 // 异常未被识别,继续调用下一个Handler来处理异常
#define EXCEPTION_CONTINUE_EXECUTION (-1) // 异常已被忽略或修复,不继续往下寻找
注意:SEH异常与VEH异常返回值是不同的,对于SEH异常,其返回的是一个 enum EXCEPTION_DISPOSITION。
3)我们根据ContextRecord中保存的寄存器我们就可以实现对我们的代码出现异常的修复,下面是除零异常代码的修复:
LONG NTAPI VehFunc(struct _EXCEPTION_POINTERS* ExceptionInfo) { if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000094) { ExceptionInfo->ContextRecord->Eip += 2; printf("除零异常已被处理了!\n"); return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } int main(int argc, char* argv[]) { // 1-veh链头部,0-veh链尾部。 AddVectoredExceptionHandler(1,VehFunc); _asm { mov ebx, 0; mov eax, 1; idiv ebx; } return 0; }
8.SEH异常
SEH异常其存在于堆栈中,头部在Fs:[0]这个寄存器中,其在堆栈中的结构如下图:
1)返回值:虽然在零环下也存在这类值,但是定义是值是不同的,尤其注意继续寻找是返回的是0.
我们在编程中,看到的返回值定义如下:
#define EXCEPTION_EXECUTE_HANDLER 1 // 异常被识别,_except模块中处理该异常
#define EXCEPTION_CONTINUE_SEARCH 0 // 异常未被识别,继续调用下一个Handler来处理异常
#define EXCEPTION_CONTINUE_EXECUTION (-1) // 异常已被忽略或修复,不继续往下寻找
但是在分析ntdll!RtlDispatchException模块中,我们使用的和内核是一致的:
enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution = 0, // 异常处理成功
ExceptionContinueSearch = 1, // 异常没有处理,继续寻找
ExceptionNestedException = 2, // 二次异常,存在嵌套异常
ExceptionCollidedUnwind = 3 // 发生嵌套的展开
};
在分析内核时,应该选取后面那个结构体定义,不要搞混两者。
2)ntdll!RtlDispatchException函数分析
之前我们只是简单的提到过该函数,其先调用VEH异常,如果不成功则调用SEH异常来执行。
下面我们来详细分析一下其SEH异常的执行流程,其思路还是比较清晰的。
3)手动添加SEH异常
我们后面使用编译器拓展的SEH,现在手动添加一下,来感受下它的存在
#include <stdio.h> #include <iostream> #include <windows.h> using namespace std; EXCEPTION_DISPOSITION NTAPI ExceptionRoutine( _Inout_ struct _EXCEPTION_RECORD* ExceptionRecord, _In_ PVOID EstablisherFrame, _Inout_ struct _CONTEXT* ContextRecord, _In_ PVOID DispatcherContext ) { if (ExceptionRecord->ExceptionCode == 0xc0000094) { ContextRecord->Eip += 2; printf("检测到除零异常,正在修复..."); return ExceptionContinueExecution; } return ExceptionContinueSearch; } int main(int argc, char* argv[]) { struct _EXCEPTION_REGISTRATION_RECORD SehRecord; _EXCEPTION_REGISTRATION_RECORD* temp; // 挂入SEH链表中 _asm { mov eax, fs: [0] ; // 获取头部 mov temp, eax; // 原来的头部存入中间变量中 lea eax, SehRecord; // 获取结构体地址 mov fs : [0] , eax; // 将其变量加入头部 } // 给SEH赋值 SehRecord.Handler = ExceptionRoutine; SehRecord.Next = temp; // 触发除零异常 _asm { mov eax, 1; mov ebx, 0; idiv ebx; } return 0; }
9.当用户层异常未处理时
标签:需要 windows 工作 define 调试 turn padding 因此 using
原文地址:https://www.cnblogs.com/onetrainee/p/12611120.html