最初发布在QQ空间,见:SEH中的prolog和epilog,内有贴图。
使用SEH的代码都需要构建栈桢,支持exception处理的栈桢,而重复的代码就应当提炼成函数,微软自然不会例外。为此系统提供了prolog和epilog系列,类似的函数有不少版本,但大同小异。这次不用调试器,分析下__SEH_prolog4_GS和__SEH_epilog4_GS 2个函数,看看它们如何工作支持异常的栈桢结构。
先看看prolog,这是内核函数KiDispatchException调用__SEH_prolog4_GS的代码,
000 push 0F8h 004 push offset off_8488DCE0 008 call __SEH_prolog4_GS
调用函数前传递2个参数,一个是需要保留的栈的大小,一个是指针。
再看看函数代码本身,详细的注释在代码后面,
; two instrucitons before call it. ; ; take KiDispatchException as example, ; ; 000 push 0F8h ; it's used as a stack size ; 004 push offset off_8488DCE0 ; it's pointer to scope table entry. __SEH_prolog4_GS proc near arg_4= dword ptr 8 000 push offset __except_handler4 004 push large dword ptr fs:0 008 mov eax, [esp+8+arg_4] ; so now there are 5 values in stack. ; ; f8 ; 8488dce0 ; return address of call ; _except_handler4 ; old fs:0 ; ; follwing is additional information ; to be pointer to EXCEPTION_POINTERS ; to be esp value ; to be cookie value 008 mov [esp+8+arg_4], ebp ; push current ebp value into place ; where 0f8 is here. 008 lea ebp, [esp+8+arg_4] ; update ebp to current frame pointer 008 sub esp, eax ; reserve stack 008 push ebx 00C push esi 010 push edi 014 mov eax, ___security_cookie 014 xor [ebp-4], eax ; encrypt pointer to scope table entry 014 xor eax, ebp 014 mov [ebp-1Ch], eax ; store cookie value 014 push eax 018 mov [ebp-18h], esp ; store current esp ; middle part 018 push dword ptr [ebp-8] ; push "return address" of current call ; for next ret instruction 01C mov eax, [ebp-4] ; mov [ebp-4] to [ebp-8] 01C mov dword ptr [ebp-4], 0FFFFFFFEh ; initialize try level 01C mov [ebp-8], eax ; so far, the stack is following: ; ; ebp ; -1 ; 8488DCE0 xor ___security_cookie ; _except_handler4 ; old fs:[0] ; to be pointer to EXCEPTION_POINTERS ; current esp value ; cookie value (___security_cookie xor ebp) ; ... ; ebx ; esi ; edi ; cookie value ; return address to parent function 01C lea eax, [ebp-10h] 01C mov large fs:0, eax ; update fs:[0] to current frame 01C retn ; return to parent function.
由于注释比较详细,就不多说什么了,补充几张图。
函数开始执行2条push指令后的栈,
执行到中间处的栈如下,注意数字顺序。
执行到最后retn之前的栈,此时(ebp-10)处已经构建好了异常栈桢结构,(ebp-18)(ebp-1c)处也赋了相应的值。
接下来分析epilog,先看看KiDispatchException如何调用它的,
; why is there 118? 11C lea esp, [ebp-118h] ; sizeof exception_frame + F8(includes additional information) + sizeof( ebx + esi + edi + cookie ) ; ebp - (10+F8+10) 000 call __SEH_epilog4_GS 000 retn 14h
将栈指针修正到合适的位置,实际上就是__SEH_prolog4_GS函数返回后的栈的位置。然后调用函数__SEH_epilog4_GS,注意call指令会将返回地址压栈。
再看看函数的实现部分,
__SEH_epilog4_GS proc near 000 mov ecx, [ebp-1Ch] ; cookie value (___security_cookie xor ebp) 000 xor ecx, ebp 000 call @__security_check_cookie@4 ; ; cmp ecx, ___security_cookie ; jnz ___report_gsfailure ; retn 000 jmp __SEH_epilog4 __SEH_epilog4_GS endp
检查cookie有没有被破坏,如果没有问题就跳转到__SEH_epilog4。
__SEH_epilog4 proc near 000 mov ecx, [ebp-10h] ; restore old fs:[0] 000 mov large fs:0, ecx 000 pop ecx ; return address of parent function -04 pop edi ; another cookie value besides [ebp-1c] -08 pop edi -0C pop esi -10 pop ebx -14 mov esp, ebp ; restore esp and ebp 000 pop ebp -04 push ecx 000 retn __SEH_epilog4 endp
这个函数比prolog简单的多了,恢复fs:[0],恢复寄存器,返回到调用__SEH_epilog4_GS的地方。
对这2个函数的分析最终演变成了对栈中数据结构的理解,IDA中的观察到的数据结构如下,成员后面加了注释:
EXCEPTION_POINTERS struc ; (sizeof=0x8, standard type) ExceptionRecord dd ? ; pointer to ExceptionRecord ContextRecord dd ? ; pointer to ContextRecord EXCEPTION_POINTERS ends
CPPEH_RECORD struc ; old_esp dd ? ; esp position exc_ptr dd ? ; pointer to EXCEPTION_POINTERS prev_er dd ? ; pointer to old fs:[0] handler dd ? ; exception handler msEH_ptr dd ? ; pointer to scope table entry (might be encrytped) disabled dd ? ; try level CPPEH_RECORD ends
注意图中栈数据的顺序是从高到低,而上面结构中数据成员是从低到高,正好相反。另外,事实上CPPEH_RECORD还差了2个值,一个在old_esp上面,用来放cookie值。另一个在disabled下面,用来存放前个ebp值(和前面的图对比)。在异常处理函数中,可以访问这个数据结构,得到自己需要的信息。
这也让我想起了<人月神话>中的一句话。"给我看你的流程图,而藏起你的表,我仍然莫名其妙,给我看你的表藏起流程图,它们是如此的明显。 "这里的“表”指的就是数据结构,它最终决定了代码的实现。