2.4 代码植入
2.4.1 代码植入的原理
本章第2节和第3节已经依次展示了淹没相邻变量,改变程序流程和淹没返回地址,改变程序流程的方法。本节将给您介绍一个更有意思的实验——通过栈溢出让进程执行输入数据中植入的代码。
在上节实验中,我们让函数返回到main函数的验证通过分支的指令。试想一下,如果我们在buffer里包含我们自己想要执行的代码,然后通过返回地址让程序跳转到系统栈里执行,我们岂不是可以让进程去执行本来没有的代码,直接去做其他事情了!
图2.4.1 利用栈溢出植入可执行代码的攻击示意图
如图2.4.1所示,在本节实验中,我们准备向password.txt文件里植入二进制的机器码,并用这段机器码来调用Windows的一个API函数MessageBoxA,最终在桌面上弹出一个消息框并显示“failwest”字样。
2.4.2 向进程中植入代码
为了完成在栈区植入代码并执行,我们在上节的密码验证程序的基础上稍加修改,使用如下的实验代码。
#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; char password[1024]; FILE * fp; LoadLibrary("user32.dll");//prepare for messagebox if(!(fp=fopen("password.txt","rw+"))) { exit(0); } fscanf(fp,"%s",password); valid_flag = verify_password(password); if(valid_flag) { printf("incorrect password!\n"); } else { printf("Congratulation! You have passed the verification!\n"); } fclose(fp); }
这段代码在2.3节溢出代码的基础上修改了3处。
(1)增加了头文件windows.h,以便程序能够顺利调用LoadLibrary函数去装载user32.dll。
(2)verify_password函数的局部变量buffer由8字节增加到44字节,这样做是为了有足够的空间来“承载”我们植入的代码。
(3)main函数中增加了LoadLibrary("user32.dll")用于初始化装载user32.dll,以便在植入代码中调用MessageBox。
实验环境如表2-4-1所示。
表2-4-1 实验环境
说明:即便完全采用所推荐的实验环境,函数返回地址、MessageBoxA函数的入口地址等也需要重新确定,因为这些地址可能依赖于操作系统的补丁版本等。这些地址的确定方法在实验指导中均给出了详细的说明。
用VC6.0将上述代码编译(默认编译选项,编译成debug版本),得到有栈溢出的可执行文件。在同目录下创建password.txt文件用于程序调试。
我们准备在password.txt文件中植入二进制的机器码,在password.txt攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示“failwest”字样。
让我们在动手之前回顾一下我们需要完成的几项工作。
(1)分析并调试漏洞程序,获得淹没返回地址的偏移。
(2)获得buffer的起始地址,并将其写入password.txt的相应偏移处,用来冲刷返回地址。
(3)向password.txt中写入可执行的机器代码,用来调用API弹出一个消息框。
本节验证程序里verify_password中的缓冲区为44个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态。
如果在password.txt中写入恰好44个字符,那么第45个隐藏的截断符null将冲掉authenticated低字节中的1,从而突破密码验证的限制。我们不妨就用44个字节作为输入来进行动态调试。
出于字节对齐、容易辨认的目的,我们把“4321”作为一个输入单元。
buffer[44]共需要11个这样的单元。
第12个输入单元将authenticated覆盖;第13个输入单元将前栈帧EBP值覆盖;第14个输入单元将返回地址覆盖。
分析过后,我们需要进行调试验证分析的正确性。首先,在password.txt中写入11组“4321”,共44个字符,如图2.4.2所示
图2.4.2 制作溢出文件
如我们所料,authenticated被冲刷后,程序将进入验证通过的分支,如图2.2.3所示。
用OllyDbg加载这个生成的PE文件进行动态调试,字符串复制函数过后的栈状态如图2.4.4所示。时的栈区内存如表2-4-2所示。
图2.4.3 验证通过
图2.4.4 调试栈的布局
表2-4-2 栈帧数据
动态调试的结果证明了前边分析的正确性。从这次调试中,我们可以得到以下信息。
(1)buffer数组的起始地址为0x0012FAF0。
(2)password.txt文件中第53~56个字符的ASCII码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址。
也就是说,将buffer的起始地址0x0012FAF0写入password.txt文件中的第53~56个字节,在verify_password函数返回时会跳到我们输入的字串开始取指执行。
我们下面还需要给password.txt中植入机器代码。
让程序弹出一个消息框只需要调用Windows的API函数MessageBox。MSDN对这个函数的解释如下。
int MessageBox( HWND 错误!超级链接引用无效。, // handle to owner window LPCTSTR 错误!超级链接引用无效。, // text in message box LPCTSTR 错误!超级链接引用无效。, // message box title UINT 错误!超级链接引用无效。 // message box style );
·hWnd [in] 消息框所属窗口的句柄,如果为NULL,消息框则不属于任何窗口。
·lpTex[in] 字符串指针,所指字符串会在消息框中显示。
·lpCaption [in] 字符串指针,所指字符串将成为消息框的标题。
·uType [in] 消息框的风格(单按钮、多按钮等),NULL代表默认风格。
我们将给出调用这个API的汇编代码,然后翻译成机器代码,用十六进制编辑工具填入password.txt文件。
题外话:熟悉MFC的程序员一定知道,其实系统中并不存在真正的MessagBox函数,对MessageBox这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数(ASCII)或者“W”类函数(UNICODE)调用。因此,我们在汇编语言中调用的函数应该是MessageBoxA。多说一句,其实MessageBoxA的实现只是在设置了几个不常用参数后直接调用MessageBoxExA。探究API的细节超出了本书所讨论的范围,有兴趣的读者可以参阅其他书籍。
用汇编语言调用MessageboxA需要3个步骤。
(1)装载动态链接库user32.dll。MessageBoxA是动态链接库user32.dll的导出函数。虽然大多数有图形化操作界面的程序都已经装载了这个库,但是我们用来实验的consol版并没有默认加载它。
(2)在汇编语言中调用这个函数需要获得这个函数的入口地址。
(3)在调用前需要向栈中按从右向左的顺序压入MessageBoxA的4个参数。
为了让植入的机器代码更加简洁明了,我们在实验准备中构造漏洞程序的时候已经人工加载了user32.dll这个库,所以第一步操作不用在汇编语言中考虑。
MessageBoxA的入口参数可以通过user32.dll在系统中加载的基址和MessageBoxA在库中的偏移相加得到。具体的我们可以使用VC6.0自带的小工具“Dependency Walker”获得这些信息。您可以在VC6.0安装目录下的Tools下找到它,如图2.4.5所示。
图2.4.5 使用Depends
运行Depends后,随便拖拽一个有图形界面的PE文件进去,就可以看到它所使用的库文件了。在左栏中找到并选中user32.dll后,右栏中会列出这个库文件的所有导出函数及偏移地址;下栏中则列出了PE文件用到的所有的库的基地址。
如图2.4.6所示,user32.dll的基地址为0x77D40000,MessageBoxA的偏移地址为0x000404EA。基地址加上偏移地址就得到了MessageBoxA在内存中的入口地址0x77D804EA。
图2.4.6 计算相关API的虚拟内存地址
提示:user32.dll的基地址和其中导出函数的偏移地址与操作系统版本号、补丁版本号等诸多因素相关,故您用于实验的计算机上的函数入口地址很可能与这里不一致。请您一定注意要在当前实验的计算机上重新计算函数入口地址,否则后面的函数调用会出错。能够适应于各种操作系统版本的通用的代码植入方法将在第5章进行详细介绍。
有了这个入口地址,就可以编写进行函数调用的汇编代码了。这里我们先把字符串“failwest”压入栈区,消息框的文本和标题都显示为“failwest”,只要重复压入指向这个字符串的指针即可;第1个和第4个参数这里都将设置为NULL。写出的汇编代码和指令所对应的机器代码如表2-4-3所示。
表2-4-3 机器代码
题外话:从汇编指令到机器码的转换可以有很多种方法。调试汇编指令,从汇编指令中提取出二进制机器代码的方法将在第5章集中讨论。由于这里仅仅用了11条指令和对应的26个字节的机器代码,如果您一定要现在就弄明白指令到机器码是如何对应的话,直接查阅Intel的指令集手工翻译也是可以的。
将上述汇编指令对应的机器代码按照上一节介绍的方法以十六进制形式逐字写入password.txt,第53~56字节填入buffer的起址0x0012FAF0,其余的字节用0x90(nop指令)填充,如图2.4.7所示。
图2.4.7 将机器代码写入文件
换回文本模式可以看到这些机器代码所对应的字符,如图2.4.8所示。
图2.4.8 ASCII编码下的机器代码
这样构造了password.txt之后再运行验证程序,程序执行的流程将如图2.4.9所示。
图2.4.9 栈溢出利用示意图
程序运行情况如图2.4.10所示。
图2.4.10 输入文件中的代码植入成功
成功地弹出了我们植入的代码。
但是在单击“OK”按钮之后,程序会崩溃,如图2.4.11所示。
图2.4.11 被破坏的栈在程序退出时引起程序崩溃
这是因为MessageBoxA调用的代码执行完成后,我们没有写用于安全退出的代码的缘故。
您会在后面的章节中见到更深入的代码植入讨论,包括编写通用的植入代码,在植入代码中安全地退出,甚至在植入代码结束后修复堆栈和寄存器,让程序重新回到正常的执行流程。