3.2 定位shellcode
3.2.1 栈帧移位与jmp esp
回忆2.4节中的代码植入实验,当我们可以用越界的字符完全控制返回地址后,需要将返回地址改写成shellcode在内存中的起始地址。在实际的漏洞利用过程中,由于动态链接库的装入和卸载等原因,Windows进程的函数栈帧很有可能会产生“移位”,即shellcode在内存中的地址是会动态变化的,因此像2.4节中那样将返回地址简单地覆盖成一个定值的做法往往不能让exploit奏效,如图3.2.1所示。
要想使exploit不至于10次中只有2次能成功地运行shellcode,我们必须想出一种方法能够在程序运行时动态定位栈中的shellcode。
回顾2.4节代码植入实验中在verify_password函数返回后栈中的情况,如图3.2.2所示。
(1)实线体现了代码植入的流程:将返回地址淹没为我们手工查出的shellcode起始地址0x0012FAF0,函数返回时,这个地址被弹入EIP寄存器,处理器按照EIP寄存器中的地址取指令,最后栈中的数据被处理器当成指令得以执行。
(2)虚线则点出了这样一个细节:在函数返回的时候,ESP恰好指向栈帧中返回地址的后一个位置!
图3.2.1 栈帧移位示意图
图3.2.2 溢出发生时栈、寄存器与代码之间的关系
一般情况下,ESP寄存器中的地址总是指向系统栈中且不会被溢出的数据破坏。函数返回时,ESP所指的位置恰好是我们所淹没的返回地址的下一个位置,如图3.2.3所示。
提示:函数返回时,ESP所指位置还与函数调用约定、返回指令等有关。例如,retn3与retn4在返回后,ESP所指的位置都会有所差异。
图3.2.3 使用“跳板”的溢出利用流程
由于ESP寄存器在函数返回后不被溢出数据干扰,且始终指向返回地址之后的位置,我们可以使用图3.2.3所示的这种定位shellcode的方法来进行动态定位。
(1)用内存中任意一个jmp esp指令的地址覆盖函数返回地址,而不是原来用手工查出的shellcode起始地址直接覆盖。
(2)函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行shellcode。
(3)由于esp在函数返回时仍指向栈区(函数返回地址之后),jmp esp指令被执行后,处理器会到栈区函数返回地址之后的地方取指令执行。
(4)重新布置shellcode。在淹没函数返回地址后,继续淹没一片栈空间。将缓冲区前边一段地方用任意数据填充,把shellcode恰好摆放在函数返回地址之后。这样,jmp esp指令执行过后会恰好跳进shellcode。
这种定位shellcode的方法使用进程空间里一条jmp esp指令作为“跳板”,不论栈帧怎么“移位”,都能够精确地跳回栈区,从而适应程序运行中shellcode内存地址的动态变化。
本节实验将把4.4节代码植入实验中的password.txt文件改造成上述思路的exploit,并加入安全退出的代码避免点击消息框后程序的崩溃。
题外话:1998年,黑客组织“Cult of the Dead Cow”的Dildog在Bugtrq邮件列表中以Microsoft Netmeeting为例首次提出了利用jmp esp完成对shellcode的动态定位,从而解决了Windows下栈帧移位问题给开发稳定的exploit带来的重重困难。毫不夸张地讲,跳板技术应该算得上是Windows栈溢出利用技术的一个里程碑。
3.2.2 获取“跳板”的地址
我们必须首先获得进程空间内一条jmp esp指令的地址作为“跳板”。通过第1章对PE文件和Win_32平台下进程4GB的虚拟内存空间的学习,我们应当明白除了PE文件的代码被读入内存空间,一些经常被用到的动态链接库也将会一同被映射到内存。其中,诸如kernel.32.dll、user32.dll之类的动态链接库会被几乎所有的进程加载,且加载基址始终相同。
2.4节实验中的有漏洞的密码验证程序已经加载了user32.dll,所以我们准备使用user32.dll中的jmp esp作为跳板。获得user32.dll内跳转指令地址最直观的方法就是编程序搜索内存。
#include <windows.h> #include <stdio.h> #define DLL_NAME "user32.dll" main() { BYTE* ptr; int position,address; HINSTANCE handle; BOOL done_flag = FALSE; handle=LoadLibrary(DLL_NAME); if(!handle) { printf(" load dll erro !"); exit(0); } ptr = (BYTE*)handle; for(position = 0; !done_flag; position++) { try { if(ptr[position] == 0xFF && ptr[position+1] == 0xE4) { //0xFFE4 is the opcode of jmp esp int address = (int)ptr + position; printf("OPCODE found at 0x%x\n",address); } } catch(...) { int address = (int)ptr + position; printf("END OF 0x%x\n", address); done_flag = true; } } }
jmp esp对应的机器码是0xFFE4,上述程序的作用就是从user32.dll在内存中的基地址开始向后搜索0xFFE4,如果找到就返回其内存地址(指针值)。
如果您想使用别的动态链接库中的地址(如“kernel32.dll”、“mfc42.dll”等),或者使用其他类型的跳转地址(如call esp、jmp ebp等),也可以通过对上述程序稍加修改而轻易获得。
除此以外,还可以通过OllyDbg的插件轻易地获得整个进程空间中的各类跳转地址。您可以到看雪论坛的相关版面下载到这个插件(OllyUni.dll),并把它放在OllyDbg目录下的Plugins文件夹内,重新启动OllyDbg进行调试,在代码框内单击右键,就可以使用这个插件了,如图3.2.4所示。
图3.2.4 用OllyDbg的插件搜索“跳板”的地址
搜索结束后,单击OllyDbg中的“L”按钮,就可以在日志窗口中查看搜索结果了。
3.2.3 使用“跳板”定位的exploit
仍然使用2.4节中的代码作为攻击目标,实验环境如表3-2-1所示
表3-2-1 实验环境
说明:函数调用地址和跳转地址依赖于系统补丁,需要在实验时重新确定。确定的方法在实验指导中有详细说明。
运行我们自己编写程序搜索跳转地址得到的结果和OllyDbg插件搜到的结果基本相同,如图3.2.5所示。
图3.2.5 OllyDbg搜出的“跳板”与程序搜出的“跳板”地址
题外话:跳转指令的地址将直接关系到exploit的通用性。事实上,kernel32.dll与user32.dll在不同的操作系统版本和补丁版本中也是有所差异的。最佳的跳转地址位于那些“千年不变”且被几乎所有进程都加载的模块中。
这里不妨采用位于内存0x77DC14CC处的跳转地址jmp esp作为定位shellcode的“跳板”。
在制作exploit的时候,还应当修复2.4节中shellcode无法正常退出的缺陷。为此,我们在调用MessageBox之后,通过调用exit函数让程序干净利落地退出。
这里仍然用dependency walker获得这个函数的入口地址。如图3.2.6所示,ExitProcess是kernel32.dll的导出函数,故首先查出kernel32.dll的加载基址0x7C800000,然后加上函数的偏移地址0x0001CDDA,得到函数入口最终的内存地址0x7C81CDDA。
图3.2.6 计算ExitProcess函数的入口地址
写出的shellcode的源代码如下所示。
#include <windows.h> int main() { HINSTANCE LibHandle; char dllbuf[11] = "user32.dll"; LibHandle = LoadLibrary(dllbuf); _asm{ sub sp,0x440 xor ebx,ebx push ebx // cut string push 0x74736577 push 0x6C696166//push failwest mov eax,esp //load address of failwest push ebx push eax push eax push ebx mov eax,0x77D804EA // address should be reset in different OS call eax //call MessageboxA push ebx mov eax,0x7C81CDDA call eax //call exit(0) } }
为了提取出汇编代码对应的机器码,我们将上述代码用VC6.0编译运行通过后,再用OllyDbg加载可执行文件,选中所需的代码后可直接将其dump到文件中,如图3.2.7所示。
通过IDA Pro等其他反汇编工具也可以从PE文件中得到对应的机器码。当然,如果熟悉intel指令集,也可以为自己编写专用的由汇编指令到机器指令的转换工具。
现在我们已经具备了制作新exploit需要的所有信息。
图3.2.7 从PE文件中提取shellcode的机器码
(1)搜索到的jmp esp地址,用作重定位shellcode的“跳板”:0x77DC14CC。
(2)修改后并重新提取得到的shellcode,如表3-2-2所示。
表3-2-2 shellcode及注释
按照2.4节中对栈内情况的分析,我们将password.txt制作成如图3.2.8所示的形式。
图3.2.8 在输入文件中部署shellcode
现在再运行密码验证程序,怎么样,程序退出的时候不会报内存错误了吧。虽然还是同样的消息框,但是这次植入代码的流程和2.4节中已有很大不同了,最核心的地方就是使用了跳转地址定位shellcode,进程被劫持的过程如图3.2.3中我们设计的那样。