x86 SEH分析

1997年的一篇文章感觉依旧封神,用户态掺杂内核分析,当时未公开的一些资料,似乎现在微软已经公开

https://blog.csdn.net/chenlycly/article/details/52575260

2011年boxcounter佬的一篇文章从内核模式分析

https://github.com/boxcounter/boxcounter.github.io/raw/master/attachments/SEH%E5%88%86%E6%9E%90%E7%AC%94%E8%AE%B0%EF%BC%88x86%E7%AF%87%EF%BC%89_v1.0.2.zip

TODO: 两篇文章都没看完,有空再来二周目

操作系统原生

当一个线程出现错误时,操作系统传递参数并调用用户定义的一个回调函数

回调函数样子

EXCEPTION_DISPOSITION
__cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext);

参数1

typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;//异常发生的原因
DWORD ExceptionFlags;//包含零个或多个异常标志
struct _EXCEPTION_RECORD *ExceptionRecord;//方便在发生嵌套异常时提供附加信息
PVOID ExceptionAddress;//发生异常的地址
DWORD NumberParameters;//指示与异常相关的附加参数的数量,就是下面的这个
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];//异常相关的附加参数数组
} EXCEPTION_RECORD;

参数2

指向establisher帧结构的指针 TODO

参数3

CONTEXT结构体指针保存特定线程的寄存器上下文,用于SEH时,CONTEXT结构表示异常发生时寄存器的值

kd> dt _context
nt!_CONTEXT
+0x000 ContextFlags : Uint4B//用来控制哪些寄存器需要保存
+0x004 Dr0 : Uint4B
+0x008 Dr1 : Uint4B
+0x00c Dr2 : Uint4B
+0x010 Dr3 : Uint4B
+0x014 Dr6 : Uint4B
+0x018 Dr7 : Uint4B
+0x01c FloatSave : _FLOATING_SAVE_AREA
+0x08c SegGs : Uint4B
+0x090 SegFs : Uint4B
+0x094 SegEs : Uint4B
+0x098 SegDs : Uint4B
+0x09c Edi : Uint4B
+0x0a0 Esi : Uint4B
+0x0a4 Ebx : Uint4B
+0x0a8 Edx : Uint4B
+0x0ac Ecx : Uint4B
+0x0b0 Eax : Uint4B
+0x0b4 Ebp : Uint4B
+0x0b8 Eip : Uint4B
+0x0bc SegCs : Uint4B
+0x0c0 EFlags : Uint4B
+0x0c4 Esp : Uint4B
+0x0c8 SegSs : Uint4B
+0x0cc ExtendedRegisters : [512] UChar

参数4

TODO

返回值

最后都要返回一个值来告诉系统下一步做什么

typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,//表示:“我已修正了此异常的故障,请你从事发点重新执行,谢谢”
ExceptionContinueSearch,//表示:“我没有处理此异常,请你继续搜索其他的解决方案,抱歉”。
ExceptionNestedException,TODO
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;

如何找到回调函数

结构化异常处理是基于线程的,三环FS:0指向TEB(Thread Environment Block)

kd> dt _TEB
nt!_TEB
+0x000 NtTib : _NT_TIB
+0x01c EnvironmentPointer : Ptr32 Void
......

_TEB第一个成员NtTib(_NT_TIB)的第一个成员指向_EXCEPTION_REGISTRATION_RECORD结构的指针

typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
#if defined(_MSC_EXTENSIONS)
union {
PVOID FiberData;
DWORD Version;
};
#else
PVOID FiberData;
#endif
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;

typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;//回调函数
} EXCEPTION_REGISTRATION_RECORD;
#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_RECORD结构:
push handler // handler函数的地址
push FS : [0] // 前一个handler函数的地址
mov FS : [0] , ESP // 安装新的_EXCEPTION_REGISTRATION_RECORD结构
}
__asm
{
xor eax,eax // 将EAX清零
mov[eax], 1 // 写EAX指向的内存从而故意引发一个错误
}
printf("After writing! scrath %d\n",scratch);
__asm
{
// 移去我们的EXECEPTION_REGISTRATION_RECODE结构
mov eax, [ESP] // 获取前一个结构
mov FS : [0] , EAX // 安装前一个结构
add esp, 8 // 将我们的EXECEPTION_REGISTRATION_RECODRD弹出堆栈
}
return 0;
}

mov[eax], 1时系统会先把异常抛给调试器,直接f11步过即可,先在_except_handler处打上断点,不然断不下来

image-20231102161123868

展开(EH_UNWINDING)

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext) {
printf("Home Grown handler: Exception Code: %08X Exception Flags %X",
ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags);
// http://www.jbox.dk/sanos/source/include/win32.h.html
if (ExceptionRecord->ExceptionFlags & 1)
printf(" EH_NONCONTINUABLE");
if (ExceptionRecord->ExceptionFlags & 2)
printf(" EH_UNWINDING");
if (ExceptionRecord->ExceptionFlags & 4)
printf(" EH_EXIT_UNWIND");
if (ExceptionRecord->ExceptionFlags & 8) // 注意这个标志
printf(" EH_STACK_INVALID");
if (ExceptionRecord->ExceptionFlags & 0x10) // 注意这个标志
printf(" EH_NESTED_CALL");
printf("\n");

// 我们不想处理这个异常,让其它函数处理吧
return ExceptionContinueSearch;
}


void HomeGrownFrame(void) {
DWORD handler = (DWORD)_except_handler;
__asm
{
// 创建_EXCEPTION_REGISTRATION_RECORD结构:
push handler // handler函数的地址
push FS : [0] // 前一个handler函数的地址
mov FS : [0] , ESP // 安装新的_EXCEPTION_REGISTRATION_RECORD结构
}

*(PDWORD)0 = 0; // 写入地址0,从而引发一个错误
printf("I should never get here!\n");

__asm
{
// 移去我们的_EXCEPTION_REGISTRATION_RECORD结构
mov eax, [ESP] // 获取前一个结构
mov FS : [0] , EAX // 安装前一个结构
add esp, 8 // 把我们_EXCEPTION_REGISTRATION_RECORD结构弹出堆栈
}
}


int main() {
__try {
HomeGrownFrame();
}
__except (EXCEPTION_EXECUTE_HANDLER) {
printf("Caught the exception in main()\n");
}

return 0;
}

image-20231102221245204

HomeGrownFrame过滤函数返回ExceptionContinueSearch不处理异常,沿着_EXCEPTION_REGISTRATION_RECORD.next找到main的处理函数,接管异常并处理,因此出错指令后面的 printf就永远不会执行

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

当一个函数的异常处理程序拒绝处理某个异常时,通常触发异常的函数执行流程并不会正常地从那个函数退出。第二次调用EH_UNWINDING操作系统给这个函数一个最后清理的机会,比如调用析构函数和__finally块之类的清理工作

展开操作会把堆栈上创建的_EXCEPTION_REGISTRATION_RECORD结构体清除

vs编译器层面的SEH


__try {
// 这里是被保护的代码
}
__except (过滤器表达式) { //异常过滤
// 异常处理程序
}

过滤表达式返回值

// Defined values for the exception filter expression
#define EXCEPTION_EXECUTE_HANDLER 1 //执行except里面的代码
#define EXCEPTION_CONTINUE_SEARCH 0 //寻找下一个异常处理函数
#define EXCEPTION_CONTINUE_EXECUTION (-1) //返回出错位置重新执行
typedef struct _EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION;

struct _EXCEPTION_REGISTRATION{//编译器加强版 _EXCEPTION_REGISTRATION_RECORD
PEXCEPTION_POINTERS xpointers;
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;//指向一个scopetable_entry结构数组
int trylevel;//scopetable_entry结构数组的索引
int _ebp;//_EXCEPTION_REGISTRATION结构创建之前栈帧指针(EBP)的值
};
struct scopetable_entry
{
DWORD previousTryLevel; //用于嵌套的__try块,每个__try块都有一个相应的SCOPETABLE结构,用于保存上一个try在scopetable的编号
PDWRD lpfnFilter; //过滤器表达式的起始地址
PDWRD lpfnHandler; //异常处理程序的地址
}

不管有多少try/except块,多少层嵌套,在使用SEH的一个函数中只创建一个EXCEPTION_REGISTRATION结构

每个try会对EXCEPTION_REGISTRATION.trylevel赋予不同的值,来scopetable里找到属于自己的异常处理流程,嵌套try通过scopetable_entry.previousTryLevel找到异常处理流程

测试程序

链接:https://pan.baidu.com/s/1_VfVoWSEE1EqOZ1KIaEm3A
提取码:tjxw

#include <Windows.h>
#include <stdio.h>
int flag = 0;
int filter(PEXCEPTION_POINTERS exceptionInfo) {
puts("second inner filter");
exceptionInfo->ContextRecord->Eax = &flag;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main() {
__try {
*(PDWORD)0 = 0;
}
__except (puts("first filter"), EXCEPTION_EXECUTE_HANDLER) {
puts("first hadle");
}

__try {
__try {
__asm {
xor eax, eax
mov[eax], 1
}
printf("%d\n", flag);
}
__except (filter(GetExceptionInformation())) {
puts("second inner handle");
}
}
__except (puts("secode filter"), EXCEPTION_EXECUTE_HANDLER) {
puts("second handle");
}
puts("main end");
}

image-20231103221002024

.text:00401070 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401070 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401070
.text:00401070 ms_exc= CPPEH_RECORD ptr -18h
.text:00401070 argc= dword ptr 8
.text:00401070 argv= dword ptr 0Ch
.text:00401070 envp= dword ptr 10h
.text:00401070
.text:00401070 ; __unwind { // __except_handler3
.text:00401070 push ebp
.text:00401071 mov ebp, esp
.text:00401073 push 0FFFFFFFFh ; _EXCEPTION_REGISTRATION.trylevel
.text:00401075 push offset stru_402568 ; _EXCEPTION_REGISTRATION.scopetable
.text:0040107A push offset __except_handler3 ; _EXCEPTION_REGISTRATION.handler
.text:0040107F mov eax, large fs:0 ; prev
.text:00401085 push eax ; _EXCEPTION_REGISTRATION.prev
.text:00401086 mov large fs:0, esp ; 安装seh
.text:0040108D sub esp, 8
.text:00401090 push ebx
.text:00401091 push esi
.text:00401092 push edi
.text:00401093 mov [ebp+ms_exc.old_esp], esp ; 保存当前函数栈帧ESP
.text:00401093
.text:00401096 ; __try { // __except at $LN8
.text:00401096 mov [ebp+ms_exc.registration.TryLevel], 0 ; 当前try块的scopetable数组下标trylevel为0
.text:0040109D mov large dword ptr ds:0, 0
.text:0040109D ; } // starts at 401096
.text:004010A7 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh ; 当前try块结束
.text:004010AE jmp short loc_4010DC ;
.text:004010AE ; 当前try块的scopetable数组下标trylevel为1
.text:004010AE
.text:004010B0 ; ---------------------------------------------------------------------------
.text:004010B0
.text:004010B0 $LN7: ; DATA XREF: .rdata:stru_402568↓o
.text:004010B0 ; __except filter // owned by 401096
.text:004010B0 push offset aFirstFilter ; "first filter"
.text:004010B5 call ds:__imp__puts
.text:004010B5
.text:004010BB add esp, 4
.text:004010BE mov eax, 1
.text:004010C3
.text:004010C3 $LN9:
.text:004010C3 retn
.text:004010C3
.text:004010C4 ; ---------------------------------------------------------------------------
.text:004010C4
.text:004010C4 $LN8: ; DATA XREF: .rdata:stru_402568↓o
.text:004010C4 ; __except($LN7) // owned by 401096 ;
.text:004010C4 mov esp, [ebp+ms_exc.old_esp] ; 执行expect块里的异常处理程序时,先恢复异常发送时的栈帧
.text:004010C7 push offset aFirstHadle ; "first hadle"
.text:004010CC call ds:__imp__puts
.text:004010CC
.text:004010D2 add esp, 4
.text:004010D5 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:004010D5
.text:004010DC
.text:004010DC loc_4010DC: ; CODE XREF: _main+3E↑j
.text:004010DC ; __try { // __except at $LN12 ;
.text:004010DC mov [ebp+ms_exc.registration.TryLevel], 1 ; 当前try块的scopetable数组下标trylevel为1
.text:004010DC ; } // starts at 4010DC
.text:004010DC
.text:004010E3 ; __try { // __except at $LN16
.text:004010E3 ; __try { // __except at $LN12
.text:004010E3 mov [ebp+ms_exc.registration.TryLevel], 2 ; 当前try块的scopetable数组下标trylevel为2
.text:004010EA xor eax, eax
.text:004010EC mov byte ptr [eax], 1
.text:004010EF mov eax, _flag
.text:004010F4 push eax
.text:004010F5 push offset _Format ; "%d\n"
.text:004010FA call _printf
.text:004010FA
.text:004010FF add esp, 8
.text:004010FF ; } // starts at 4010E3
.text:004010FF ; } // starts at 4010E3
.text:004010FF
.text:00401102 ; __try { // __except at $LN12
.text:00401102 mov [ebp+ms_exc.registration.TryLevel], 1 ; 内部嵌套的try块结束后,立即将scopetable数组下标trylevel设置为外部try块的
.text:00401109 jmp short loc_401130
.text:00401109
.text:0040110B ; ---------------------------------------------------------------------------
.text:0040110B
.text:0040110B $LN15: ; DATA XREF: .rdata:stru_402568↓o
.text:0040110B ; __except filter // owned by 4010E3
.text:0040110B mov ecx, [ebp+ms_exc.exc_ptr]
.text:0040110E push ecx ; exceptionInfo
.text:0040110F call _filter
.text:0040110F
.text:00401114 add esp, 4
.text:00401117
.text:00401117 $LN17:
.text:00401117 retn
.text:00401117
.text:00401118 ; ---------------------------------------------------------------------------
.text:00401118
.text:00401118 $LN16: ; DATA XREF: .rdata:stru_402568↓o
.text:00401118 ; __except($LN15) // owned by 4010E3
.text:00401118 mov esp, [ebp+ms_exc.old_esp]
.text:0040111B push offset aSecondInnerHan ; "second inner handle"
.text:00401120 call ds:__imp__puts
.text:00401120
.text:00401126 add esp, 4
.text:00401126 ; } // starts at 401102
.text:00401126
.text:00401129 ; __try { // __except at $LN12
.text:00401129 mov [ebp+ms_exc.registration.TryLevel], 1
.text:00401129 ; } // starts at 401129
.text:00401129
.text:00401130
.text:00401130 loc_401130: ; CODE XREF: _main+99↑j
.text:00401130 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:00401137 jmp short loc_401165
.text:00401137
.text:00401139 ; ---------------------------------------------------------------------------
.text:00401139
.text:00401139 $LN11: ; DATA XREF: .rdata:stru_402568↓o
.text:00401139 ; __except filter // owned by 4010DC
.text:00401139 ; __except filter // owned by 4010E3
.text:00401139 ; __except filter // owned by 401102
.text:00401139 ; __except filter // owned by 401129
.text:00401139 push offset aSecodeFilter ; "secode filter"
.text:0040113E call ds:__imp__puts
.text:0040113E
.text:00401144 add esp, 4
.text:00401147 mov eax, 1
.text:0040114C
.text:0040114C $LN13:
.text:0040114C retn
.text:0040114C
.text:0040114D ; ---------------------------------------------------------------------------
.text:0040114D
.text:0040114D $LN12: ; DATA XREF: .rdata:stru_402568↓o
.text:0040114D ; __except($LN11) // owned by 4010DC
.text:0040114D ; __except($LN11) // owned by 4010E3
.text:0040114D ; __except($LN11) // owned by 401102
.text:0040114D ; __except($LN11) // owned by 401129
.text:0040114D mov esp, [ebp+ms_exc.old_esp]
.text:00401150 push offset aSecondHandle ; "second handle"
.text:00401155 call ds:__imp__puts
.text:00401155
.text:0040115B add esp, 4
.text:0040115E mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
.text:0040115E
.text:00401165
.text:00401165 loc_401165: ; CODE XREF: _main+C7↑j
.text:00401165 push offset aMainEnd ; "main end"
.text:0040116A call ds:__imp__puts
.text:0040116A
.text:00401170 add esp, 4
.text:00401173 jmp short loc_401177
.text:00401173
.text:00401175 ; ---------------------------------------------------------------------------
.text:00401175 jmp short loc_401179
.text:00401175
.text:00401177 ; ---------------------------------------------------------------------------
.text:00401177
.text:00401177 loc_401177: ; CODE XREF: _main+103↑j
.text:00401177 xor eax, eax
.text:00401177
.text:00401179
.text:00401179 loc_401179: ; CODE XREF: _main+105↑j
.text:00401179 mov ecx, [ebp+ms_exc.registration.Next]
.text:0040117C mov large fs:0, ecx ;卸载seh
.text:00401183 pop edi
.text:00401184 pop esi
.text:00401185 pop ebx
.text:00401186 mov esp, ebp
.text:00401188 pop ebp
.text:00401189 retn
.text:00401189 ; } // starts at 401070
.text:00401189
.text:00401189 _main endp

main函数里栈帧如下

$-24     saved_edi <--esp
$-20 saved_esi ^
$-1C saved_ebx |
$-18 当前函数栈帧ESP--->
$-14 PEXCEPTION_POINTERS 是GetExceptionInformation等函数返回位置,异常发送时才会赋值
$-10 _EXCEPTION_REGISTRATION.prev <--fs:0 指向前一个EXCEPTION_REGISTRATION结构
$-C __except_handler3
$-8 _EXCEPTION_REGISTRATION.scopetable
$-4 _EXCEPTION_REGISTRATION.trylevel 初始0xfffffffff
ebp==> prev_ebp
main_ret

scopetable_entry结构体数组情况

image-20231103225048130

__except_handler3的细节暂时不分析了

image-20231103122031592

从程序加载的dll找到

image-20231103122110609