3) 利用监视点定位导致溢出的代码点 https://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 #include #include #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 \n", argv[0] ); return( EXIT_FAILURE ); } strcpy( buf, argv[1] ); printf( "%s\n", buf ); return( EXIT_SUCCESS ); } /* end of main */ 前面讨论了如何尽早中断在vulnerable_0.exe进程空间中,这次介绍如何利用监视点 定位导致溢出的代码点。 在<>中介绍过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 01) BPMD #0023:0012F948 W DR3 IF (*(0x12F948)==0x41414141) 02) 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 01) 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 01) 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,回到我们感兴趣的地方。