2.3 修改函数返回地址
Posted narisu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2.3 修改函数返回地址相关的知识,希望对你有一定的参考价值。
2.3 修改函数返回地址
2.3.1 返回地址与程序流程
上节实验介绍的改写邻接变量的方法是很有用的,但这种漏洞利用对代码环境的要求相对比较苛刻。更通用、更强大的攻击通过缓冲区溢出改写的目标往往不是某一个变量,而是瞄准栈帧最下方的 EBP 和函数返回地址等栈帧状态值。
回顾上节实验中输入 7 个‘q’程序正常运行时的栈状态,栈帧数据如表所示。
局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值 buffer 0x0012fb18 0x71(‘q‘) 0x71(‘q‘) 0x71(‘q‘) 0x71(‘q‘) 0x0012fb1c NULL 0x71(‘q‘) 0x71(‘q‘) 0x71(‘q‘) authenticated 0x0012fb20 0x00 0x00 0x00 0x01 前栈帧 EBP 0x0012fb24 0x00 0x12 0xFF 0x80 返回地址 0x0012fb28 0x00 0x40 0x10 0xEB OD中的动态调试,如下图:
如果继续增加输入的字符,那么超出 buffer[8]边界的字符将依次淹没 authenticated、前栈帧 EBP、返回地址。也就是说,控制好字符串的长度就可以让字符串中相应位置字符的 ASCII 码覆盖掉这些栈帧状态值。
按照上面对栈帧的分析,不难得出下面的结论。
- 输入 11 个‘q’,第 9~11 个字符连同 NULL 结束符将 authenticated 冲刷为 0x00717171。运行情况如图所示。
- 输入 15 个‘q’,第 9~12 个字符将 authenticated 冲刷为 0x71717171;第 13~15 个字符连同 NULL 结束符将前栈帧 EBP 冲刷为 0x00717171。运行情况如图所示。
- 输入 19 个‘q’,第 9~12 个字符将 authenticated 冲刷为 0x71717171;第 13~16 个字将前栈帧 EBP 冲刷为 0x71717171;第 17~19 个字符连同 NULL 结束符将返回地址冲刷为 0x00717171。运行情况如图所示。
在vc6.0,这里用 19 个字符作为输入,看看淹没返回地址会对程序产生什么影响。出于双字对齐的目的,我们输入的字符串按照“4321”为一个单元进行组织,最后输入的字符串为“4321432143214321432”,运行情况如图所示。
说明,栈溢出导致程序崩溃。
用 OllyDbg 加载程序,在字符串复制函数调用结束后观察栈状态,如图 2.3.2 所示。
图 2.3.2 溢出前栈中的布局 实际的内存状况和我们分析的结论一致,此时的栈状态如表 2-3-2 所示。
表 2-3-2 栈帧数据 局部变量名 内存地址 偏移3处的值 偏移2字节 偏移1字节 偏移0字节 buffer[0~3] 0x0012fb18 0x31(‘1‘) 0x32(‘2‘) 0x33(‘3‘) 0x34(‘4‘) buffer[4~7] 0x0012fb1c 0x31(‘1‘) 0x32(‘2‘) 0x33(‘3‘) 0x34(‘4‘) authenticated(被覆盖前) 0x0012fb20 0x00 0x00 0x00 0x01 authenticated(被覆盖后) 0x0012fb20 0x31(‘1‘) 0x32(‘2‘) 0x33(‘3‘) 0x34(‘4‘) 前栈帧 EBP (被覆盖前) 0x0012fb24 0x00 0x12 0xFF 0x80 前栈帧 EBP (被覆盖后) 0x0012fb24 0x31(‘1‘) 0x32(‘2‘) 0x33(‘3‘) 0x34(‘4‘) 返回地址(被覆盖前) 0x0012fb28 0x00 0x40 0x10 0xEB 返回地址(被覆盖后) 0x0012fb28 0x00(NULL) 0x32(‘2‘) 0x33(‘3‘) 0x34(‘4‘) 前面已经说过,返回地址用于在当前函数返回时重定向程序的代码。在函数返回的“retn” 指令执行时,栈顶元素恰好是这个返回地址。“retn”指令会把这个返回地址弹入 EIP 寄存器,之后跳转到这个地址去执行。
在这个例子中,返回地址本来是 0x004010EB,对应的是 main 函数代码区的指令,如图 2.3.3 所示。
图 2.3.3 正常情况下函数返回后的指令 现在我们已经把这个地址用字符的 ASCII 码覆盖成了 0x00323334,函数返回时的状态如 图 2.3.4 所示。(依然是在地址00401059处设置断点,使用f8快捷键调试)
使用f8快捷键动态调试,到地址0040106f处停止:
接着使用快捷键f7步入,如图
图 2.3.4 溢出后程序返回到无效地址 0x00323334 由于 0x00323334 是一个无效的指令地址,所以处理器在取指的时候发生了错误使程序崩 溃。但如果这里我们给出一个有效的指令地址,就可以让处理器跳转到任意指令区去执行(比如直接跳转到程序验证通过的部分),也就是说,我们可以通过淹没返回地址而控制程序的执行流程。以上就是通过淹没栈帧状态值控制程序流程的原理,也是本节实验要做的事。
2.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("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!
");
}
else
{
printf("Congratulation! You have passed the verification!
");
}
fclose(fp);
}
以上节实验中的代码为基础,稍作修改后得到上述代码。程序的基本逻辑和上一节中的代码大体相同,只是现在将从同目录下的 password.txt 文件中读取字符串,而不是用键盘输入。 我们可以用十六进制的编辑器把我们想写入但不能直接键入的 ASCII 字符写进这个 password.txt 文件。
实验环境
同1.4 Crack小实验(以后若无特殊声明,均以此环境为准)
如果完全采用实验本文的实验环境,将精确地重现指导中所有的细节,否则需要根据具体情况重新调试。
实验步骤
用 VC6.0 将上述代码编译链接(使用默认编译选项,Build 成 debug 版本),在与 PE 文件 同目录下建立 password.txt 并写入测试用的密码“1234567”,测试程序是否正确。
之后,就可以用 OllyDbg 加载调试了。
开始动手之前,我们先理清思路,看看要达到实验目的我们都需要做哪些工作。
- 要摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。这虽然可以通过分析代码得到,但我还是推荐从动态调试中获得这些信息。
- 要得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行。
- 要在 password.txt 文件的相应偏移处填上这个地址。
这样 verify_password 函数返回后就会直接跳转到验证通过的正确分支去执行了。
首先用 OllyDbg 加载得到可执行 PE 文件(第1步在password.txt写入正确密码,然后编译执行,产生PE文件,即test.exe文件),如图 2.3.5 所示。
图 2.3.5 提示验证通过的代码位置 阅读图 2.3.5 中显示的反汇编代码,可以知道通过验证的程序分支的指令地址为 0x00401102。
0x004010E2 处的函数调用就是 verify_password 函数,之后在 0x004010EA 处将 EAX 中的函数返回值取出,在 0x004010ED 处与 0 比较,然后决定跳转到提示验证错误的分支或提示验证通过的分支。
提示验证通过的分支从 0x00401102 处的参数压栈开始。如果我们把返回地址覆盖成这个 地址,那么在 0x004010E2 处的函数调用返回后,程序将跳转到验证通过的分支,而不是进入 0x004010E7 处分支判断代码。这个过程如图 2.3.6 所示。
图 2.3.6 栈溢出攻击示意图 仍然出于字节对齐、容易辨认的目的,我们将“4321”作为一个输入单元。 buffer[8]共需要两个这样的单元。
第 3 个输入单元将 authenticated 覆盖;第 4 个输入单元将前栈帧 EBP 值覆盖;第 5 个输入单元将返回地址覆盖。为了把第 5 个输入单元的 ASCII 码值 0x34333231 修改成验证通过分支的指令地址
0x00401102,我们将借助十六进制编辑工具 UltraEdit 来完成(0x40、0x11 等 ASCII 码对应的符号很难用键盘输入)。
步骤 1:将上面创建的 password.txt 文件用记事本打开,在其中写入 5 个“4321” 后保存。如图 2.3.7 所示。
图 2.3.7 制作触发栈溢出的输入文件 步骤 2:保存后用 UltraEdit_32 重新打开,如图 2.3.8 所示。
图 2.3.8 制作触发栈溢出的输入文件 步骤 3:将 UltraEdit_32 切换到十六进制编辑模式,如图 2.3.9 所示。
图 2.3.9 制作触发栈溢出的输入文件 步骤 4:将最后 4 个字节修改成新的返回地址,注意这里是按照“内存数据”排列的,由于“大顶机”的缘故,为了让最终的“数值数据”为 0x00401102,我们需要逆序输入这 4 个字节,如图 2.3.10 所示。
图 2.3.10 制作触发栈溢出的输入文件 步骤 5:这时我们可以切换回文本模式,最后这 4 个字节对应的字符显示为乱码,如图 2.3.11 所示。
图 2.3.11 制作触发栈溢出的输入文件 将 password.txt 保存后,用 OllyDbg 加载程序并调试,首先可以看到成功绕过密码验证:
我们再回头看一下最终的栈状态:authenticated被覆盖为 0x0040106C;EBP被覆盖为0x31323334,返回地址被覆盖后为0x00401102(正好为验证成功的地址)
可以看到最终的栈状态如表 2-3-4 所示。
表 2-3-2 栈帧数据 局部变量名 内存地址 偏移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 0x00 0x40 0x10 0x6C 前栈帧 EBP (被覆盖前) 0x0012fb20 0x00 0x12 0xFF 0x80 前栈帧 EBP (被覆盖后) 0x0012fb20 0x31(‘1‘) 0x32(‘2‘) 0x33(‘3‘) 0x34(‘4‘) 返回地址(被覆盖前) 0x0012fb24 0x00 0x40 0x10 0xE7 返回地址(被覆盖后) 0x0012fb24 0x00 0x40 0x11 0x02 VC6.0中,重新编译执行。程序执行状态如图 2.3.12 所示。
图 2.3.12 栈溢出成功改变了程序执行流程 由于栈内 EBP 等被覆盖为无效值,使得程序在退出时堆栈无法平衡,导致崩溃。虽然如 此,我们已经成功地淹没了返回地址,并让处理器如我们设想的那样,在函数返回时直接跳转到了提示验证通过的分支。
Preference
http://www.cnblogs.com/0831j/p/9219081.html
https://github.com/walkerfuz/writeups/blob/master/books/0day_security_second_edition.md
疑惑
- 表 2-3-2 栈帧数据,authenticated被覆盖后的值为什么不是 0x31323334 ?
以上是关于2.3 修改函数返回地址的主要内容,如果未能解决你的问题,请参考以下文章