看到这一章我又抓狂了:作者提及从进程空间类型为Map的映射文件中寻找如下指令地址:
call/jmp dword ptr[esp+0x8]
call/jmp dword ptr[esp+0x14]
call/jmp dword ptr[esp+0x1c]
call/jmp dword ptr[esp+0x2c]
call/jmp dword ptr[esp+0x44]
call/jmp dword ptr[esp+0x50]
call/jmp dword ptr[ebp+0xc]
call/jmp dword ptr[ebp+0x24]
call/jmp dword ptr[ebp+0x30]
call/jmp dword ptr[ebp-0x4]
call/jmp dword ptr[ebp-0xc]
call/jmp dword ptr[ebp-0x18]
至于为什么要寻找这些指令,作者的态度是轻拂衣袖不留任何解释,空留我一脸迷茫。迷茫很久我决定还调试一遍观察进程栈布局并把调试的结果记录于此。示例代码就用作者书本上的那个,这里就不重复贴代码了。
进入__try/__except块之后,程序当前的异常处理链如下:
0:000> !exchain
0012fe80<-----这个数值很重要,所有的jmp [ebp+N]最终是要跳到这个地址上: offset!ILT+115(__except_handler4)+0 (00411078)
0012ffa8: offset!ILT+115(__except_handler4)+0 (00411078)
0012ffe0: kernel32!_except_handler3+0 (7c839ac0)
CRT scope 0, filter: kernel32!BaseProcessStart+29 (7c843882)
func: kernel32!BaseProcessStart+3a (7c843898)
Invalid exception stack at ffffffff
0:000> dd fs:[0] L1 ;进程异常处理链表头
003b:00000000 0012fe80 ;当前第一个异常块的地址为:0x12fe80
0:000> dd 0012fe80 L2 ;第一个异常块的内容
0012fe80 0012ffa8 00411078
在这个异常链的保护下,当异常发生并且OS接管异常后,经过一路过关斩将最终会进入函数ntdll!ExecuteHandler4(如果程序没有溢出)
0:000> x *!__except_handler4 ;查找异常处理函数ExecuteHandler4所在模块
00411840 offset!_except_handler4 = <no type information>
102d3280 MSVCR90D!_except_handler4 = <no type information>
0:000> bp offset!_except_handler4 ;为了方便观察异常发生时,OS调用异常处理函数ExecuteHandler4的栈回溯,需要在这个函数上下断点
0:000> g ;触发除0异常
(a78.e6c): Integer divide-by-zero - code c0000094 (first chance)
First chance exceptions are reported before any exception handling.
我们来观察一下异常处理的栈回溯信息:
0:000> kb
ChildEBP RetAddr Args to Child
0012f9b4 7c9232a8 0012faa0 0012fe80 0012fab4 offset!_except_handler4
0012f9d8 7c92327a 0012faa0 0012fe80 0012fab4 ntdll!ExecuteHandler2+0x26 ;由ntdll!ExecuteHandler2调用异常处理函数offset!_except_handler4
0012fa88 7c92e46a 00000000 0012fab4 0012faa0 ntdll!ExecuteHandler+0x24
0012fa88 00411493 00000000 0012fab4 0012faa0 ntdll!KiUserExceptionDispatcher+0xe
0012fe90 00411588 0041573c 00edf6ee 00edf7b4 offset!test+0x73 [c:\documents and settings\administrator\桌面\studio\offset\offset\offset.cpp @ 22]
0012ff68 00411b88 00000001 00394c70 00393320 offset!main+0x28 [c:\documents and settings\administrator\桌面\studio\offset\offset\offset.cpp @ 30]
0012ffb8 004119cf 0012fff0 7c817067 00edf6ee offset!__tmainCRTStartup+0x1a8 [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 586]
0012ffc0 7c817067 00edf6ee 00edf7b4 7ffdf000 offset!mainCRTStartup+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c @ 403]
根据栈回溯信息,我们可以确定offset!_except_handler4是由ntdll!ExecuteHandler2调用的,而异常处理函数的接口形式是固定的,形如:
EXCEPTION_DISPOSITION ExcuteHandler(PEXCEPTION_RECORD,PEXCEPTION_REGISTRATION,struct _CONTEXT*,void*);
既然ntdll!ExecuteHandler2负责调用offset!_except_handler4,那么它也必须负责为offset!_except_handler4传递参数。代码片段摘取了部分ntdll!ExecuteHandler2实现,尽管是汇编代码,但还是请读者耐心的往下看下去:
0:000> uf ntdll!ExecuteHandler2
ntdll!ExecuteHandler2:
7c923282 55 push ebp
7c923283 8bec mov ebp,esp
7c923285 ff750c push dword ptr [ebp+0Ch]
7c923288 52 push edx
7c923289 64ff3500000000 push dword ptr fs:[0]
7c923290 64892500000000 mov dword ptr fs:[0],esp
7c923297 ff7514 push dword ptr [ebp+14h]
7c92329a ff7510 push dword ptr [ebp+10h]
7c92329d ff750c push dword ptr [ebp+0Ch] <-------------------
7c9232a0 ff7508 push dword ptr [ebp+8]
7c9232a3 8b4d18 mov ecx,dword ptr [ebp+18h]
7c9232a6 ffd1 call ecx
箭头指向处出现了作者提到的第一个跳板指令的跳转目标:[ebp+0x0c]。当然,看了这段代码,你并不能确定最后一行call ecx就是调用offset!_except_handler4。可以通过查看ecx的符号来确定call ecx的目标。另外,由于我在offset!_except_handler4入口加了断点,程序停留在函数入口并没有对ecx做操作:
0:000> r esp,ebp ;call ecx后进入到offset!_except_handler4中时,ebp/esp的值
esp=0012f9b8 ebp=0012f9d8
0:000> ln ecx
(00411078) offset!ILT+115(__except_handler4) | (0041107d) offset!ILT+120(__lock)
Exact matches:
除了获得这个信息,
还可以看到刚进入offset!_except_handler4时,esp和ebp在数值上差0x20.这个差值很重要,为什么这么说?如果你仔细观察作者提到的指令地址就会发现
这样的规律:[ebp+N-0x20]=[esp+M],换句话说,两个指令指向的地址是同一个,只是用了不同的寄存器做索引,于是,我们有了这样的对应关系:
call/jmp dword ptr[esp+0x8] = call/jmp dword ptr[ebp-0x18]
call/jmp dword ptr[esp+0x14] = call/jmp dword ptr[ebp-0xc]
call/jmp dword ptr[esp+0x1c] = call/jmp dword ptr[ebp-0x4]
call/jmp dword ptr[esp+0x2c] = call/jmp dword ptr[ebp+0xc]
call/jmp dword ptr[esp+0x44] = call/jmp dword ptr[ebp+0x24]
call/jmp dword ptr[esp+0x50] = call/jmp dword ptr[ebp+0x30]
有了这个关系,我们分析的时候可以把注意力放在一类指令上,比如,后面的章节我将集中分析[ebp+N]系列的指令。
目前为止,溢出后能利用SEH并绕过SafeSeh保护的原因是因为异常处理结构EXCEPTION_REGISTRATION_RECORD存放在retAddr前面(地址比retAddr更低)的栈地址。当函数的缓冲区被溢出后,shellcode分布在从变量地址到retAddr附近的栈空间上(中间也包含了异常处理节点)。要让异常处理程序在茫茫4G进程空间定位并执行这段空间(shellcode一般才几百字节)几乎是海底捞针。不过这并不代表没有办法定位,办法就是利用异常处理过程中传递的
EXCEPTION_REGISTRATION_RECORD变量。如前所述,EXCEPTION_REGISTRATION_RECORD结构中包含了触发异常时,当前栈上的异常处理结构:
0:000> dt EXCEPTION_REGISTRATION_RECORD
offset!EXCEPTION_REGISTRATION_RECORD
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION
由于人为制造溢出的缘故,书中提到Next所在的位置被篡改为短跳转语句,Handler被篡改为call [ebp+N]指令所在地址。这样安排的目的是让函数ntdll!ExecuteHandler2执行指令call ecx(ecx中应该为offset!_except_handler4)时,错误的从ecx中取出jmp [ebp+N]的地址,先跳到进程空间映射文件中执行jmp [ebp+N],然后跳转到栈中变量EXCEPTION_REGISTRATION_RECORD!Next继续取指令执行,当然,这回取到的是一个短跳转语句。
大概的流程读者可能知道了,但是我知道仍然有许多未解的疑惑----那就是[ebp+N]代表啥?哎,这是最难的部分,慢慢来吧。先看[ebp+N]这种表达方式是不是很眼熟?对,这些语句就是用作取局部变量/函数参数。先看看第一个[ebp-0x0c]:
0:000> kb ;查看堆栈,程序当前进入函数offset!_except_handler4
ChildEBP RetAddr Args to Child
0012f9b4 7c9232a8 0012faa0 0012fe80 0012fab4 offset!_except_handler4
0012f9d8 7c92327a 0012faa0 0012fe80 0012fab4 ntdll!ExecuteHandler2+0x26
0:000> r eip ;查看当前执行到函数中哪条指令以及函数反汇编
eip=00411840
0:000> uf .
offset!_except_handler4:
00411840 8bff mov edi,edi
00411842 55 push ebp
;上面的结果告诉我们,虽然进入函数offset!_except_handler4,但ebp没有变动仍然保持它在ntdll!ExecuteHandler2中的值,
;这和发生溢出后异常处理调用被溢出的异常处理函数而进入到单指令函数call [ebp+N]时的情景是一致的
0:000> r ebp
ebp=0012f9d8
0:000> ?? @ebp-0xc ;[ebp-0x0c]指向的地址
unsigned int 0x12f9cc
0:000> dd 0x12f9cc L2 ;这个地址包含的内容是一个异常处理结构,这也可以从下面!exchain的输出得到印证
0012f9cc 0012fe80 <-----文章开头提过这个数值,这是第一次出现 7c9232bc
0:000> ln 7c9232bc
(7c923282) ntdll!ExecuteHandler2+0x3a | (7c92330a) ntdll!RtlpUnlinkHandler
0:000> !exchain
0012f9cc: ntdll!ExecuteHandler2+3a (7c9232bc)
0012fe80: offset!ILT+115(__except_handler4)+0 (00411078)
0012ffa8: offset!ILT+115(__except_handler4)+0 (00411078)
0012ffe0: kernel32!_except_handler3+0 (7c839ac0)
CRT scope 0, filter: kernel32!BaseProcessStart+29 (7c843882)
func: kernel32!BaseProcessStart+3a (7c843898)
Invalid exception stack at ffffffff
[ebp-0x0c]这个异常处理结构是进入ntdll!ExecuteHandler2后形成的内嵌异常处理器 (参见<软件调试>P723) 反汇编代码如下:
ntdll!ExecuteHandler2:
7c923282 55 push ebp ;[ebp+0]
7c923283 8bec mov ebp,esp
7c923285 ff750c push dword ptr [ebp+0Ch] ;将函数ntdll!ExecuteHandler2参数2压入栈中,压入后变成局部变量,位置在[ebp-4]
7c923288 52 push edx ;第二个局部变量[ebp-8]
7c923289 64ff3500000000 push dword ptr fs:[0] ;fs:[0]原本保存了异常发生时的异常节点EXCEPTION_REGISTRATION_RECORD!Next.第三个变量[ebp-c]
7c923290 64892500000000 mov dword ptr fs:[0],esp
上面第4条push指令,将fs:[0]的值重新压入堆栈,这个值原本指向程序第一个异常处理节点(前面多次提到该节点存放在栈上,已被溢出覆盖)。经过这次push操作,这个节点存放于地址[ebp-c]。jmp [ebp-c]就是跳到栈上异常处理节点EXCEPTION_REGISTRATION_RECORD!prev地址处。
剩下的指令地址将在下一篇分析