整理自《逆向工程核心原理》相关章节,windows异常机制除了SEH外还有VEH,两者粒度不同,异常接管顺序不同,后续进一步整理VEH的
查看结构体可以用WinDbg
例如:`dt _PEB`、`dt _TEB` ## 简介 Windows的异常处理机制。在程序源代码中用`__try`、`__except`、`__finally`关键字实现。 与C++中的`try`、`catch`处异常处理具有不同的结构。SEH是一种从属于VC++开发工具和Windows操作系统的异常处理机制。应用场景
使用SEH进行反调试
基本概念
1. 异常处理过程
- 进程运行中如果发生异常,OS会委托进程处理。若进程代码中存在具体的异常处理(SEH异常处理器),则能够顺利处理异常,程序继续运行。否则OS就会启动默认的异常处理机制,终止程序运行。
- 在调试运行时,处理方法不同。被调试进程内部发生的一场首先会由OS抛给调试进程处理(被调试者的SEH依据优先顺序推给调试器)。调试器具有被调试者的所有权限(运行、终止、虚拟内存、寄存器的读写权限)。被调试者发生异常时,调试器会暂停运行,采取某种措施处理异常后继续调试。
常用的处理方法为:
- 直接修改异常:代码、寄存器、内存
- 将异常抛给被调试者处理
- OS默认的异常处理机制
2. 最常见的OS异常
EXCEPTION_DATATYPE_MISALIGNMENT (0X80000002)
EXCEPTION_BREAKPOINT (0X80000003)
EXCEPTION_SINGLE_STEP (0x80000004)
EXCEPTION_ACCESS_VIOLATION (0xC0000005)
EXCEPTION_IN_PAGE_ERROR (0xC0000006)
EXCEPTION_ILLEGAL_INSTRUCTION (0xC000001D)
EXCEPTION_NONCONTINUABLE_EXCEPTION (0xC0000025)
EXCEPTION_INVALID_DISPOSITION (0xC0000026)
EXCEPTION_ARRAY_BOUNDS_EXCEEDED (0xC000008C)
EXCEPTION_FLT_DENORMAL_OPERAND (0xC000008D)
EXCEPTION_FLT_DIVIDE_BYZERO (0xC000008E)
EXCEPTION_FLT_INEXACT_RESULT (0xC000008F)
EXCEPTION_FLT_INVALID_OPERATION (0xC0000090)
EXCEPTION_FLT_OVERFLOW (0xC0000091)
EXCEPTION_FLT_STACK_CHECK (0xC0000092)
EXCEPTION_FLT_UNDERFLOW (0xC0000093)
EXCEPTION_INT_DIVIDE_BY_ZERO (0xC0000094)
EXCEPTION_INT_OVERFLOW (0xC0000095)
EXCEPTION_PRIV_INSTRUCTION (0xC0000096)
EXCEPTION_STACK_OVERFLOW (0xC00000FD)
-
EXCEPTION_ACCESS_VIOLATION(C0000005)
:非法访问异常,试图访问不存在或不具访问权限的内存区域。 -
EXCEPTION_BREAKPOINT(80000003)
:运行的代码被设置了断点之后,CPU尝试执行该地址处的指令时,就会发生EXCEPTION_BREAKPOINT
异常。原理是,设置断点会将该处的指令修改为0xCC,但Ollydbg不会将临时断点的实际指令显示出来,可以使用PE Tools工具转储进程内存之后,使用Hex Editor打开文件,即可看到真是指令。如果注册表将默认调试器设置为Ollydbg,那么发生异常时,操作系统会自动运行Ollydbg,附加发生异常的进程
-
EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)
:CPU遇到无法解析的指令时引发该异常。 -
EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)
:除法运算中若分母为零就会发生除零异常。程序中可能出现的情况是分母为变量,变量在某个时刻变为0,再执行除法运算时就会出现异常。 -
EXCEPTION_SINGLE_STEP(80000004)
:将EFLAGS寄存器的第八位TF(Trap Flag陷阱标志)设置为1之后,CPU就会进入单步工作模式。每执行一条指令之后,就会暂停,抛出异常。
3. SEH链和相关的结构体
-
SEH以链的形式存在(由
_EXCEPTION_REGISTRATION_RECORD
结构体组成的链表),第一个异常处理器如果未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。typedef struct _EXCEPTION_REGISTRATION_RECORD { PEXCEPTION_REGISTRATION_RECORD Next; PEXCEPTION_DISPOSITION Handler; } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD; //链表以Next成员为FFFFFFFF的结构体结束,表示链表的最后一个结点 //异常顺着链传递,直到有异常处理器处理
-
异常处理函数的定义:
EXCEPTION_DISPOSITION _except_handler ( EXCEPTION_RECORD *pRecord, EXCEPTION_REGISTRATIOIN_RECORD *pFrame, CONTEXT *pContext, PVOID pValue );
函数接收四个参数,返回
EXCEPTION_DISPOSITION
枚举类型。异常处理函数是一个回调函数,系统调用它时会传入四个参数,参数保存着异常相关的信息。
第一个参数:typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常代码 DWORD ExceptionFlags; struct _EXCEPTION_RECORD * ExceptionRecord; PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_AMXIMUM_PARAMETERS];//15 } EXCEPTION_RECORD, *PEXCEPTION_RECORD
重要成员为
ExceptionCode
和ExceptionAddress
。第三个参数:
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; BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];//512 }
CONTEXT
结构体用来备份CPU寄存器的值,为的是多线程环境中在线程切换时,CPU寄存器的值就会被保存到CONTEXT
以便下次从之前暂停的状态继续执行。
重要成员为Eip成员,在异常处理器中可以修改为其他地址,暂停的线程就会执行新设置的Eip地址处的代码。返回值:
typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution = 0, //继续执行异常代码 ExceptionContinueSearch = 1, //运行下一个异常处理器 ExceptionNestedException = 2, //在OS内部使用 ExceptionCollidedUnwind = 3 //在OS内部使用 } EXCEPTION_DISPOSITION;
异常处理器处理异常后会返回
ExceptionContinueExecution(0)
,从发生异常的代码出继续运行。若无法处理异常,则返回ExceptionContinueSearch(1)
,将一场派送到SEH链的下一个异常处理器。 -
访问进程的SEH链的方法——通过TEB结构体的NtTib成员
TEB.NtTib.ExceptionList = FS:[0]
-
SEH的安装方法
在C语言中,使用
__try
、__except
、__finally
关键字可以向代码添加SEH。汇编
PUSH @MyHandler PUSH DWORD PTR FS:[0] MOV DWORD PTR FS:[0], ESP
原理是FS:[0]保存了SEH链第一个
_EXCEPTION_REGISTRATION_RECORD
结构体的地址(FS:[0]存储了TEB结构体,TEB
结构体第一个成员是NtTib
,NtTib
第一个成员是ExceptionList
,ExceptionList
是_EXCEPTION_REGISTRATION_RECORD
结构体组成的链表)。
先将自定义的异常处理器压入栈中,然后将FS:[0]压入栈中,再结合栈空间向低地址增长的特点,栈中保存的两个地址即可形成一个新的_EXCEPTION_REGISTRATION_RECORD
结构体,分别对应了Next
成员和Handler
成员。
将栈顶地址存回到FS:[0]之后,此时的SEH链第一个异常处理器即自定义的异常处理器,第二个异常处理器是原SEH链的第一个异常处理器(由VC++等编译器生成PE文件时默认添加到其启动函数的),最后一个_EXCEPTION_REGISTRATION_RECORD
结构体的Next成员为FFFFFFFF,Handler
成员处于内核空间,是OS的默认异常处理器(创建进程时,OS自动产生默认的SEH)。
利用参数,编写SEH回调函数
在回调函数设置断点之后,系统调用回调函数是,会将四个参数放在栈中。分别是
[ESP+4]
:指向EXCEPTION_RECORD,重点成员ExceptionCode
、ExceptionAddress
。
[ESP+8]
:指向EXCEPTION_REGISTRATION_RECORD,SEH链的起始地址。
[ESP+C]
:指向CONTEXT结构体,重点成员为偏移为B8处的Eip寄存器的值。
[ESP+10]
其他细节
- 当进程中发生异常时,若SEH未处理或注册的SEH不存在。此时会调用系统的
kernel32!UnhandledExceptionFIlter()
API。 - 该API会运行系统的最后一个异常处理器——Top Level Exception Filter或Last Exception Filter(通常行为是弹出错误消息框、终止进程)。
kernel32!UnhandledExceptionFilter()
调用了ntdll!QueryInformationProcess(ProcessDebugPort)
。来判断是否正在调试进程。如果正在进行调试,则将异常传递给调试器。否则系统异常处理器终止进程。- 通过
kernel32!SetUnhandledExceptionFilter()
可以修改系统最后的异常处理器。函数定义如下://返回的是上一个Top Level Exception Filter的地址,方便恢复 LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( __in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
- Top Level Exception Filter的函数定义:
typedef struct _EXCEPTION_POINTERS{ PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS; LONG TopLevelExceptionFilter( PEXCEPTION_POINTER pExcept; );