前面写了不少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 { 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 { pop edi; pop esi; ret; } } __declspec(naked) void FakeShellcode() { MessageBox(NULL,"","",MB_OK); } int ExceptionHandler(void) { return 0; }前面的博文"vc++6对windows SEH扩展分析"提到vc++对SEH机制做了扩展,在堆栈上扩展了一个结构
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的内容,其中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 00mapfile内容:
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
模拟handler覆盖后EXCEPTION_REGISTRATION内容:
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 000x40100F是ILT增量连接表中跳转到Fake_Handler函数的地址
当异常发生,系统首先到fs:[0]队列中找最近节点的异常处理函数,如果处理不了,则通过该节点的prev域找次近节点处理。现在就假设出现一个异常,那得到执行的将是被覆盖了的Fake_Handler函数。我们跟着系统的节奏进入Fake_Handler函数。
Fake_Handler是个裸函数,进入该函数后,编译器并没有自作主张的生成函数帧,因此在执行L41前堆栈的布局如下:
[esp]:Fake_Handler [esp+4]:<span><span>_EXCEPTION_RECO</span></span> [esp+8]:<span><span>EXCEPTION_REGIST</span></span> [esp+0x0C]:<span><span>_CONTEXT *pContex</span></span> [esp+0x10]:<span><span>pDispatcherContext</span></span>当然,你们可以质疑我是不是在大力乱神,我喜欢听反对声音,证明之:
进入该函数时堆栈布局:
0012FAFC B9 72 F2 76 E4 FB 12 00 箁騰潲.. 0012FB04 38 FF 12 00 00 FC 12 00 8....... 0012FB0C B8 FB 12 000x76F272B8看着像是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 ptr [edx],0x909006EB;将原本prev的内容改成一条跳转语句jmp 06和两个nop的opcode。这有个问题为什么是jmp 06?一般Exploit SEH溢出后内存布局是:buff+next SEH 的地址(被jmp 06;nop;nop代替)+SEH Handler 的地址(4字节地址,在本文被Fake_Handler覆盖)+shellcode。jmp 06执行时,eip指向第一个nop指令,jmp 06执行后,eip跳过中间2B的nop填充指令和4B被覆盖的异常处理过程函数的地址,之后eip直接去shellcode中取指运行~
main函数中在后面的内容已经没有解释的必要了,本篇完~
相关链接:
1.Exploit 编写系列教程第三篇_基于SEH的Exploit(+3b)
2.绕过SEHOP安全机制