文章目录
VEH
当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接处理,而是修正3环EIP为KiUserExceptionDispatcher函数后就结束了
这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行
KiUserExceptionDispatcher函数分析
在ntdll中找到KiUserExceptionDispatcher函数
首先会调用RtlDispatchException函数,通过这个函数找到当前用户异常的处理函数。如果处理成功了,就会调用ZwContinue重新进入0环。
那么, 如果RtlDispatchException没有找到当前用户异常的处理函数,就会调用ZwRaiseException对当前的异常进行第二次分发
关键在于对RtlDispatchException函数的理解,这个函数的作用就是找到异常的处理函数,一个一个调用,如果有异常处理函数能够处理当前的异常,就直接调用处理函数,然后返回。
###RtlDispatchException函数分析
这里会调用RtlCallVectoredExceptionHandlers函数,这个函数的作用就是找VEH全局链表,这个链表里存储的是一个一个的异常处理函数。如果找到了函数直接返回
如果没有找到则会继续找SEH
代码实现添加VEH异常处理函数
typedef PVOID(NTAPI *FnAddVectoredExceptionHandler)(ULONG, _EXCEPTION_POINTERS*);
FnAddVectoredExceptionHandler MyAddVectoredExceptionHandler;
LONG NTAPI VectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
MessageBox(NULL,L"VEH异常处理函数执行了...",L"VEH异常",MB_OK);
if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xC0000094)//除0异常
{
//将除数修改为1
pExcepInfo->ContextRecord->Ecx = 1;
//修改发生异常的代码的Eip idiv ecx长度2字节 从下一行开始执行
pExcepInfo->ContextRecord->Eip = pExcepInfo->ContextRecord->Eip + 2;
return EXCEPTION_CONTINUE_EXECUTION;//已处理
}
return EXCEPTION_CONTINUE_SEARCH;//未处理
}
int main()
{
//动态获取AddVectoredExceptionHandler函数地址
HMODULE hModule = GetModuleHandle(L"Kernel32.dll");
MyAddVectoredExceptionHandler = (FnAddVectoredExceptionHandler)::GetProcAddress(hModule,"AddVectoredExceptionHandler");
//参数1表示插入VEH链的头部, 0插入到VEH链的尾部
MyAddVectoredExceptionHandler(0, (_EXCEPTION_POINTERS *)&VectExcepHandler);
//构造除0异常
int val = 0;
_asm
{
xor edx, edx
xor ecx, ecx
mov eax, 100
idiv ecx //edx = eax / ecx
mov val, edx
}
printf("val = %d\n",val);
getchar();
}
代码解释:
首先定义了一个VEH的异常处理函数VectExcepHandler,这个函数只能有两个返回值:
- EXCEPTION_CONTINUE_EXECUTION 表示异常已处理
- EXCEPTION_CONTINUE_SEARCH 表示异常未处理
那么我们怎么将异常处理函数插入到链表里呢?这里需要用到一个函数AddVectoredExceptionHandler
,这个函数位于Kernel32.dll
。这里需要动态获取函数地址。
动态获取到函数地址以后,就可以将异常处理函数写到VEH全局链表里
接着再构造一个除0异常,接着异常触发,执行我们写的异常处理函数
这个异常处理函数的参数是一个结构体指针,有两个成员
typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
第一个成员ExceptionRecord是异常发生时的信息,第二个成员ContextRecord是异常发生时的上下文环境。
有了这个参数我们就可以捕获异常发生时的相关信息并且修改异常发生时的寄存器环境。
代码中是先判断异常代码是否为除0异常,然后修改发生异常的Eip和Ecx,接着返回异常已处理。如果不是除0异常就返回异常未处理。
VEH异常的处理流程
- CPU捕获异常
- 通过KiDispatchException进行分发(3环异常将EIP修改为KiUserExceptionDispatcher)
- KiUserExceptionDispatcher调用RtlDispatchException
- RtlDispatchException查找VEH处理函数链表,并调用相关处理函数
- 代码返回到ZwContinue再次进入0环
- 线程再次返回3环后,从修正的位置开始执行
SEH
KiUserExceptionDispatcher会调用RtlDispatchException函数来查找并调用异常处理函数,查找的顺序:
- 先查全局链表:VEH
- 再查局部链表:SEH
ExceptionList
SEH是线程相关的,存储在线程的堆栈中。通过FS:0可以找到这个异常处理函数链表。
3环的FS:0指向的是TEB
kd> dt _TEB
ntdll!_TEB
+0x000 NtTib : _NT_TIB
而TEB的第一个成员是一个子结构体_NT_TIB
kd> dt _NT_TIB
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
这个子结构体的第一个成员就是ExceptionList,异常处理链表
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD* Next; //下一个节点,-1就是没有下一个节点了
PEXCEPTION_ROUTINE Handler; //指向异常处理函数
} EXCEPTION_REGISTRATION_RECORD;
这个结构体至少包含两个成员。第一个成员指向下一个节点,第二个成员指向SEH处理函数。
这样一个一个的结构体在当前的堆栈中构建了一个局部的链表。如下图:
RtlDispatchException函数的执行流程
这个函数首先会查VEH,接着调用RtlpGetStackLimits
,我们跟进这个函数
这个函数会将FS:8和FS:4位置的值取出
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
这两个值分别是StackBase和StackLimit,当前堆栈的起始位置和界限。取这两个值是为了检测SEH处理函数是否处于当前的堆栈中。
.text:7C9577F0 call _RtlpGetRegistrationHead@0 ; 获取异常链表头
接着调用RtlpGetRegistrationHead,获取异常链表头
.text:7C957834 push eax
.text:7C957835 call _RtlIsValidHandler@4 ; RtlIsValidHandler(x)
然后检测当前的处理函数是否有效
.text:7C95784F push dword ptr [ebx+4]
.text:7C957852 lea eax, [ebp+var_14]
.text:7C957855 push eax
.text:7C957856 push [ebp+arg_4]
.text:7C957859 push ebx
.text:7C95785A push esi
.text:7C95785B call _RtlpExecuteHandlerForException
然后调用异常处理函数
代码实现添加SEH异常处理函数
//最原始的 SEH链表结构(这个结构怎么写都行)
struct _EXCEPTION
{
struct _EXCEPTION* Next;
DWORD Handler;
};
EXCEPTION_DISPOSITION _cdecl MyEexception_handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
PVOID DispatcherContext
)
{
MessageBox(NULL,L"SEH异常处理函数执行了...",L"SEH异常",NULL);
if (ExceptionRecord->ExceptionCode == 0xC0000094)
{
ContextRecord->Eip = ContextRecord->Eip + 2;
ContextRecord->Ecx = 100;
return ExceptionContinueExecution;
}
return ExceptionContinueSearch;
}
int main()
{
DWORD temp;
_EXCEPTION Exception;//必须在当前线程的堆栈中
//fs[0]-> Exception
_asm
{
mov eax, fs:[0]
mov temp,eax
lea ecx,Exception
mov fs:[0],ecx
}
//为SEH成员赋值
Exception.Next = (_EXCEPTION*)temp;
Exception.Handler = (DWORD)&MyEexception_handler;
//创建异常
int val = 0;
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,1
idiv ecx //edx = eax /ecx
mov val,ecx
}
//摘除刚插入的SEH
_asm
{
mov eax, temp
mov fs:[0],eax
}
printf("val = %d",val);
getchar();
}
代码解释:
首先需要定义一个和ExceptionList一样的结构体,这个结构体可以随便取名。
由于SEH处理函数是提供给RtlDispatchException调用的,所以需要遵循一定的格式。
EXCEPTION_DISPOSITION _cdecl MyEexception_handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
PVOID DispatcherContext
)
- ExceptionRecord:记录异常产生时的信息结构体
- EstablisherFrame:指向堆栈中的异常结构体地址
- ContextRecord:存储异常发生时的各种寄存器的值 栈位置等
代码流程如下:
- 在main函数里创建了一个异常结构体,这个结构体必须在当前的堆栈中,
- 利用内联汇编的方式让FS:0指向刚刚创建的异常结构体
- 对异常结构体进行赋值
- 抛出异常
- 等异常处理完成以后摘除SEH
SEH异常的处理流程
- FS:[0]指向SEH链表的第一个成员
- SEH的异常处理函数必须在当前线程的堆栈中
- 只有当VEH中的异常处理函数不存在或者不处理才会到SEH链表中查找