- 利用监视点定位导致溢出的代码点
http://scz.617.cn/windows/200311231550.txt
/*
- For x86/EWindows XP SP1 & VC 7
- cl vulnerable_0.c /nologo /Os /G6 /W3 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /link /RELEASE */
#include <stdio.h> #include <stdlib.h> #include <string.h>
#pragma comment( linker, "/INCREMENTAL:NO" ) #pragma comment( linker, "/subsystem:console" )
int __cdecl main ( int argc, char * argv[] ) { unsigned char buf[8];
if ( 2 != argc )
{
fprintf( stderr, "Usage: %s <any string>\n", argv[0] );
return( EXIT_FAILURE );
}
strcpy( buf, argv[1] );
printf( "%s\n", buf );
return( EXIT_SUCCESS );
} /* end of main */
前面讨论了如何尽早中断在vulnerable_0.exe进程空间中,这次介绍如何利用监视点 定位导致溢出的代码点。
在<<SMB系列(11)--TRANSACT2_OPEN处理过程存在远程缓冲区溢出漏洞>>中介绍过GDB 调试技巧,利用watch功能定位导致溢出的代码点。windbg的ba不能在用户级调试中 使用,很奇怪为什么是这样。SoftICE的bpm断点等价于gdb的watch命令。
打开faults on,让EIP等于0x41414141时SoftICE弹出:
Break due to BP 00: BPX #001B:00401289 (ET=4.41 seconds) :bl 00) BPX #001B:00401289 :faults on :g Break due to UnhandledException NTSTATUS=STATUS_ACCESS_VIOLATION
在当前栈顶(ESP)以低位置搜索特征字节,试图定位ret指令用过的那个栈帧:
:s -a esp&ffff0000 L esp&0000ffff "AAAAAAAAAAAAAAAA" Pattern found at 0023:0012F948 (0000F948) Pattern found at 0023:0012FA60 (0000FA60) 0000000002 occurances found :db 0012F948 L 20 0023:0012F948 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0023:0012F958 0D 0A 00 00 A8 03 00 00-00 00 00 00 01 00 00 00 ................ :db 0012FA60 L 20 0023:0012FA60 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0023:0012FA70 0D 0A 02 03 02 03 02 03-02 03 10 02 10 02 10 02 ................ :faults off :g
注意到这两处结尾都有"\r\n",从源代码可知这两处必不是buf[]所在,像printf() 所用缓冲区。一般没有源代码可供参考,只能试着先在这两外下写监视点,,找出向 此写入"AA..AA"的代码点。重新执行vulnerable_0.exe,中断在总入口点:
:bd 0 :bpmd 0012F948 w if *0012F948==41414141 :bpmd 0012FA60 w if *0012FA60==41414141 :bl 00) * BPX #001B:00401289
- BPMD #0023:0012F948 W DR3 IF (*(0x12F948)==0x41414141)
- BPMD #0023:0012FA60 W DR2 IF (*(0x12FA60)==0x41414141)
再次提醒,bpm断点随进程空间消失而消失,因此应该尽早中断在vulnerable_0.exe 进程空间中,然后设置bpm断点,而不是其它时刻。g继续执行,其中一个写监视点命 中:
:g Break due to BP 02: BPMD #0023:0012FA60 W DR2 IF (*(0x12FA60)==0x41414141) :d 0012FA60 L 20 0023:0012FA60 41 41 41 41 02 03 02 03-02 03 02 03 02 03 02 03 AAAA............ 0023:0012FA70 02 03 02 03 02 03 02 03-02 03 10 02 10 02 10 02 ................ :d 0012F948 L 20 0023:0012F948 00 01 00 00 84 FD 12 00-00 01 00 00 84 FC 12 00 ................ 0023:0012F958 00 01 00 00 A8 03 00 00-00 00 00 00 01 00 00 00 ................ :
有代码在向0x0012FA60写入"AA..AA",在这段代码附近F10单步跟几步,就发现源串 跟如下代码相关:
001B:00403C11 8B55F8 MOV EDX,[EBP-08] 001B:00403C14 FF45F8 INC DWORD PTR [EBP-08] 001B:00403C17 8A12 MOV DL,[EDX]
:dd ebp-8 L 4 0023:0012FE74 003718BC C0BDC4AA 0012FEE4 0040217A ..7.........z!@. :db 003718BC-20 L 40 0023:0037189C 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0023:003718AC 00 00 00 00 01 02 01 01-00 01 08 00 41 41 41 41 ............AAAA 0023:003718BC 41 41 41 41 41 41 41 41-41 41 41 41 0A 00 00 00 AAAAAAAAAAAA.... 0023:003718CC 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
这回发现0x003718B8处也有16个'A',后面还有一个'\n'。再次搜索内存:
:s -a 0 L 7fffffff "AAAAAAAAAAAAAAAA" Pattern found at 0023:0012FEDC (0012FEDC) Pattern found at 0023:00142371 (00142371) Pattern found at 0023:00142FD9 (00142FD9) Pattern found at 0023:00370B3D (00370B3D) Pattern found at 0023:003718B8 (003718B8) 0000000005 occurances found
不必搜索0x80000000开始的空间,那是内核空间。
:stack FrameEBP RetEIP Symbol 0012FE7C 0040217A vulnerable_0!.text+2C11 0012FEE4 41414141 vulnerable_0!.text+117A
查看调用栈回溯时意外发现某个返回地址已经被覆盖成0x41414141,与内存搜索结果 对照,显然我们应该在0x0012FEDC下写监视点:
:be 0 :bc 2 :bpe 1 :BPMD #0023:0012FEDC W DR3 IF (*(0x0012FEDC)==0x41414141) :bl 00) BPX #001B:00401289
- BPMD #0023:0012FEDC W DR3 IF (*(0x12FEDC)==0x41414141) :g
不过这次bpm断点已经无效,0x0012FEDC已经被写过了,而且进程空间消失后bpm断点 随之消失。我用bpe命令先设置一次,留在命令缓冲中。下次因bpx中断后,只需用上 键头找出bpm命令重新设置即可,不必输入很长的一串。重新执行vulnerable_0.exe:
Break due to BP 00: BPX #001B:00401289 (ET=14.80 seconds) :bl 00) BPX #001B:00401289 :BPMD #0023:0012FEDC W DR3 IF (*(0x0012FEDC)==0x41414141) :bl 00) BPX #001B:00401289
- BPMD #0023:0012FEDC W DR3 IF (*(0x12FEDC)==0x41414141) :db 0012FEDC L 20 0023:0012FEDC 00 00 00 00 31 00 00 00-44 9F 59 80 02 00 00 00 ....1...D.Y..... 0023:0012FEEC FF FF FF FF 00 08 00 00-10 8C 18 FD B1 77 4F 80 .............wO.
g继续执行,写监视点命中:
:g Break due to BP 01: BPMD #0023:0012FEDC W DR3 IF (*(0x12FEDC)==0x41414141) :db 0012FEDC L 20 0023:0012FEDC 41 41 41 41 41 41 41 00-C0 FF 12 00 F9 13 40 00 AAAAAAA.......@. 0023:0012FEEC 02 00 00 00 20 0B 37 00-58 0B 37 00 94 00 00 00 .... .7.X.7..... :stack FrameEBP RetEIP Symbol 0012FEE4 004013F9 vulnerable_0!.text+011B 0012FFC0 77E814C7 vulnerable_0!.text+03F9 0012FFF0 00000000 kernel32!_BaseProcessStart+0023
在栈帧0x0012FEE4(EBP)所对应的函数中,有代码导致自己的栈帧被覆盖,这是最典 型的栈溢出。而我们已经找到导致溢出的代码点:
EAX=7EFEFEFE EBX=7FFDF000 ECX=00370B44 EDX=41414141 ESI=00000A28 EDI=0012FEDF EBP=0012FEE4 ESP=0012FECC EIP=0040111B o d I s Z a P c CS=001B DS=0023 SS=0023 ES=0023 FS=0038 GS=0000 ─────────────────────────────────PROT32─ 001B:0040111B 83C704 ADD EDI,04 001B:0040111E BAFFFEFE7E MOV EDX,7EFEFEFF 001B:00401123 8B01 MOV EAX,[ECX] 001B:00401125 03D0 ADD EDX,EAX ... ... 001B:00401150 EBC7 JMP 00401119 001B:00401152 8917 MOV [EDI],EDX (PASSIVE)-KTEB(8106B240)-TID(0164)──vulnerable_0!.text+011B──────
总结一下全过程:
a. 执行问题程序,EIP为0x41414141时SoftICE弹出,在内存中搜索特征字节,记录 地址,准备下次设置写监视点用。
用stack命令查看此刻调用栈回溯,获取更多有用信息。
立即根据PE头计算出程序总入口点,在程序总入口点设置bpx断点。
b. 重新执行问题程序,SoftICE因bpx断点而弹出。根据步骤a记录下来的地址设置写 监视点。
g命令继续执行问题程序。
c. SoftICE因写监视点命中而弹出。再次在内存中搜索特征字节,配合stack命令确 定被覆盖的栈帧以及相应的缓冲区起始地址。
清空那些无用的写监视点,保持bpx断点。
g命令结束本次执行。
d. 重新执行问题程序,SoftICE因bpx断点而弹出。根据步骤c记录下来的地址设置写 监视点。
g命令继续执行问题程序。
e. SoftICE因写监视点命中而弹出。此刻EIP附近的代码就是我们要找的代码。
这是一个逐步逼近的调试过程。vulnerable_0.c相当简单,因此逼近过程快。如果碰 上没有源代码可供参考的复杂应用程序,就远不是这样轻松了。不过整体抽象思路依 旧。
czy后来提到,在EIP等于0x41414141时,检查ESP-4、ESP-8两个位置,他认为这就是 RetAddr、EBP所在。现在来讨论一下这个观点。
函数有__cdecl、__stdcall、__fastcall之分,不能保证ret时是简单的ret,还是弹 出形参的"ret n"。在不清楚是何种调用风格之前,不能假设ESP-4对应RetAddr,C调 用风格有此结论,后两种调用风格则在无形参时有此结论。没有源码时只能"在当前 栈顶(ESP)以低位置搜索特征字节,试图定位ret指令用过的那个栈帧"。
顺便讨论另一个问题。VC编译出来的程序有bp-based frame、non bp-based frame之 分,就是说函数入口处是否有显式的"push ebp/mov ebp, esp"或者enter指令存在。 如果两个push之后是"call __SEH_prolog"指令,就属于non bp-based frame情形。 据我逆向经验,XP SP1/ntdll中涉及SEH/__try块的基本都是non bp-based frame情 形,此时stack布局如下:
内存高址方向
[EBP+0x004] RetAddr [EBP-0x000] _ebp [EBP-0x004] trylevel [EBP-0x008] scopetable [EBP-0x00C] handler [EBP-0x010] prev [EBP-0x014] PEXCEPTION_POINTERS/GetExceptionInformation() [EBP-0x018] Hold the final ESP after all the prologue code has executed [EBP-0x01C] 第001个局部变量
hume指出2000/ntdll是bp-based frame情形,此时第001个局部变量是[EBP-0x004]。 IDA Pro逆向后很容易区分这两种情形。
引发异常后SEH机制会进行全局展开。即使没有全局展开,流程到达某个 scopetable[n].lpfnHandler时,必然有一系列堆栈操作,这将破坏stack中原有数据。 如果在这之后搜索内存中的特征字节,往往找不到所期望的那个区域(发生溢出的缓 冲区、被覆盖的栈帧)。所以前面我说需要逐步逼近。
不过今天讨论czy这个想法时又仔细想了想全过程。应该下这样一个断点:
:bpx ntdll!KiUserExceptionDispatcher do "dd (esp)+c L10;dd ((esp+4))+8c L40"
然后搜索内存中的特征字节,似乎可以尽力保持堆栈原貌,此时最接近ESP的特征字 节所在区域可能就是所期望的区域。我现在没时间测试验证这点,czy有时间可以试 试。
至此又想起另一个问题。通过覆盖_EXCEPTION_REGISTRATION结构的_except_handler 使流程转向,当流程到达_except_handler时,前面已经有一些堆栈操作。构造攻击 模板时要意识到这个问题。"利用SEH机制使流程转向"小节最后成功的那个攻击模板 符合要求。
谢谢czy的讨论,现在对这个问题的理解更深了一些。
以前只是在KiUserExceptionDispatcher入口处下断点定位ExceptionAddress,现在 看来,还应该包括在KiUserExceptionDispatcher入口处下断点搜索内存中的特征字 节。
czy还提到,一般VC写的程序运行之初都要调用GetCommandLineA(),对它下个断点, 多按几下F12,回到我们感兴趣的地方。