4.3.2 控制程序的执行流程
用键盘输入字符的ASCII表示范围有限,很多值(如0x11、0x12等符号)无法直接用键盘输入,所以我们把用于实验的代码稍加改动,将程序的输入由键盘改为从文件中读取字符串。
#include <stdio.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[8]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; char password[1024]; FILE * fp; 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); } |
以上节实验中的代码为基础,稍作修改后得到上述代码。程序的基本逻辑和上一节中的代码大体相同,只是现在将从同目录下的password.txt文件中读取字符串,而不是用键盘输入。我们可以用十六进制的编辑器把我们想写入但不能直接键入的ASCII字符写进这个password.txt文件。
实验环境如表4-3-3所示。
表4-3-3 实验环境
|
|
推荐使用的环境 |
备 注 |
|
操作系统 |
Windows XP sp2 |
其他Win32操作系统也可进行本实验 |
|
编译器 |
Visual C++ 6.0 |
如使用其他编译器,需重新调试 |
|
编译选项 |
默认编译选项 |
VS2003和VS2005中的GS编译选项会使栈溢出实验失败 |
|
build版本 |
debug版本 |
如使用release版本,则需要重新调试 |
如果完全采用实验指导所推荐的实验环境,将精确地重现指导中所有的细节;否则需要根据具体情况重新调试。
用VC6.0将上述代码编译链接(使用默认编译选项,BUILD成debug版本),在与PE文件同目录下建立password.txt并写入测试用的密码之后,就可以用OllyDbg加载调试了。
开始动手之前,我们先理清思路,看看要达到实验目的我们都需要做哪些工作。
(1)要摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。这虽然可以通过分析代码得到,但我还是推荐从动态调试中获得这些信息。
(2)要得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行。
(3)要在password.txt文件的相应偏移处填上这个地址。
这样verify_password函数返回后就会直接跳转到验证通过的正确分支去执行了。
首先用OllyDbg加载得到可执行PE文件,如图4.3.5所示。
|
| 图4.3.5 提示验证通过的代码位置 |
阅读图4.3.5中显示的反汇编代码,可以知道通过验证的程序分支的指令地址为0x00401122。
0x00401102处的函数调用就是verify_password函数,之后在0x0040110A处将EAX中的函数返回值取出,在
0x0040110D处与0比较,然后决定跳转到提示验证错误的分支或提示验证通过的分支。
提示验证通过的分支从0x00401122处的参数压栈开始。如果我们把返回地址覆盖成这个地址,那么在0x00401102 处的函数调用返回后,程序将跳转到验证通过的分支,而不是进入0x00401107处分支判断代码。这个过程如图4.3.6所示。
|
| 图4.3.6 栈溢出攻击示意图 |
通过动态调试,发现栈帧中的变量分布情况基本没变。这样我们就可以按照如下方法构造password.txt中的数据。
仍然出于字节对齐、容易辨认的目的,我们将“4321”作为一个输入单元。
buffer[8]共需要两个这样的单元。
第3个输入单元将authenticated覆盖;第4个输入单元将前栈帧EBP值覆盖;第5个输入单元将返回地址覆盖。
为了把第5个输入单元的ASCII码值0x34333231修改成验证通过分支的指令地址0x00401122,我们将借助十六进制编辑工具UltraEdit来完成(0x40、0x11等ASCII码对应的符号很难用键盘输入)。
步骤1:创建一个名为password.txt的文件,并用记事本打开,在其中写入5个“4321”后保存到与实验程序同名的目录下,如图4.3.7所示。
|
| 图4.3.7 制作触发栈溢出的输入文件 |
步骤2:保存后用UltraEdit_32重新打开,如图4.3.8所示。
|
| 图4.3.8 制作触发栈溢出的输入文件 |
步骤3:将UltraEdit_32切换到十六进制编辑模式,如图4.3.9所示。
|
| 图4.3.9 制作触发栈溢出的输入文件 |
步骤4:将最后4个字节修改成新的返回地址,注意这里是按照“内存数据”排列的,由于“大顶机”的缘故,为了让最终的“数值数据”为0x00401122,我们需要逆序输入这4个字节,如图4.3.10所示。
|
| 图4.3.10 制作触发栈溢出的输入文件 |
步骤5:这时我们可以切换回文本模式,最后这4个字节对应的字符显示为乱码,如图4.3.11所示。
|
| 图4.3.11 制作触发栈溢出的输入文件 |
将password.txt保存后,用OllyDbg加载程序并调试,可以看到最终的栈状态如表4-3-4所示。
表4-3-4 栈帧数据
|
局部变量名 |
内 存 地 址 |
偏移3处的值 |
偏移2处的值 |
偏移1处的值 |
偏移0处的值 |
|
buffer[0~3] |
0x0012FB14 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
buffer[4~7] |
0x0012FB18 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
authenticated(被覆盖前) |
0x0012FB1C |
0x00 |
0x00 |
0x00 |
0x01 |
|
authenticated(被覆盖后) |
0x0012FB1C |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
前栈帧EBP(被覆盖前) |
0x0012FB20 |
0x00 |
0x12 |
0xFF |
0x80 |
|
前栈帧EBP(被覆盖后) |
0x0012FB20 |
0x31 (‘1’) |
0x32 (‘2’) |
0x33 (‘3’) |
0x34 (‘4’) |
|
返回地址(被覆盖前) |
0x0012FB24 |
0x00 |
0x40 |
0x11 |
0x07 |
|
返回地址(被覆盖后) |
0x0012FB24 |
0x00 |
0x40 |
0x11 |
0x22 |
程序执行状态如图4.3.12所示。
|
| 图4.3.12 栈溢出成功改变了程序执行流程 |
由于栈内EBP等被覆盖为无效值,使得程序在退出时堆栈无法平衡,导致崩溃。虽然如此,我们已经成功地淹没了返回地址,并让处理器如我们设想的那样,在函数返回时直接跳转到了提示验证通过的分支。