之前一直没有整理过的东西,现在补一下。
x64 SEH简介
先回顾一下x86的SEH构造:
- 在自身的栈空间中分配并初始化一个 EXCEPTION_REGISTRATION(_RECORD) 结构体。
- 将该 EXCEPTION_REGISTRATION(_RECORD) 挂入当前线程的异常链表。
可以看出x86的SEH都是动态构建的。
x64不再基于链式存储SEH,而是使用表式存储,信息直接存储在PE文件中的.pdata节中,具体位置可于DataDirectory的Exception Table(IMAGE_DIRECTORY_ENTRY_EXCEPTION)中找到 。
Exception Table由结构为RUNTIME_FUNCTION的数组组成。
1 | typedef struct _RUNTIME_FUNCTION { |
每个RUNTIME_FUNCTION就是一个FunctionEntry,记录了函数的信息,其中BeginAddress和EndAddress就是函数的地址范围,而UnwindData指向UNWIND_INFO。
1 | typedef enum _UNWIND_OP_CODES { |
该结构包含了函数的序幕操作和异常处理信息,序幕操作包括开辟栈空间、保存非易失寄存器等。CountOfCodes记录了UnwindCode结构的个数。UnwindCode之后的成员是可选的,取决于是否有异常处理过程或者是否是链式的。ExceptionHandler指向异常处理函数,在MSVC中通常是__C_specific_handler,ExceptionData则是ExceptionHandler指定使用的数据,MSVC中通常是SCOPE_TABLE结构。SCOPE_TABLE包含了函数中try的信息,具体含义在下面代码的注释当中。首先异常会先分发到__C_specific_handler,然后通过RIP和SCOPE_TABLE结构判断是哪个try。
实验代码测试
代码如下
1 |
|
编译时将增量链接关闭,确保取得真实函数地址,运行的结果如下
1 | This is a SEH test |
接下来通过ida验证一下
其中except里的filter指向了一个直接返回EXCEPTION_EXECUTE_HANDLER的函数。
无模块注入中SEH的处理办法
这里直接抄一下xjun大佬的代码
1 | static VOID |
函数分成了两个部分,一部分是处理64位程序,另一部分是处理32位程序,分析一下为什么这么写:
首先简单复习一下用户层的异常分发,异常处理从内核层返回到用户层之后首先是到KiUserExceptionDispatcher,然后x64环境下会先判断是否是Wow64,如果是的话就分发到32位的ntdll当中,处理过程和原生32位的一致。
之后KiUserExceptionDispatcher会调用RtlDispatchException。64位的RtlDispatchException会调用RtlLookupFunctionEntry,RtlLookupFunctionEntry不但会查找原本就存在于PE当中FunctionEntry,还会再调用RtlpLookupDynamicFunctionEntry来查找通过动态方式添加到RtlpDynamicFunctionTableTree中的FunctionEntry。微软文档在说明如何处理动态生成函数时提到了两个函数RtlInstallFunctionTableCallback和RtlAddFunctionTable,这两个函数就是把FunctionEntry动态添加到了RtlpDynamicFunctionTableTree当中。而32位的RtlDispatchException还是通过老规矩SEH链来调用异常处理函数,至于这里为啥还要插个InvertedFunctionTable,大概是因为32位的RtlDispatchException还会调用一个RtlIsValidHandler来检查Handler是否合法,wrk中RtlIsValidHandler的实现如下
1 | BOOLEAN |
可以发现如果找不到FunctionTable就直接返回TRUE了,所以32位似乎不插这个InvertedFunctionTable也是没关系的。
参考
《加密与解密》第四版