2.3 修改函数返回地址

Posted narisu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2.3 修改函数返回地址相关的知识,希望对你有一定的参考价值。

2.3 修改函数返回地址

2.3.1 返回地址与程序流程

  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 码覆盖掉这些栈帧状态值。

    按照上面对栈帧的分析,不难得出下面的结论。

    1. 输入 11 个‘q’,第 9~11 个字符连同 NULL 结束符将 authenticated 冲刷为 0x00717171。运行情况如图所示。技术分享图片
    2. 输入 15 个‘q’,第 9~12 个字符将 authenticated 冲刷为 0x71717171;第 13~15 个字符连同 NULL 结束符将前栈帧 EBP 冲刷为 0x00717171。运行情况如图所示。技术分享图片
    3. 输入 19 个‘q’,第 9~12 个字符将 authenticated 冲刷为 0x71717171;第 13~16 个字将前栈帧 EBP 冲刷为 0x71717171;第 17~19 个字符连同 NULL 结束符将返回地址冲刷为 0x00717171。运行情况如图所示。技术分享图片
  2. 在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‘)
  3. 前面已经说过,返回地址用于在当前函数返回时重定向程序的代码。在函数返回的“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小实验(以后若无特殊声明,均以此环境为准)

如果完全采用实验本文的实验环境,将精确地重现指导中所有的细节,否则需要根据具体情况重新调试。

实验步骤

  1. 用 VC6.0 将上述代码编译链接(使用默认编译选项,Build 成 debug 版本),在与 PE 文件 同目录下建立 password.txt 并写入测试用的密码“1234567”,测试程序是否正确。

    技术分享图片

    技术分享图片

  2. 之后,就可以用 OllyDbg 加载调试了。

    开始动手之前,我们先理清思路,看看要达到实验目的我们都需要做哪些工作。

    1. 要摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。这虽然可以通过分析代码得到,但我还是推荐从动态调试中获得这些信息。
    2. 要得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行。
    3. 要在 password.txt 文件的相应偏移处填上这个地址。

    这样 verify_password 函数返回后就会直接跳转到验证通过的正确分支去执行了。

  3. 首先用 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 栈溢出攻击示意图
  4. 仍然出于字节对齐、容易辨认的目的,我们将“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

疑惑

  1. 表 2-3-2 栈帧数据,authenticated被覆盖后的值为什么不是 0x31323334 ?

以上是关于2.3 修改函数返回地址的主要内容,如果未能解决你的问题,请参考以下文章

2.3-2.5 进程创建+虚拟地址空间+GDB多进程调试

Python闭包和装饰器

Python ❀ 函数

20155306 白皎 免考实践总结

201555332盛照宗—网络对抗实验1—逆向与bof基础

getActivity() 在片段上返回 null?