前面写了不少SEH相关文章,这里来个复杂点的栈溢出SEH。文章不重复解释SEH运行原理,但对主要步骤加以调试和注释,另外本文参考考了雪上 Exploit 编写系列教程第三篇_基于SEH的Exploit 一文。一般来说Exploit重在溢出,不过本文旨在是演示Exploit SEH的原理,因此省略溢出过程直接在栈上修改。

程序源码(vc++6.0 Debug版本)如下:   

#include<windows.h>

int ExceptionHandler(void);
void Fake_Handler();
void FakeShellcode();

int main(int argc,char *argv[])
{
__try
{
__asm -->这段__asm代码用于修改栈上的_EXCEPTION_REGISTRATION结构
{
mov edx,ebp;
sub edx,0x10;
mov DWORD ptr [edx],0x909006EB;
lea eax,Fake_Handler;
mov DWORD ptr [edx+4],eax;
mov BYTE ptr [edx+8],0xE9;
lea ebx,[edx+8];
lea ecx,FakeShellcode;
sub ecx,5;
mov eax,ecx;
sub eax,ebx;
mov DWORD ptr [edx+9],eax;
}
__asm -->这段代码用于产生异常
{
xor eax,eax;
mov [eax],eax;
}
}
__except(ExceptionHandler())
{}
return 0;
}

__declspec(naked) void Fake_Handler()
{
__asm --> 当异常发生,控制流将进入ExceptHandle,pop/pop/ret指令流使控制流从ExcepHandle跳转到当前栈中_EXCEPTION_REGISTRATION_RECORD结构执行
{
pop edi;
pop esi;
ret;
}
}

__declspec(naked) void FakeShellcode()
{
MessageBox(NULL,"","",MB_OK);
}

int ExceptionHandler(void)
{
return 0;
}

前面的博文"vc++6对windows SEH扩展分析"提到vc++对SEH机制做了扩展,将_EXCEPTION_REGISTRATION_RECORD结构

struct _EXCEPTION_REGISTRATION_RECORD 
   {
     struct _EXCEPTION_REGISTRATION *prev;
     void (*handler)(PEXCEPTION_RECORD,
           PEXCEPTION_REGISTRATION,
           PCONTEXT,
           PEXCEPTION_RECORD);
}

扩展为

struct _EXCEPTION_REGISTRATION  
   {
     struct _EXCEPTION_REGISTRATION *prev;
     void (*handler)(PEXCEPTION_RECORD,
           PEXCEPTION_REGISTRATION,
           PCONTEXT,
           PEXCEPTION_RECORD);
     struct scopetable_entry *scopetable;
     int trylevel;
     int _ebp;
     PEXCEPTION_POINTERS xpointers;
   };

代码L13,L14借由_EXCEPTION_REGISTRATION!_ebp的地址减去一个偏移,定位到_EXCEPTION_REGISTRATION!prev的地址,这同时是当前函数栈中的

EXCEPTION_REGISTRATION异常处理结构的起始地址。后面的代码全是在这个结构上倒腾~

L16,L17将EXCEPTION_REGISTRATION!handler从__except_handler3修改为Fake_Handler。


下面的代码片段是仅修改函数异常处理结构_EXCEPTION_REGISTRATION!prev后的栈内存状态。栈内存0012FF40处的0x4012A0为__except_handler3:

0012FF38  EB 06 90 90 A0 12 40 00  ..悙..@.
0012FF40 20 F0 41 00 00 00 00 00 餉.....
0012FF48 88 FF 12 00 69 14 40 00

mapfile内容:

0001:00000291       __NLG_Dispatch             00401291 f   LIBCD:exsup.obj
0001:000002a0 __except_handler3 004012a0 f LIBCD:exsup3.obj
0001:0000035d __seh_longjmp_unwind@4 0040135d f LIBCD:exsup3.obj

可能有人会疑惑,为什么要把_EXCEPTION_REGISTRATION!prev改为"EB 06 90 90"?这是因为后面异常进入Fake_Handler后,执行pop/pop/ret指令流,跳转到_EXCEPTION_REGISTRATION!prev所在的地址。再往后,eip将在_EXCEPTION_REGISTRATION!prev所在的地址取指令运行。如果不修改,这块区域保存了下一个_EXCEPTION_REGISTRATION结构的地址值,鬼晓得这个地址值将是怎样的代码流。所以要改为一段有用的代码;理论上可以是任意可执行的代码,但是EXCEPTION_REGISTRATION!handler保存的是Fake_Handler的地址,如果_EXCEPTION_REGISTRATION!prev处的代码被改为顺序执行的指令,势必会执行到EXCEPTION_REGISTRATION!handler所在的地址。因此需要用跳转指令跳过EXCEPTION_REGISTRATION!handler。

模拟handler覆盖后EXCEPTION_REGISTRATION!handler内容:

0012FF38  EB 06 90 90 0F 10 40 00  ..悙..@.
0012FF40 20 F0 41 00 00 00 00 00 餉.....
0012FF48 88 FF 12 00 69 14 40 00

0x40100F是ILT增量连接表中跳转到Fake_Handler函数的地址


当异常发生,系统首先到fs:[0]队列中找最近节点的异常处理函数,如果处理不了,则通过该节点的prev域找次近节点处理。现在就假设出现一个异常,那得到执行的将是被覆盖了的Fake_Handler函数。我们跟着系统的节奏进入Fake_Handler函数。

Fake_Handler是个裸函数,进入该函数后,编译器并没有自作主张的生成函数帧,因此在执行L41前堆栈的布局如下:

[esp]:Fake_Handler
[esp+4]:_EXCEPTION_RECORD
[esp+8]:EXCEPTION_REGISTER
[esp+0x0C]:_CONTEXT *pContex
[esp+0x10]:pDispatcherContext

当然,你们可以质疑我是不是在大力乱神,我可以证明之:

进入该函数时堆栈布局:

0012FAFC  B9 72 F2 76 E4 FB 12 00  箁騰潲..
0012FB04 38 FF 12 00 00 FC 12 00 8.......
0012FB0C B8 FB 12 00

内存0012FAFC处的0x76F272B9看着像是ntdll中的地址,

看下0x12FBE4处的内容,理应是_EXCEPTION_RECORD结构,里面至少包含了出错信息和出错指令地址:

0012FBE4  05 00 00 C0 00 00 00 00  ........
0012FBEC 00 00 00 00 99 10 40 00 ......@.

其内容是0xC0000005,违例访存和0x401099:mov [eax],eax所在指令地址:

30:           xor eax,eax;
00401097 xor eax,eax
31: mov [eax],eax;
00401099 mov dword ptr [eax],eax
32: }
33: }

到这,可以确定堆栈上参数如上面假设那样。好,经过L41,L42的

两个pop操作目前esp指向了EXCEPTION_REGIST结构---- 当前被展开的异常处理节点,这个节点的内容已经被溢出覆盖。当执行L43 ret指令时会发生什么?想象一下pop eip,将esp的内容传递给eip,之后eip去那取指令运行。

看看执行ret时,esp指向:

ESI = 0012FBE4 EDI = 76F272B9
EIP = 00401102 ESP = 0012FB04
...
0012FB04 38 FF 12 00 00 FC 12 00 8.......

0x12FB04处的内容是0x12FF38,跟过去看看:

0012FF38  EB 06 90 90 0F 10 40 00  ..悙..@.
0012FF40 E9 C0 10 2D 00 00 00 00 槔.-....
0012FF48 88 FF 12 00 69 14 40 00 ....i.@.

如果一时记不起这块内存的内容,麻烦向上滚动一下鼠标滚轮,前面提到这是程序开始时,由编译器在函数堆栈上安置的

EXCEPTION_REGIST异常处理节点!!


很明显了,eip将去堆栈上取指令运行,而且取出的指令来自EXCEPTION_REGIST!prev所在(已经被溢出覆盖)。

回到main函数L15处这里的指令mov DWORD

    main函数中在后面的内容已经没有解释的必要了,本篇完~

相关链接:

1.​​Exploit 编写系列教程第三篇_基于SEH的Exploit(+3b)​

2.​​绕过SEHOP安全机制​