这篇文章写于14年10月,彼时正对Windows的SEH机制感兴趣,且国庆假期时间比较充裕,便记了几篇相关的笔记。后来逐渐忙于课业便没继续分析下去,3年来多次想完成这个系列,但每次刚一开头总会被各种琐事打断,一直未能如愿,希望这一次能如愿以偿地完成分析吧。
2017.12.04补充:
当时分析这个函数的动机有以下几个方面:
Matt Pietrek在他的大作《A Crash Course on the Depths of Win32 Structured Exception Handling》中提到了退出展开,其中有这么一段话:
When an exception occurs, the system walks the list of EXCEPTION_REGISTRATION structures until it finds a handler for the exception. Once a handler is found, the system walks the list again, up to the node that will handle the exception. During this second traversal, the system calls each handler function a second time. The key distinction is that in the second call, the value 2 is set in the exception flags. This value corresponds to EH_UNWINDING.
我认为这段话是不准确的,事实上这样的操作被称为局部展开,是用户代码自愿进行的(言下之意就是这部分操作不是由Windows的用户层结构化异常处理机制来完成的)。在Matt Pietrek上面列举的例子中,由于在最外层使用了C/C++结构化异常处理中的异常处理块,因此导致内部原生异常处理程序不处理异常、执行流程转到外部的try-except块后,在其中(准确的说是except_handler4函数)执行了局部展开操作。因此,“局部展开会由系统完成”是一个很普遍的误会,网上很多文章都有类似的问题。就连《Windows核心编程》中的最后一部分也直接取名叫“结构化异常处理”,而事实上,其讲解的仅仅只是Visual Studio中C/C++的结构化异常处理机制而已,而局部展开行为尽管合理,但也需要用户手动进行,在系统的用户层结构化处理中是不会进行局部展开的。
其实局部展开的秘密,就隐藏在RtlUnwind函数的代码中。长时间以来,这个函数都处于undocument状态,虽然现在(2017年12月)可以在MSDN上查到了,但也是寥寥数语,不如自己分析来的痛快。
在漏洞利用中,SEH一直是一个热点话题。在经历了几轮魔道斗争后,SEH机制越来越成熟。而关于SEH安全机制的文章,网上一抓一大把,大多拿出的是伪代码和讲解,挖出代码进行分析的很少很少。本着“大海捞针,总不死心”和“眼见为实”的精神,咱们自己来分析一下。)
Visual C++中的结构化异常处理是构建在Windows的用户层异常处理机制之上的。因此有必要先了解后者,以便更好地分析前者。
逆向成果
RtlUnwind函数是一个用户层的Windows API函数,其作用是执行SEH的展开操作(正是由于Windows的SEH机制不自动进行展开操作,才提供了此API,供需要执行该操作的用户调用)。下面介绍其参数:
void WINAPI RtlUnwind(
_In_opt_ PVOID TargetFrame,
_In_opt_ PVOID TargetIp,
_In_opt_ PEXCEPTION_RECORD ExceptionRecord,
_In_ PVOID ReturnValue
);
TargetFrame :
- 当进行局部展开时,传入目标EXCEPTION_REGISTRATION_RECORD结构地址,当完成展开操作后,FS:[0]将指向这里。而执行该函数前的fs:[0]指向的节点到目标节点之间的前闭后开的所有合法节点中的处理函数都会被调用,被调用时传入的ExcepttionRecord.ExceptionFlags将包含EXCEPTION_UNWINDING操作。
- 当进行全局展开时,传入NULL即可。此时SEH链上所有节点的SEH处理函数都会被调用,调用时传入的ExcepttionRecord.ExceptionFlags将包含EXCEPTION_EXIT_UNWIND标志。
- 其余情况下需要谨慎传入,特别是不能传入比FS:[0]更低的地址,否则将引发异常。
TargetIp:虽然很多地方说通过这个参数可以设定函数执行完成后要执行的指令地址,但经过分析,至少在Win8.1和XP下,这个参数并没有什么用。
ExceptionRecord:在展开过程中,传递给被调用的异常处理函数的EXCEPTION_RECORD结构。如果传入NULL,将由本函数自己构造一个,其构造的那个ExceptionCode为STATUS_UNWIND,ExceptionAddress为调用本函数时压入的返回地址。
- ReturnValue:作为传给被调用的异常处理函数的CONTEXT结构中的eax值与恢复线程上下文环境时的eax值。
先贴上逆向的结果——C语言版本的RtlUnwind的代码(在IDA里按了F5然后对着3年前自己的逆向成果得来的):
void __stdcall RtlUnwind(PVOID TargetFrame, PVOID TargetIp, PEXCEPTION_RECORD ExceptionRecord, PVOID ReturnValue)
{
PEXCEPTION_RECORD finalExcepttionRecord; // esi
unsigned int dwCurrentSEHNode; // ebx
unsigned int pfnHandler; // ecx
void *pExceptionRecord; // ecx
int dwRet; // eax
int *dwPrevSEHNode; // ST10_4
unsigned int StackLimit; // [esp+Ch] [ebp-384h]
unsigned int StackBase; // [esp+10h] [ebp-380h]
unsigned int DispatcherContext; // [esp+14h] [ebp-37Ch]
EXCEPTION_RECORD tempExceptionRecord; // [esp+18h] [ebp-378h]
EXCEPTION_RECORD temp2ExceptionRecord; // [esp+68h] [ebp-328h]
CONTEXT stContext; // [esp+B8h] [ebp-2D8h]
int savedregs; // [esp+390h] [ebp+0h]
void *retaddr; // [esp+394h] [ebp+4h]
finalExcepttionRecord = ExceptionRecord;
RtlpGetStackLimits((void ***)&StackLimit, (void ***)&StackBase);// 获取线程的栈底和栈顶
if ( !ExceptionRecord ) // 如果没有传入ExceptionRecord,则构造一个,作为出现异常时抛出的那个
// 否则就用传入的那个
{
finalExcepttionRecord = &temp2ExceptionRecord;
temp2ExceptionRecord.ExceptionCode = 0xC0000027;// 0xC0000027 = STATUS_UNWIND
temp2ExceptionRecord.ExceptionFlags = 0;
temp2ExceptionRecord.ExceptionRecord = 0;
temp2ExceptionRecord.ExceptionAddress = retaddr;// 这里取出的是调用本函数之前call压入的返回地址
temp2ExceptionRecord.NumberParameters = 0;
}
if ( TargetFrame )
finalExcepttionRecord->ExceptionFlags |= 2u;// #define EXCEPTION_UNWINDING 0x2
// 如果传入的TargetFrame非空,则进行局部展开
else
finalExcepttionRecord->ExceptionFlags |= 6u;// #define EXCEPTION_EXIT_UNWIND 0x4
// 如果传入的TargetFrame为NULL,则进行退出展开
RtlpCaptureContext(&savedregs, (int)&stContext);// 填写stContext结构,通用寄存器组清0,
// EIP设置为call本函数时压入的返回地址
// EBP设置为调用本函数时的EBP值(即前一帧的EBP)
// ESP设置为指向TargetFrame的值(因为从本函数ret后这里就应该是ESP该指向的位置)
stContext.Esp += 16; // 上下文结构中的ESP结构加上16,平衡了压入的4个参数
stContext.Eax = (unsigned int)ReturnValue; // 将传入的ReturnValue设置给eax作为返回值
dwCurrentSEHNode = RtlpGetRegistrationHead(); // 从fs:0处获得异常链的首节点
while ( dwCurrentSEHNode != 0xFFFFFFFF ) // 当前线程注册了自己的SEH处理函数,而非kernel32默认的那个
{
if ( (PVOID)dwCurrentSEHNode == TargetFrame )// 如果已到达指定节点,则按照之前设定的Context恢复线程的环境并运行
{
ZwContinue(&stContext, 0);
}
else if ( TargetFrame && (unsigned int)TargetFrame < dwCurrentSEHNode )// 传入的TargetFrame不为NULL且SEH链首节点高于TargetFrame,抛异常
// (因为合法的SEH节点肯定在栈中,则从SEH首节点开始,应该依次降低)
// 而这里反到首节点在目标节点下方,肯定传入的参数有问题
{
tempExceptionRecord.NumberParameters = 0;
tempExceptionRecord.ExceptionCode = 0xC0000029;
tempExceptionRecord.ExceptionFlags = 1;
tempExceptionRecord.ExceptionRecord = finalExcepttionRecord;
RtlRaiseException(&tempExceptionRecord);
}
if ( dwCurrentSEHNode < StackLimit // 异常情况:SEH首节点比栈顶极限位置还高(这里的高指的是地址更低)
|| dwCurrentSEHNode + 8 > StackBase // 异常情况:SEH首节点的最末尾成员比栈底还要低(这里的低指的是地址更高)
|| dwCurrentSEHNode & 3 // 异常情况:SEH首节点地址不能被4整除(没对齐)
|| (pfnHandler = *(_DWORD *)(dwCurrentSEHNode + 4), pfnHandler < StackBase) && pfnHandler >= StackLimit// 异常情况:异常处理节点的函数地址落在栈空间中(说明多半是被溢出了)
|| !RtlIsValidHandler(pfnHandler, 0, (int)&stContext) )// 异常情况:被SafeSEH机制给判定为非法处理函数地址
{ // 以上异常情况,抛出异常,拒绝执行处理函数
tempExceptionRecord.NumberParameters = 0;
tempExceptionRecord.ExceptionCode = 0xC0000028;
tempExceptionRecord.ExceptionFlags = 1;
tempExceptionRecord.ExceptionRecord = finalExcepttionRecord;
RtlRaiseException(&tempExceptionRecord);
}
else
{
dwRet = RtlpExecuteHandlerForUnwind( // 调用目标SEH节点的处理函数,执行退出前的清理动作
pExceptionRecord,
(int)finalExcepttionRecord,
dwCurrentSEHNode,
(int)&stContext,
(int)&DispatcherContext,
*(_DWORD *)(dwCurrentSEHNode + 4))
- 1; // 这里很顽皮,居然在原SEH函数的结果基础上减了1
// ExceptionContinueExecution = 0n0 //已处理异常,程序可继续执行
// ExceptionContinueSearch = 0n1 //函数不处理该异常,继续搜寻异常处理函数链
// ExceptionNestedException = 0n2 //在处理异常的过程中产生了新的异常
// ExceptionCollidedUnwind = 0n3 //嵌套展开
// 分别减1
if ( dwRet ) // 如果不为ExceptionContinueSearch,则表明异常处理函数没按套路出牌
{
if ( dwRet == 2 ) // 如果返回的是CollidedUnwind,则将DispatcherContext作为下一个SEH节点展开
{
dwCurrentSEHNode = DispatcherContext;
}
else // 否则如果返回ContinueExecution、NestedException都会抛异常
{
tempExceptionRecord.NumberParameters = 0;
tempExceptionRecord.ExceptionCode = 0xC0000026;
tempExceptionRecord.ExceptionFlags = 1;
tempExceptionRecord.ExceptionRecord = finalExcepttionRecord;
RtlRaiseException(&tempExceptionRecord);
}
}
dwPrevSEHNode = (int *)dwCurrentSEHNode;
dwCurrentSEHNode = *(_DWORD *)dwCurrentSEHNode;// 工作指针后移
RtlpUnlinkHandler(dwPrevSEHNode); // 将fs:0指向当前节点的下一个节点,即取消掉本节点
}
}
if ( TargetFrame == (PVOID)0xFFFFFFFF ) // 如果传入的TargetFrame为0xFFFFFFFF,也不可能把这个节点干掉了,表明也到头了,该返回了
ZwContinue(&stContext, 0);
else
ZwRaiseException(finalExcepttionRecord, &stContext, 0);// 如果传入的TargetFrame为其他值,则抛异常(如果匹配到了的话,早在while中就恢复上下文执行了)
}
以下是2014.10.08的笔记:
逆向过程
不知由于何种原因,在Win 8.1 X86平台下,试图用静态方式找出RtlUnwind函数的宿主还费了一番功夫。首先,在kernel32.dll的导出表中发现了该函数,可是立即又发现其仅是一个傀儡:
.text:6B81C83C ; void __stdcall RtlUnwindStub(PVOID TargetFrame, PVOID TargetIp, PEXCEPTION_RECORD ExceptionRecord, PVOID ReturnValue)
.text:6B81C83C public _RtlUnwindStub@16
.text:6B81C83C _RtlUnwindStub@16 proc near
.text:6B81C83C
.text:6B81C83C TargetFrame= dword ptr 4
.text:6B81C83C TargetIp= dword ptr 8
.text:6B81C83C ExceptionRecord= dword ptr 0Ch
.text:6B81C83C ReturnValue= dword ptr 10h
.text:6B81C83C
.text:6B81C83C 000 jmp ds:__imp__RtlUnwind@16 ; RtlUnwind(x,x,x,x)
.text:6B81C83C _RtlUnwindStub@16 endp
于是跟着JMP跳到了:
.idata:6B880A24 extrn _api_ms_win_core_registry_l1_1_0_NULL_THUNK_DATA:byte:4
.idata:6B880A28 ;
.idata:6B880A28 ; Imports from api-ms-win-core-rtlsupport-l1-2-0.dll
.idata:6B880A28 ;
.idata:6B880A28 ; SIZE_T __stdcall RtlCompareMemory(const void *Source1, const void *Source2, SIZE_T Length)
.idata:6B880A28 extrn __imp__RtlCompareMemory@12:dword
.idata:6B880A28 ; CODE XREF: FSPErrorMessages::CMessageTagCache::LookupNodeByMessageTag(ulong,FSPErrorMessages::MessageTag *)+29p
.idata:6B880A28 ; DATA XREF: FSPErrorMessages::CMessageTagCache::LookupNodeByMessageTag(ulong,FSPErrorMessages::MessageTag *)+29r ...
.idata:6B880A2C ; __declspec(dllimport) __stdcall RtlRaiseException(x)
.idata:6B880A2C extrn __imp__RtlRaiseException@4:dword
.idata:6B880A2C ; CODE XREF: PsspValidateWin32WalkMarker+9Ep
.idata:6B880A2C ; DATA XREF: PsspValidateWin32WalkMarker+9Er
.idata:6B880A30 ; WORD __stdcall RtlCaptureStackBackTrace(DWORD FramesToSkip, DWORD FramesToCapture, PVOID *BackTrace, PDWORD BackTraceHash)
.idata:6B880A30 extrn __imp__RtlCaptureStackBackTrace@16:dword
.idata:6B880A30 ; DATA XREF: RtlCaptureStackBackTraceStub(x,x,x,x)+6r
.idata