如何分析X64的SEH

robots

 

参考链接

https://www.pediy.com/kssd/pediy12/142371.html

https://www.bilibili.com/video/BV1tJ411M7kd

微软文档 查询 https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-160

感谢周壑老师和boxcounter。

 

环境

VS2019

idapro7.5

 

正文

在PE+的结构中,异常处理的信息存储在ExceptionDirectory中,且每个字段都是3*4=12字节。

typedef struct _RUNTIME_FUNCTION {
    ULONG BeginAddress;
    ULONG EndAddress;
    ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

了解SEH的习惯,x64 SEH 不基于栈,不发生异常和通常执行没有区别(效率高),每个非叶函数至少对应一个 RUNTIME FUCNTION结构体叶函数如果使用了SEH, 也会对应 RUNTIME FUCNTION结构体。

既不调用函数、又没有修改栈指针,也没有使用 SEH 的函数就叫做“叶函数”。

 

代码演示

使用代码

#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
int filter() {
    printf("filter\n");
    return 1;
}

void exc() {
    int x = 0;
    int y = x / x;
}

int main() {
    __try {
        __try {
            exc();
        }
        __finally {
            printf("111\n");
        }
    }
    __except (filter()) {
        printf("222\n");
    }
    system("pause");
    return 0;
}

通过除零异常,进行异常处理流程的学习。根据执行结果可看到,在exc()异常(EXCEPT_POINT )后,首先执行filter函数(EXCEPT_FILTER ),然后执行finally函数(FINALLY_HANDLER ),再执行except中的异常处理函数(EXCEPT_HANDLER )。 这是基础的执行流程。

然后在IDA中进行分析,找到main函数的引用,指向pdata段的一个RUNTIME_FUNCTION结构体,RUNTIME_FUNCTION里面存储的地址都是基于BaseAddress的32位RVA。依次是BeginAddress 就是函数开始地址,EndAddress也就是结束的地址,UnwindData是指向_UNWIND_INFO的地址。这个结构体表明了该异常处理的范围和异常处理回滚(unwind)所需要的信息。

_UNWIND_INFO是用来记录一个函数上堆栈指针的操作,以及非易失性寄存器保存在堆栈上的位置。(除了rcx,rdx,r8,r9,r10,r11为易失寄存器,其他都是非易失寄存器,使用前push进行保存,使用后pop进行恢复)。

typedef enum _UNWIND_OP_CODES {
    UWOP_PUSH_NONVOL = 0, /* info == register number */
    UWOP_ALLOC_LARGE,     /* no info, alloc size in next 2 slots */
    UWOP_ALLOC_SMALL,     /* info == size of allocation / 8 - 1 */
    UWOP_SET_FPREG,       /* no info, FP = RSP + UNWIND_INFO.FPRegOffset*16 */
    UWOP_SAVE_NONVOL,     /* info == register number, offset in next slot */
    UWOP_SAVE_NONVOL_FAR, /* info == register number, offset in next 2 slots */
    UWOP_SAVE_XMM128 = 8, /* info == XMM reg number, offset in next slot */
    UWOP_SAVE_XMM128_FAR, /* info == XMM reg number, offset in next 2 slots */
    UWOP_PUSH_MACHFRAME   /* info == 0: no error-code, 1: error-code */
} UNWIND_CODE_OPS;

typedef union _UNWIND_CODE {
    struct {
        UBYTE CodeOffset;
        UBYTE UnwindOp : 4;
        UBYTE OpInfo   : 4;
    };
    USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER  0x01
#define UNW_FLAG_UHANDLER  0x02
#define UNW_FLAG_CHAININFO 0x04

typedef struct _UNWIND_INFO {
    UBYTE Version       : 3;
    UBYTE Flags         : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister : 4;
    UBYTE FrameOffset   : 4;
    UNWIND_CODE UnwindCode[1];
/*  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
*   union {
*       OPTIONAL ULONG ExceptionHandler;
*       OPTIONAL ULONG FunctionEntry;
*   };
*   OPTIONAL ULONG ExceptionData[]; */
} UNWIND_INFO, *PUNWIND_INFO;

typedef struct _SCOPE_TABLE {
    ULONG Count;
    struct
    {
        ULONG BeginAddress;
        ULONG EndAddress;
        ULONG HandlerAddress;
        ULONG JumpTarget;
    } ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;

 

手把手解析_UNWIND_INFO结构体

结构体后面的冒号表示使用多少位,例如 Version+Flags一共使用8位,也就是1字节。

第0行

代表着结构数据有Version + Flags,SizeOfProlog,CountOfCodes,FrameRegister+FrameOffset。

Version + Flags

0x19h = 0y00010011

Version= 0y011 = 3

Flags = 0y00010 = 2

根据数值找到对应的flag。

UNW_FLAG_NHANDLER 0x0 不对异常进行处理

UNW_FLAG_EHANDLER 0x01 使用Except函数进行处理。

UNW_FLAG_UHANDLER 0x02 使用finally函数处理。

UNW_FLAG_CHAININFO 0x04 使用调用链。

SizeOfProlog

函数头的大小。比较产生异常的相对函数头的大小与该值,判断回滚操作。函数头大小为6字节。

如果大于该值,则两个UNWIND_CODE都执行。如果小于该值,则通过UNWIND_CODE的CodeOffset进一步判断。CodeOffset小于相对数值则会进行该UNWIND_CODE的回滚。

CountOfCodes

下面UWIND_CODE的数量。2个。

FrameRegister+FrameOffset

根据FP进行相关操作。

第一行

UNWIND_CODE

UWIND_CODE用于记录函数头中有关非易失性寄存器和RSP的操作。

解析第一个,<6,32h>.

在距离便宜头部6字节及以内的地方异常,都会执行该操作。

32h = 0y00100011

UnwindOp = 2 //UWOP_ALLOC_SMALL

OpInfo = 3

所以创建了3*8+8 = 0n32= 0x24 所以记录了创建栈空间0x24字节,回滚时则需要释放32字节空间。

然而IDA已经标注了OPCODE,所以能很方便的进行判断。

第二个则是记录了压入 RDI。

0x70 = 0y01110000

UnwindOp = 0y0000 = 0n0

OpInfo = 0y0111 = 0n7

第三行

_C_specific_handler_0 是一个导入函数,是进行异常处理分发的,可以不用分析。

第四行

第四行解析_SCOPE_TABLE结构体。

有2组ScopeRecord。

引用boxcounter:

  • Count 表示 ScopeRecord 数组的大小。
  • ScopeRecord 等同于 x86 中的 scopetable_entry 成员。其中,
  • BeginAddress 和 EndAddress 表示某个 __try 保护域的范围。
  • HandlerAddress 和 JumpTarget 表示 EXCEPTION_FILTER、EXCEPT_HANDLER 和 FINALLY_HANDLER。具体对应情况为:
    • 对于 try/except 组合,HandlerAddress 代表 EXCEPT_FILTER,JumpTarget 代表 EXCEPT_HANDLER。
    • 对于 try/finally 组合,HandlerAddress 代表 FINALLY_HANDLER,JumpTarget 等于 0。

    这四个域通常都是 RVA,但当 EXCEPT_FILTER 简单地返回或等于 EXCEPTION_EXECUTE_HANDLER 时,HandlerAddress 可能直接等于 EXCEPTION_EXECUTE_HANDLER,而不再是一个 RVA。

所以第一排指向Finally函数。

第二排指向filter和Except函数。

这时候看注释就明白很多。

当Flags为UNW_FLAG_CHAININFO

0x21 = 0y00100001

Flags = 0y00100=4,会看到末尾指向了一个_RUNTIME_FUNCTION,形成了链式结构,继续进行回滚判断。类似子函数对引用母函数的回滚。

 

进行异常回滚模拟

如果是在exc()函数中异常,首先查看自身函数的 RUNTIME_FUNCTION,找到UNWIND_INFO,进行回滚判断并操作。

 1. 恢复栈空间0x10字节 add rsp,0x10h
 2. pop rdi

然后根据栈查看调用者,再查看RUNTIME_FUNCTION找到UNWIND_INFO。这里也就是main的UNWIND_INFO。判断FLAGS为,00010为2,UNW_FLAG_UHANDLER 。

由于调用exc()函数实在六字节外,所以进行回滚。

 3. add rsp,0x20h
 4. pop rdi

然后交给_C_specific_handler_0,

判断异常相对于_SCOPE_TABLE字段的位置,都在内部。则两个都要执行。

由于第一个是JmpTarget是0,所以是finally,在此处进行记录。直到找到filter接管后,再执行。

然后第二个SCOPE_TABLE:

 5. 执行EXCEPT_FILTER
 6. FINALLY_HANDLER
 7. EXCEPT_HANDLER
 8. 顺序执行到system("pause")

总结

分析好后,就能更好地去理解IDA的注释了,读懂注释了。

 

异常处理函数反向查找引用函数

通过最后的引用的RVA,进行搜索,最终定位到C_SCOPE_TABLE,找到UWIND_INFO结构体,然后找到其引用RUNTIME_FUNCTION,定位到调用者函数。

以filter为例,定位到调用者,假设看不到其引用。我们进行搜索。

定位到疑似结构体。

然后就能找到引用该异常处理的函数部分。

万一有用?

文档中获取特定信息的宏。

#define GetUnwindCodeEntry(info, index) \
    ((info)->UnwindCode[index])

#define GetLanguageSpecificDataPtr(info) \
    ((PVOID)&GetUnwindCodeEntry((info),((info)->CountOfCodes + 1) & ~1))

#define GetExceptionHandler(base, info) \
    ((PEXCEPTION_HANDLER)((base) + *(PULONG)GetLanguageSpecificDataPtr(info)))

#define GetChainedFunctionEntry(base, info) \
    ((PRUNTIME_FUNCTION)((base) + *(PULONG)GetLanguageSpecificDataPtr(info)))

#define GetExceptionDataPtr(info) \
    ((PVOID)((PULONG)GetLanguageSpecificData(info) + 1)
(完)