SEH 机制
Structedexception handling 结构化异常处理的简称。
这里介绍的异常处理并不是我们通常看到的__try__catch 语句,而是操作系统提供的一种异常机制。通过这种称为SHE 的机制,用户可以通过定义注册自己的异常处理函数以在异常出现的时候有机会执行自己的代码,决定程序流程。我们平时直观看到的异常处理机制都是编译器建立在windows SHE 基础之上的一种扩充和使用。在了解windows SHE 的基础之后我们再来分析编译器提供的SHE 的实现过程。
首先分析Win X86 SHE 的原理。
上面提到用户必须定义注册自己的异常处理函数。
异常处理函数的函数定义是什么?
函数定义
EXCEPTION_DISPOSITION __cdecl _except_handler(
_In_ struct _EXCEPTION_RECORD *_ExceptionRecord,
_In_ void * _EstablisherFrame,
_Inout_ struct_CONTEXT *_ContextRecord,
_Inout_ void *_DispatcherContext
);
我们看到,该函数类型为,三个结构体指针,返回值为一个我们暂时不知道的类型。下面我们根据需要介绍需要了解的结构体。
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct_EXCEPTION_RECORD *ExceptionRecord;
PVOIDExceptionAddress;
DWORD NumberParameters;
DWORDExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
ExceptionCode 异常编号,比如STATUS_ACCESS_VIOLATION。
ExceptionAddress 发生异常的地址。
第三个参数为指向CONTEXT结构体的指针。
typedef 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;
}CONTEXT;
表示特定线程的寄存器值。当用于SHE ,CONTEXT 结构表示异常发生时的寄存器值。
因此现在我们可以看到,当异常发生的时候,系统会保存大量程序的运行信息,比如异常发生的地址,发生异常时寄存器的状态,异常的类型,然后调用一个拥有上述签名的函数,来处理异常。
那么,系统通过什么样的结构来找到相应的异常处理函数呢?
答案是下述结构体:
_EXCEPTION_REGISTRATIONstruc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends
系统将异常处理函数组织成一个链表,这个链表的最后一个处理函数的地址为0xFFFFFFFF,第一个该结构的地址保存在FS:[0] 中(FS寄存器指向TEB 基地址,TEB 第一个结构为TIB,TIB 第一个结构为ExceptionList即EXCEPTION_REGISTRATION_RECORD指针,该结构与上述结构相同),即异常处理函数为线程相关的。等到登记完成以后,线程就可以抛出或处理异常。“系统为异常的出现以及异常的处理提供了一种管理方法,或者一组数据结构”用户自己来提供不同异常对应的解决方案。
登记操作:
push handler // Addressof handler function
push FS:[0] // Address of previous handler
mov FS:[0],ESP // Install new EXECEPTION_REGISTRATION
上述三行汇编就是登记操作的最简单形式。
到这里,我们已经知道如何去定义并登记自己的异常处理函数到线程的异常处理链表中。那么,我们来看这个函数的返回值类型
typedef enum _EXCEPTION_DISPOSITION{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;
ExceptionContinueExecution 程序从异常发生的地址继续执行
ExceptionContinueSearch 无法处理该异常,继续沿着异常链表搜索并调用其异常处理函数
剩下的两个枚举先不介绍。
我们知道,程序的调用都有对应的调用堆栈,那么当异常发生的时候,异常函数的调用堆栈是怎样的?是谁调用了我们的异常处理函数?
要搞明白这个问题,首先应该清楚异常的处理过程。
KiDispatchException的处理过程
通过上图我们可以看到,如果是内核态代码且其没有附加的调试器,设计的异常处理函数为:RtlDispatchException 函数。
如果是用户态的异常,且没有调试器附载,KiDispatchException先确认用户态栈有足够的空间容纳CONTEXT 和 EXCEPTION_RECORD 结构,将其复制到用户态栈中,将TrapFram 调整为在用户态执行所需的合适的值,然后将KeUerExceptionDispatcher 指针赋给EIP,该函数内部依然调用的是RtlDispatchException函数。RtlDispatchException(用户态)函数寻找异常处理器,如果返回TRUE,已经有异常处理器处理了该异常,KiUserExceptionDispatcher 调用ZwContinue 系统服务继续执行原来发生异常的代码
用户态异常处理器链表(从FS:[0]开始) 尾部为Kernel32.dll中的 UnhanldedExceptionFilter---通常所见到的一些程序崩溃信息。
RtlDispatchException
接下来介绍RtlDispatchException 函数,该函数虽然有用户态和内核态,但其实是同一份代码的两份拷贝而已。
BOOLEAN
RtlDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT ContextRecord
)
Wrk 中有对应的源码,有兴趣的可以查看。
该函数内部依次遍历上面描述的异常处理函数链表,然后通过调用RtlpExecuteHandlerForException来执行其异常处理函数,并根据其返回值决定接下来的动作。
另外,RtlpExecuteHandlerForException函数内部会构建一个异常,对应的异常处理函数与嵌套异常的判断有关,这里我们暂时不深入介绍,后面我们将看到这一点。
ExceptionContinueExecution,结束遍历,返回。如果记为‘EXCEPTION_NONCONTINUABLE’的异常,调用RtlRaiseException。
ExceptionContinueSearch,继续遍历下一个结点。
ExceptionNestedException,嵌套异常,我们暂时不介绍这种情况。
只有正确处理 ExceptionContinueExecution 才会返回 TRUE,其他情况都返回FALSE。
到这里,操作系统提供的SHE 机制已经介绍完毕,其功能仅仅是遍历异常链表,挨个调用注册的异常处理函数,如果其中有某个处理函数处理了该异常,从异常触发点继续执行,否则不管是整个链表中没有找到合适的处理函数,还是遍历过程出现异常,都会导致系统崩溃。那么,应用层的程序如果没有注册自己的异常处理函数,简单的访问错误岂不是会导致系统崩溃?答案是不会,因为我们在运行自己的应用程序代码之前,提前执行了系统的函数,其中包含了一个默认的异常处理函数,该异常处理函数即我们常见的崩溃报告窗口。
应用层默认的异常处理函数
应用层线程的起始地址为:RtlUserThreadStart:
我们查看其代码并观察其运行堆栈如下:
.text:4B2E0F89 ;__stdcall_RtlUserThreadStart(x, x)
.text:4B2E0F89__RtlUserThreadStart@8:
.text:4B2E0F89 mov edi, edi
.text:4B2E0F8B push ebp
.text:4B2E0F8C mov ebp, esp
.text:4B2E0F8E push ecx
.text:4B2E0F8F push ecx
.text:4B2E0F90 lea eax, [ebp-8]
.text:4B2E0F93 push eax
.text:4B2E0F94 call _RtlInitializeExceptionChain@4 ;RtlInitializeExceptionChain(x)
.text:4B2E0F99 mov edx, [ebp+0Ch]
.text:4B2E0F9C mov ecx, [ebp+8]
.text:4B2E0F9F call ___RtlUserThreadStart@8 ;__RtlUserThreadStart(x,x)
.text:4B2E0FAA ;__stdcall __RtlUserThreadStart(x, x)
.text:4B2E0FAA___RtlUserThreadStart@8 proc near ;CODE XREF: .text:4B2E0F9Fp
.text:4B2E0FAA
.text:4B2E0FAAExitStatus = dword ptr -2Ch
.text:4B2E0FAAvar_28 = dword ptr -28h
.text:4B2E0FAAvar_24 = dword ptr -24h
.text:4B2E0FAAvar_20 = dword ptr -20h
.text:4B2E0FAAms_exc = CPPEH_RECORD ptr -18h
.text:4B2E0FAA
.text:4B2E0FAA ;FUNCTION CHUNK AT .text:4B31E07C SIZE 00000071 BYTES
.text:4B2E0FAA
.text:4B2E0FAA push 1Ch
.text:4B2E0FAC push offset stru_4B378710
.text:4B2E0FB1 call __SEH_prolog4_GS
.text:4B2E0FB6 mov edi, ecx
.text:4B2E0FB8 and [ebp+ms_exc.registration.TryLevel], 0
.text:4B2E0FBC mov esi, _Kernel32ThreadInitThunkFunction
.text:4B2E0FC2 push edx
.text:4B2E0FC3 test esi, esi
.text:4B2E0FC5 jz loc_4B31E07C
.text:4B2E0FCB mov ecx, esi
.text:4B2E0FCD call ds:___guard_check_icall_fptr ;RtlpHpAppCompatDontChangePolicy()
.text:4B2E0FD3 mov edx, edi
.text:4B2E0FD5 xor ecx, ecx
.text:4B2E0FD7 call esi ; _Kernel32ThreadInitThunkFunction
.text:4B2E0FD9 jmp loc_4B31E0E0
__SEH_prolog4_GS
proc near .text:4B3020FC
.text:4B3020FC
.text:4B3020FCarg_4 = dword ptr 8
.text:4B3020FC
.text:4B3020FC push offset __except_handler4
.text:4B302101 push large dword ptr fs:0
.text:4B302108 mov eax, [esp+8+arg_4]
Eax = 1CH
.text:4B30210C mov [esp+8+arg_4], ebp
.text:4B302110 lea ebp, [esp+8+arg_4]
.text:4B302114 sub esp, eax
Eax = 1CH
.text:4B302116 push ebx
.text:4B302117 push esi
.text:4B302118 push edi
.text:4B302119 mov eax, ds:___security_cookie
.text:4B30211E xor [ebp-4], eax
.text:4B302121 xor eax, ebp
.text:4B302123 mov [ebp-1Ch], eax
.text:4B302126 push eax
Ntdll32!_security_cookie与 ebp 异或后值入栈验证frame 是否被破坏。而offset stru_4B378710 所处的位置为EXCEPTION_REGISTRATION_RECORD 结构的成员FilterFrame,security_cookie 与其异或后在这里放下。
.text:4B302127 mov [ebp-18h], esp
.text:4B30212A push dword ptr [ebp-8]
.text:4B30212D mov eax, [ebp-4]
Eax= offsetstru_4B378710 xor __security_cookie
.text:4B302130 mov dword ptr [ebp-4], 0FFFFFFFEh
.text:4B302137 mov [ebp-8], eax
.text:4B30213A lea eax, [ebp-10h]
.text:4B30213D mov large fs:0, eax
.text:4B302143 retn
函数返回后,返回到__RtlUserThreadStart函数处
然后函数调用_Kernel32ThreadInitThunkFunction该函数调用线程的启动函数。
.text:4B2E0FB6 mov edi, ecx
.text:4B2E0FB8 and [ebp+ms_exc.registration.TryLevel], 0
.text:4B2E0FBC mov esi, _Kernel32ThreadInitThunkFunction
.text:4B2E0FC2 push edx
.text:4B2E0FC3 test esi, esi
.text:4B2E0FC5 jz loc_4B31E07C
.text:4B2E0FCB mov ecx, esi
.text:4B2E0FCD call ds:___guard_check_icall_fptr ;RtlpHpAppCompatDontChangePolicy()
.text:4B2E0FD3 mov edx, edi
.text:4B2E0FD5 xor ecx, ecx
.text:4B2E0FD7 call esi ; _Kernel32ThreadInitThunkFunction
.text:4B2E0FD9 jmp loc_4B31E0E0
.text:6B8162A0 ;__fastcall BaseThreadInitThunk(x, x, x)
.text:6B8162A0 public @BaseThreadInitThunk@12
.text:6B8162A0@BaseThreadInitThunk@12 proc near ;DATA XREF: .rdata:6B881DECo
.text:6B8162A0 ;.rdata:off_6B890348o
.text:6B8162A0
.text:6B8162A0var_4 = dword ptr -4
.text:6B8162A0 arg_0 = dwordptr 8
.text:6B8162A0
.text:6B8162A0 mov edi, edi
.text:6B8162A2 push ebp
.text:6B8162A3 mov ebp, esp
.text:6B8162A5 push ecx
.text:6B8162A6 mov eax, ___security_cookie
.text:6B8162AB xor eax, ebp
.text:6B8162AD mov [ebp+var_4], eax
.text:6B8162B0 push esi
.text:6B8162B1 mov esi, edx
.text:6B8162B3 test ecx, ecx
.text:6B8162B5 jnz short loc_6B8162CB
.text:6B8162B7 push [ebp+arg_0]
.text:6B8162BA mov ecx, esi
.text:6B8162BC call ds:___guard_check_icall_fptr ; _guard_check_icall_nop(x)
.text:6B8162C2 call esi
我们看到,用户态每个线程的刚开始就被包含在一个异常里面,下一节我们来研究编译器提供的增强版本的异常处理机制。为了分析的简单,下一篇我们分析X86 Ring0 的SEH,其实和Ring3 做的工作差不多,Ring0 看着更清晰。