0.SEH简介
SEH(“Structured Exception Handling”),即结构化异常处理·是windows操作系统处理程序错误或异常的技术。SEH使windows操作系统的一种系统机制,与特定的程序设计语言无关。
1.中断与异常
中断:外部硬件设备或者异步产生
异常:内部事件产生,分为故障(可恢复)、陷阱(可恢复)、终止(不可恢复,系统必须重启) # 2.产生异常
硬件异常:cpu执行指令时引发的异常。举例:int 3 可以引发断点中断。
软件异常:函数调用引发的异常。举例:RaiseException
RaiseException的详细定义
void WINAPI RaiseException(
_In_ DWORD dwExceptionCode, //引发异常的应用程序定义的异常代码。
_In_ DWORD dwExceptionFlags, //异常标志。
_In_ DWORD nNumberOfArguments, //lpArguments数组中的参数数量。可以为null
_In_ const ULONG_PTR * lpArguments //参数,可以为null
);
使用RaiseException实现异常:
#include <stdio.h>
#include <windows.h>
DWORD FilterFunction()
{
printf("1 "); // printed first
return EXCEPTION_EXECUTE_HANDLER;
}
VOID main(VOID)
{
__try
{
__try
{
RaiseException(
RaiseException(
MessageBox(NULL,TEXT("123"),TEXT("WinMain"),MB_OK), // exception code
EXCEPTION_NONCONTINUABLE, // continuable exception
0, NULL);
__finally
{
printf("2 "); // this is printed second
}
}
__except ( FilterFunction() )
{
printf("3\n"); // this is printed last
}
}
在vs2010中编译,可以看到成功运行,但是拖入od之后运行会直接退出
3.异常处理流程
发生异常时系统的处理顺序
1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.调试器可能处理,也可能不处理异常。
2.如果程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.
3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,可交由链起来的其他例程处理.
4.如果这些线程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
5.如果程序未处于被调试状态或者debugger没有能够处理,并且调用SetUnhandledExceptionFilter安装了最后异常处理例程的话,系统转向对它的调用。
6.如果没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序。
7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会。其后程序就终结了。
划重点:seh是系统发现异常或错误的时候,在终结应用程序之前,在这个回调函数里执行一些代码,给予程序,给系统的最后的修正机会。
4.seh的几个数据结构
4.1 teb结构
teb,Thread Environment Block,线程环境块,记录线程的信息,每个线程对应一个teb结构。
typedef struct _NT_TIB //sizeof 1ch
{
00h struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //异常处理相关的结构体
04h PVOID StackBase; //栈基址
08h PVOID StackLimit;
0ch PVOID SubSystemTib;
union {
PVOID FiberData;
10h DWORD Version;
};
14h PVOID ArbitraryUserPointer;
18h struct _NT_TIB *Self; //自引用指针,即为NtCurrentTeb() 函数所读出的TEB结构体指针
}NT_TIB;
windows创建线程时,操作系统会分配teb给线程,将fs段选择器指向当前线程的teb数据,这样就可以让程序通过[FS:0],读取到teb
4.2 EXCEPTION_REGISTRATIO
EXCEPTION_REGISTRATION struc
prev dd ? //指向前一个EXCEPTION_REGISTRATIO的指针
handler dd ? //异常回调函数的地址
EXCEPTION_REGISTRATION ends
如果系统遇到一个不知道如何处理的异常的时候,会去查找异常处理链表。
4.3 应用
使用od打开含有seh的程序,把所有的调试异常全部设置为不可忽略,f9运行。
esi为0,程序试图读取内存为0的地址空间,这是访问违规的,可以看到状态栏的提示
而当把状态栏的忽略非法访问选中,则程序不会出现提示,从调试的角度来看,有利于程序流程的追踪。 划重点:seh机制,和tls函数很像,都改变了程序原来的执行流程,然后再返回执行。
4.4 EXCEPTION_POINTERS
一个异常发生时,操作系统向引起异常的线程堆栈中压入的结构,包含两个指针。
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord DWORD; //指向ExceptionRecord
PCONTEXT ContextRecord DWORD; //指向Context结构
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
其中ExceptionRecord包含有关最近发生异常的详细信息
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags; //异常标志
struct _EXCEPTION_RECORD *ExceptionRecord; //指向上一个异常记录的指针
PVOID ExceptionAddress; //异常发生的地址
DWORD NumberParameters; //异常信息的参数的个数
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
其中Context是线程在运行时处理器各主要寄存器的完整镜像,用于保存线程运行时的环境。
typedef struct _CONTEXT
{
DWORD ContextFlags; //说明该结构中哪些域有效,需要时恢复更新
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave; //浮点寄存器区
DWORD SegGs; //各种段寄存器
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi; //各种寄存器
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT;
程序可以通过修改context的成员,达到反调试的目的。 一个小的demo,具体功能见注释。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
DWORD scratch;
EXCEPTION_DISPOSITION
__cdecl
_except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
unsigned i;
// 指明是我们让流程转到我们的异常处理程序的
printf( "Hello from an exception handler\n" );
// 改变CONTEXT结构中EAX的值,以便它指向可以成功进写操作的位置
ContextRecord->Eax = (DWORD)&scratch;
// 告诉操作系统重新执行出错的指令
return ExceptionContinueExecution;
}
int main()
{
DWORD handler = (DWORD)_except_handler;
__asm
{
// 创建EXCEPTION_REGISTRATION结构:
push handler // handler函数的地址
push FS:[0] // 前一个handler函数的地址
mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构
}
__asm
{
mov eax,0 // 将EAX清零
mov [eax], 1 // 写EAX指向的内存从而故意引发一个错误
}
printf( "After writing!\n" );
__asm
{
// 移去我们的EXECEPTION_REGISTRATION结构
mov eax,[ESP] // 获取前一个结构
mov FS:[0], EAX // 安装前一个结构
add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈
}
return 0;
}
运行结果:
5.异常回调函数
handlerproc{
_lpExceptionRecord, //指向一个ExceptionRecord结构
_lpSEH, //指向Exception_Registration结构的地址
_lpContext结构, //指向Context结构
_lpDispatcherContext
}
四种返回值及含义
1.ExceptionContinueExecution(0):回调函数处理了异常,可以从异常发生的指令处重新执行。
2.ExceptionContinueSearch(1):回调函数不能处理该异常,需要要SEH链中的其他回调函数处理。
3.ExceptionNestedException(2):回调函数在执行中又发生了新的异常,即发生了嵌套异常
4.ExceptionCollidedUnwind(3):发生了嵌套的展开操作
在回调函数中,指向context字段,可以通过修改context内容去执行程序,如:清除断点,改变流程。下面的一段asm程序达到了清除断点实现反追踪的目的
.code
_start:
; 在堆栈中构造一个 EXCEPTION_REGISTRATION 结构
Assume FS:NOTHING
push offset perThread_Handler
push fs:[0]
mov fs:[0],esp
; 会引发异常的指令
mov esi,0
mov eax,[esi]
WouldBeOmit:
invoke MessageBox,0,addr Text,addr Caption,MB_OK ;这一句将无法被执行
; 异常处理完毕后,从这里开始执行
ExecuteHere:
invoke MessageBox,0,addr TextSEH,addr Caption,MB_OK
; 恢复原来的 SEH 链
pop fs:[0]
add esp,4
invoke ExitProcess,NULL
; 将 EIP 指向ExecuteHere处的位置并恢复堆栈
perThread_Handler proc uses ebx pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
mov eax,pContext
Assume eax:ptr CONTEXT
lea ebx, ExecuteHere ; 异常后准备从ExecuteHere后开始执行
mov [eax].regEip,ebx
xor ebx,ebx
mov [eax].iDr0,ebx ; 对Drx调试寄存器清零,使断点失效(反跟踪)
mov [eax].iDr1,ebx
mov [eax].iDr2,ebx
mov [eax].iDr3,ebx
mov [eax].iDr7,341
mov eax,0 ; ExceptionContinueExecution,表示已经修复
ret
perThread_Handler endp
end _start