VEH&SEH异常详解
首发于奇安信攻防社区:https://forum.butian.net/share/1503
前言
因为Trap_Frame
结构在3环的原因,会有一个从0环临时返回3环的过程,所以在用户层的异常执行过程相比于内核层更加复杂。
VEH
首先定位到KiUserExceptionDispatcher
函数,这里首先通过RtlDispatchException
来找到异常处理的函数
如果这里异常没有处理成功会再次分发异常
RtlDispatchException
这里RtlDispatchException
是分发异常的函数,0环跟3环是共用的,但是有一些细节是不同的,我们跟进去看看
首先看一下3环的RtlDispatchException
里面有一个RtlCallVectoredExceptionHandlers
但是在0环的RtlDispatchException
里面是没有这个函数的
跟进RtlCallVectoredExceptionHandlers
,首先找全局链表(VEH链表),存储了很多个异常处理函数,如果在全局链表里面没有找到,就会继续往下找局部链表(SEH链表)
我们尝试用代码来实现VEH
// VEH1.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
typedef PVOID (NTAPI *FnAddVectoredExceptionHandler)(ULONG, _EXCEPTION_POINTERS *);
FnAddVectoredExceptionHandler MyAddVectoredExceptionHeader;
// UEH异常处理函数只能返回2个值
// EXCEPTION_CONTINUE_EXECUTION 已处理
// EXCEPTION_CONTINUE_SEARCH 未处理
LONG NTAPI VectExceptionHandler( PEXCEPTION_POINTERS pExcepInfo )
{
::MessageBoxA(NULL, "VEH Function run", "VEH error", MB_OK);
if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
{
//pExcepInfo->ContextRecord->Eip = pExcepInfo->ContextRecord->Eip + 2;
pExcepInfo->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
VOID VEH()
{
// 动态获取AddUectoredExceptionHandler函数地址
HMODULE hMyModule = GetModuleHandle("kernel32.dll");
MyAddVectoredExceptionHeader = (FnAddVectoredExceptionHandler)::GetProcAddress(hMyModule, "AddVectoredExceptionHandler");
// 参数1表示插入VEH链的头部、0表示插入到UEH链的尾部
MyAddVectoredExceptionHeader( 0, (_EXCEPTION_POINTERS *)&VectExceptionHandler );
// 构造除0异常
__asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx // EDX:EAX 除以 ECX
}
printf("veh_code run here again");
}
int main(int argc, char* argv[])
{
VEH();
getchar();
return 0;
}
AddVectoredExceptionHandler
这里有几个注意的点,首先是注册异常处理函数,使用到AddVectoredExceptionHandler
,这个函数是在kernel32.dll
里面的,在xp以前的版本里面是没有的,所以需要动态获取
PVOID AddVectoredExceptionHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER Handler
);
第二个要注意的点就是veh是一个链表,第一个参数表示插入异常的位置,1的话就是插入到链表的头部,0的话就是插入到链表的尾部
第三个点就是异常处理的指针指向两个结构,一个是ContextRecord
,这个结构里面存储的是所有寄存器的值,另外一个就是ExceptionRecord
,这个结构里面存储的就是异常的具体信息
第四个点就是因为构造的是ecx为0,那么这里异常处理函数就可以修改eip指向的地址或者修改ecx的值为1即可
看下效果,首先是执行了我们自己注册的异常处理函数里面的MessageBoxA
,然后程序正常下向下执行
VEH异常流程
1.CPU捕获异常信息
2.通过KiDispatchException
进行分发(EIP=KiUserExceptionDispatcher
)
3.KiUserExceptionDispatcher
调用RtlDispatchException
4.RtlDispatchException
查找VEH处理函数链表 并调用相关处理函数
5.代码返回到KiUserExceptionDispatcher
6.调用ZwContinue
再次进入0环(ZwContinue
调用NtContinue
,主要作用就是恢复TRAP_FRAME
然后通过KiServiceExit
返回到3环)。
7.线程再次返回3环后,从修正后的位置开始执行
SEH
SEH就是一个跟0环异常处理结构类似的链表
首先看一下RtlpGetStackLimits
取出了fs:[8]
和fs:[4]
我们知道fs:[0]
指向的是_NT_TIB
结构,那么fs:[4]
对应的就是StackBase
,fs:[8]
对应的就是StackLimit
,即基址和界限
然后再拿到fs:[0]
拿到一系列的参数之后,会首先进行一系列的判断
RtlpExecuteHandlerForException
最后调用RtlpExecuteHandlerForException
处理异常
SEH异常的实现
// SEH1.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
// ring0
/*
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
*/
struct MyException
{
struct MyException *prev;
DWORD Handle;
};
EXCEPTION_DISPOSITION __cdecl MyException_Handler(
struct _EXCEPTION_RECORD *ExceptionRecord, // ExceptionRecord 存储异常信息 什么类型异常产生位置
void* EstablisherFrame, // MyException结构体地址
struct _CONTEXT *ContextRecord, // Context结构体 异常发生时的各种寄存器值堆栈位置等
void* DispatcherContext)
{
::MessageBoxA(NULL, "SEH Function", "SEH error", MB_OK);
if (ExceptionRecord->ExceptionCode == 0xC0000094)
{
ContextRecord->Eip = ContextRecord->Eip + 2;
return ExceptionContinueExecution;
}
return ExceptionContinueSearch;
}
void ExceptionTest()
{
DWORD temp;
MyException myException;
__asm
{
mov eax,FS:[0]
mov temp,eax
lea ecx,myException
mov FS:[0],ecx
}
myException.prev = (MyException*)temp;
myException.Handle = (DWORD)&MyException_Handler;
__asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx
}
// 摘除链表
__asm
{
mov eax,temp
mov FS:[0],eax
}
printf("SEH run again");
}
int main(int argc, char* argv[])
{
ExceptionTest();
getchar();
return 0;
}
_EXCEPTION_REGISTRATION
首先定义一个异常处理_EXCEPTION_REGISTRATION
结构
然后定义异常处理函数
EXCEPTION_DISPOSITION __cdecl MyException_Handler(
struct _EXCEPTION_RECORD *ExceptionRecord, // ExceptionRecord 存储异常信息 什么类型异常产生位置
void* EstablisherFrame, // MyException结构体地址
struct _CONTEXT *ContextRecord, // Context结构体 异常发生时的各种寄存器值堆栈位置等
void* DispatcherContext)
然后在当前线程里面声明结构体,把自己的结构体挂到链表里面,并定义Next
指针指向下一个结构体
然后构造除0异常,然后将我们自己定义的结构体从链表里面摘除
运行结果如下
总结
1.FS:[0]指向SEH链表的第一个成员
2.SEH的异常处理函数必须在当前线程的堆栈中
3.只有当VEH中的异常处理函数不存在或者不处理才会到SEH链表中查找
SEH异常流程
1.RtlpGetStackLimits
取出_NT_TIB
结构的fs:[4]
和fs:[8]
,那么fs:[4]
对应的就是StackBase
,fs:[8]
对应的就是StackLimit
,即基址和界限
2.RtlpGetRegistrationHead
取出_NT_TIB
结构的fs:[0]
,取出ExceptionList
3.然后调用RtlpExecuteHandlerForException
处理异常
SEH编译器扩展
编译器可以直接帮我们进行简化挂入链表、异常过滤、执行异常处理程序的操作
在过滤表达式处只能有以下三种情况
1) EXCEPTION_EXECUTE_HANDLER(1) 执行except代码
2) EXCEPTION_CONTINUE_SEARCH(0) 寻找下一个异常处理函数
3) EXCEPTION_CONTINUE_EXECUTION(-1) 返回出错位置重新执行
而过滤表达式可以有3种写法
1) 直接写常量值
2) 表达式
3) 调用函数
常量值
// SEH2.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
void ExceptionTest()
{
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx
}
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
printf("SEH function run");
}
}
int main(int argc, char* argv[])
{
ExceptionTest();
getchar();
return 0;
}
表达式
使用GetExceptionCode
得到异常码
// SEH3.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
void ExceptionTest()
{
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx
}
}
_except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH)
{
printf("SEH function run");
}
}
int main(int argc, char* argv[])
{
ExceptionTest();
getchar();
return 0;
}
调用函数
使用GetExceptionInformation
得到指向异常处理结构的指针
// SEH4.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo)
{
pExceptionInfo->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION; // -1 返回出错位置重新执行
}
void ExceptionTest()
{
_try
{
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx
}
}
_except(ExceptFilter(GetExceptionInformation()))
{
printf("SEH function run");
}
}
int main(int argc, char* argv[])
{
ExceptionTest();
getchar();
return 0;
}
这里先修改值之后回到异常发生的地方重新执行,没有异常了,然后就走到getchar()
的地方
加下方wx,拉你一起进群学习
往期推荐