异常处理模式

当一个线程出现错误时,操作系统调用一个回调函数负责处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
__cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,		// 提供指向具有标准 Win32/64 定义的异常记录的指针
void * EstablisherFrame, // 此函数的固定堆栈分配的基址
struct _CONTEXT *ContextRecord, // 指向引发异常时的异常上下文(在异常处理程序情况下)
// 或当前“展开”上下文(在终止处理程序情况下)
void * DispatcherContext); // 指向此函数的调度程序上下文

// _EXCEPTION_RECORD 结构
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
// DispatcherContext
typedef struct _DISPATCHER_CONTEXT {
ULONG64 ControlPc;
ULONG64 ImageBase;
PRUNTIME_FUNCTION FunctionEntry;
ULONG64 EstablisherFrame;
ULONG64 TargetIp;
PCONTEXT ContextRecord;
PEXCEPTION_ROUTINE LanguageHandler;
PVOID HandlerData;
} DISPATCHER_CONTEXT, *PDISPATCHER_CONTEXT;
// 来自 https://docs.microsoft.com/zh-cn/cpp/build/exception-handling-x64?view=msvc-160&viewFallbackFrom=vs-2017

返回值:

ExceptionContinueExcetion(等于0):回调函数返回后,系统将线程环境设置为_lpContext参数指定的CONTEXT结构并继续执行;
ExceptionContinueSearch(等于1):回调函数拒绝处理这个异常,系统将通过EXCEPIONT_REGISTRATION结构的prev字段得到前一个回调函数的地址并调用它;
ExceptionNestedException(等于2):回调函数在执行中又发生了新的异常,即嵌套异常;
ExceptionCollidedUnwind(等于3):发生了嵌套的展开操作。

SEH链状结构

SEH以链的形式存在,若异常处理器未处理该异常,则将异常传递给下一个异常处理器,直至异常得到处理

一般来说操作系统会设置一个默认的异常处理程序在链的最后一项,它异于其他定义的异常处理函数并总是选择处理异常,以保证异常一定被处理

SEH链的项通常是

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD{
PEXCEPTION_REGISTRATION_RECORD Next; // 指向下一个结构体的指针
PEXCEPTION_DISPOSITION Handler; // 异常处理函数的地址
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

展开Unwinding

当异常发生时,系统遍历EXCEPTION_REGISTRATION结构链表,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次遍历这个链表,直到处理这个异常的结点为止。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义为EH_UNWINDING

二次调用的过程保证了未处理异常的异常处理函数执行一些清理工作,完成”展开“的动作,在异常处理块中产生的堆栈数据会被清除,然后流程从处理异常的那个回调函数决定的地方开始继续执行。

利用SEH链反调试

原理:

注册SEH后, 正常情况下发生异常会转入SEH处理流程, 但是如果这时处于被调试状态则异常事件会先发给调试器. 基于这个原理就能探测到进程是否是

被调试运行

若没有注册SEH,操作系统默认调用UnhandledExceptionFilter()

因此反调试可以通过故意触发异常,在特定异常处理器中修改上下文的EIP,异常处理结束后将到被设置的EIP处执行

若处于被调试状态,程序会在设定的代码中死循环或者直接退出程序。

参考资料

x64 异常处理 | Microsoft Docs

深入解析结构化异常处理(SEH) - by Matt Pietrek_chenlycly的专栏-CSDN博客

Windows调试艺术——断点和反调试(上) - 安全客,安全资讯平台 (anquanke.com)