通过一次自制实验一步步入门格式化字符串漏洞——记一次学习32位ELF文件格式化字符串漏洞原理的实验-详细分析

实验环境:vscode+wsl+kali_linux+pwngdb+pwndbg
参考博文:使用wsl+kali_linux搭建pwn环境
参考博文:CSDN-_n19hT(作者)-gdb调试 | pwndbg+pwndbg联合使用

一、实验前准备

(一)程序源代码

分析的目标程序的源码:

//p2-1.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int format_string()
{
    char str[12];
    scanf("%s",&str);
    printf("The string is :");
    printf(str);
    return 0;
}
int main()
{
    format_string();
    return 0;
}

(二)编译可执行程序

参考博文-IT宝库-编程技术问答 其他开发-小编(作者)-在Ubuntu的32位上交叉编译64位程序时,缺少include “bits/c++config.h”
参考博文-Gitbook-CTF All In One(作者)-4.4 GCC 堆栈保护技术

1.无法正确编译

使用命令g++ p2-1.cpp -o p2-1_leak_32 -m32编译32位ELF文件,出现下述报错。
在这里插入图片描述

2.解决方案:

执行命令:sudo apt-get install gcc-multilib g++-multilib
sudo apt-get install gcc-4.8-multilib g++-4.8-multilib(本文用前一个解决问题,后一个则不行)

3.为便于分析程序,添加编译选项

(1)关闭随机化和栈保护

为了方便分析程序,将程序所有的保护关闭(主要是PIE、但是,嗯、还是全关掉吧),使用下述命令:
g++ p2-1.cpp -o p2-1_leak_32 -m32 -fno-stack-protector -z execstack -no-pie -z norelro

(2)限制地址无关选项

如果不打算阅读第二、(一)节的内容,可直接使用下面的命令,否则可以先用上面条目的gcc命令。
但是实验中最终使用的命令为:g++ p2-1.cpp -o p2-1_leak_32 -m32 -fno-stack-protector -z execstack -no-pie -z norelro -fno-pic(是在试验过程中发现有问题改的)
本文输出的可执行文件名称为p2-1_leak_32
在这里插入图片描述
注意这里的LSB,下面会用到
二、(一)节的内容非常的基础,非常的啰嗦,有基础建议不看=_=

二、实验过程

(一)分析程序运行时栈空间布局

注意,本文实验过程中修改过两次源代码,导致反汇编代码中main函数部分地址在文中多次发生变化,者将会影响本节内容的栈空间布局示意图的最底部format_string的return address的值多次发生变化,即本文中此处的地址不一定是准确的,但是由于分析时很少有地方涉及到该地址的问题,因此本文不做更正。

下面每个条目将展示每次栈空间发生变化时的栈空间的示意图

1.

开启gdbpwngdb调试
在这里插入图片描述
从红色划线位置(当前位置)执行gdbni命令到达0x80491e6 <main+16> call format_string() <format_string()>指令处
在这里插入图片描述
gdb命令si步入format_string函数
在这里插入图片描述
注意此时call指令的下一条指令为0x080491eb,由于call指令可以被视作push {next instruction's address}+move eip {callee function's address},所以执行完call指令后,栈顶的内容会变为0x08491eb本文接下来的内容,对栈的分析将会以此处栈的地址(0xffffc6ac)作为被分析的栈的部分的最底部位置

下图中划红线位置标记的均为main函数中call指令下一条指令的地址,即为format_string函数的返回地址
在这里插入图片描述
BACKTRACE显示的为函数调用关系,即gdbbt命令,当前函数的当前一行记录着当前函数的起始地址,下面一行即记录着当前函数的恢复点(return address)
在这里插入图片描述
当前栈空间示意图如下图:
请添加图片描述

啰嗦几句:(关于地址写在栈右侧的原因,上文提到过,此可执行文件为LSB小端序,低字节放在地址低位,对于栈底部的return address:0x080491eb来说,以字节(byte)为单位,0x080x040x910xeb分别在地址0xffffc6af0xffffc6ae0xffffc6ad0xffffc6c,为避免0xfffc6ac写在左侧造成误解,错误认为0xffffc6ac指向字节0x08,此处将地址写在栈右侧)

为什么高地址在下面:
在这里插入图片描述
上图可以看出stack是由高地址向低地址增长,但是由于我们阅读文件时,文件往往是从地址0开始的,因此很自然的高地址在下。(个人习惯,也有人习惯从上向下画栈)

1.5

执行完push ebp后:我们看到几条很令人迷惑的指令:
在这里插入图片描述
下面是对这几条指令的解释:
参考书籍:《程序员的自我修养——链接、装载与库》-俞甲子(作者)-P192-P193(类型二:模块内部数据访问)(pdf版下载)
提取码:1111
在这里插入图片描述

在这里插入图片描述

使用命令:g++ p2-1.cpp -o p2-1_leak_32 -m32 -fno-stack-protector -z execstack -no-pie -z norelro -fno-pic
再次使用gdb调试,进入到format_string函数后,反汇编代码如下图所示
在这里插入图片描述

2.

执行完上图中的mov ebp, esp指令,栈空间示意图如下:
请添加图片描述

3.

ni执行完sub esp, 0x18指令后
在这里插入图片描述
栈空间示意图如下:
请添加图片描述

4.

接下来是对scanf函数的调用,执行过画圈区域的指令,栈空间如图所示:

在这里插入图片描述
请添加图片描述

5.

push eax
push 0x804a008
请添加图片描述

6.

此处再多思考一下,这次被压入栈中的是什么内容呢?——自上而下应该是自左向右的两个参数,因此0x0804a008就应该是字符串%s,而0xffffc694就是字符串数组str`的起始地址,因此更新栈的示意图如下图所示:
请添加图片描述

7.

在调用完scanf函数后,输入如下图蓝色划线处所示为字符串AAAABBBB
在这里插入图片描述
此时查看栈空间,发现原先0xffffc694的位置已经被新输入的字符串的16进制ascii码所替代
在这里插入图片描述
此时栈空间布局示意图:
请添加图片描述
栈空间0xffffc684的位置已经被写入了0xffffc694(字符串数组的地址),既不作为第一个print

8.

执行接下来两条指令:add esp, 0x10,sub esp, 0xc
在这里插入图片描述
栈空间布局示意图:
请添加图片描述

9.

执行push 0x84a00b
在这里插入图片描述
请添加图片描述

10.

在第二个call printf设下断点,接下来正式开始分析格式化字符串漏洞

请添加图片描述

(二)格式化字符串漏洞分析

1.泄露栈上信息

(1)为什么使用形如%x%x%x%x%x%x%x%x的格式化字符串可以泄露栈上信息 && 有什么样的规律

接下来就是执行printf函数按顺序打印printf参数,即当前栈顶(第一个参数)对应的内容(输入的字符串数组str的内容)。
思考:什么因素决定了printf函数参数的个数?——printf函数中,直接传入的变量或常量的个数、printf参数中格式化字符的个数,如%x。而正常来说,如果源代码使用printf("%x",buf),那么在汇编代码中执行完call printf指令后,如果buf是一个字符串,那么字符串”%x“的起始地址会作为printf的第一个参数被放入当前的栈顶,buf的起始地址就会作为printf的第二个参数放入esp+4的位置,printf的输出结果为esp+4的内容(buf的16进制起始地址)。
而如果源代码中使用这样的代码:printf("%x")——仅使用了格式化字符,而没有传递buf,那么字符串”%x“的起始地址仍会作为printf的第一个参数被放入当前的栈顶,但是printf并没有实际上真实存在的第二个参数,所以printf函数打印了esp+4的4个byte内容的16进制格式。如果是printf("%x%x")就打印esp+4起栈顶部8个byte内容16进制,每4个byte对应一个%x(参数)。以此类推,如果输入没有限制输入,只要%x足够多,理论上可以将栈上内容全部输出。
是这样吗?并非往往如此,如下图,如果字符串数组初始化的大小过小,比如按照本实验中一开始给出的源码中,定义str数组时为:char str[]12];,在这样的情况下,如果输入为%x%x%x%x%x%x%x%x,即8个%x
在执行printf时,栈上布局如下图所示
在这里插入图片描述

此时函数的输出如下
在这里插入图片描述
即打印出了从esp+4(printf第二个参数的起始位置)开始,8个word(1word=4byte)的内容,其中从0xffffc694开始为str数组的内容,为8个%x0x2578(ps:我们看到的是0x7825,这是因为在小端序LSB的布局中数值的低位放在地址高位,上图中的地址高位在左侧而非右侧)举个例子,当输入为ABCD时栈的内容如下
(字符串的内容为一个byte一个byte读入内存,而整数是每4byte读入内存,因此可以看到下图所示栈中内容中,每四个byte的空间里,字符串的内容是从右向左的顺序为正确语义,而数字的内容直接可以从左向右读,就是正确的地址信息。)

在这里插入图片描述
A在最右侧)

上面提到:(上上张图)str中的第一个%x就处于printf第一个参数的位置,str中的第二个%x处于printf第二个参数的位置,以此类推,按照第一个参数打印栈顶esp+40xffffc684)起1个word(4byte)的内容(第二个0xffffc694),第二个参数打印esp+8(0xffffc688)起1word的内容(0xf7d97482)。因此输入为8个%x时,输出一直能输出到0xffffc6a0起的4byte的0x78257825

(2)使用形如%x%x%x%x%x%x%x%x的格式化字符串有什么样的限制

但是这也差不多就是极限了,再多输入一个%x,就占满ebp0x0xffffc6a8)之前的所有的栈空间,注意,字符串数组默认结尾会有1byte的’\0’(0x00),再剩下的1byte会按照内存对齐规则对齐。
在这里插入图片描述

而如果输入为10个%x。在执行printf函数时,栈空间如下:
在这里插入图片描述

原先栈底(ebp0xffffc6a8))的值0xffffc6b8被改为了0xffffc600(00即为\0的16进制acsii码),这时format_string函数的ebp被篡改了,不能够正常返回,程序会异常退出,则此程序不能够通过构造足够多的%x这样的str字符数组来泄露更多的信息。
请添加图片描述
在这里插入图片描述

(3)使用形如%10$x%10$p的格式化字符串实现对栈上信息的任意地址读

没有其他的方法了吗?
参考博文-知乎-合天网安实验室_紫色仰望合天智汇(作者)格式化字符串漏洞及利用_萌新食用
有的:可以通过%10$x%10$p这样的%k$x样式的格式化字符,就可以实现栈上任意位置的信息泄露(任意地址读)。那么问题来了。怎么得到k的大小呢:设某一地址到esp的偏移量为x个word,那么k = x(x>=1),%k$x的含义即为:输出从栈顶起算,第k个参数(esp+4*(k-1)(该参数为一个地址)(k>=2))所对应的内容的16进制格式。
再看下下图所示的栈空间,printf第二个参数的内容为0xffffc694(k = 0 x f f f f c 684 − 0 x f f f f c 680 4 = 1 \frac{0xffffc684-0xffffc680}{4} = 1 40xffffc6840xffffc680=1),ebp处在第11个参数的位置(k = 0 x f f f f c 6 a 8 − 0 x f f f f c 680 4 = 10 \frac{0xffffc6a8-0xffffc680}{4} = 10 40xffffc6a80xffffc680=10),format_string函数的返回地址处在第12个参数的位置(k = 0 x f f f f c 6 a c − 0 x f f f f c 680 4 = 11 \frac{0xffffc6ac-0xffffc680}{4} = 11 40xffffc6ac0xffffc680=11)。
请添加图片描述

这次再次将输入换为下图划线位置所示内容
在这里插入图片描述
(如果之前讲述的内容都搞清楚了,就不会两个格式化字符串之间的-字符与打印结果的关系这个问题上存在疑惑,如果不存在疑惑,则忽略本句话)

按照如上分析,程序的输出应该为下图中划线的内容
在这里插入图片描述
确实如此。

在这里插入图片描述

2.篡改栈上信息

(1)使用形如%10$n的格式化字符串实现对栈上信息的任意地址写

接下来尝试篡改栈上某一地址的内容,通过任意地址写的命令%k$n(注意前一个k和上面所说的%k$x中的k含义相同,为偏移量,后一个n与%n$x中的x含义相同,是一种格式化字符串的格式。)%k$n的含义为:将从栈顶起算,第k个参数(esp+4*(k-1)(该值为一个地址)k>=2))所对应的内容(把这个参数所在地址中的内容)修改为%k$n之前的格式化字符串中的字符数量的值,下面举个例子:
将输入改为下图所示内容
在这里插入图片描述
分析一下:由于第二个参数对应k=1,其内容为0xffffc694%1$n之前的字符串的长度为4,那么AAAA%1$n的作用应该是:将0xffffc694地址处的内容改为4。
在执行第二个printf之前,0xffffc694的内容为0x41414141
在这里插入图片描述
执行gdb命令ni执行完第二个printf函数后,再次查看栈,0xffffc694的内容被改为为0x4
在这里插入图片描述
再试一个:
在这里插入图片描述
输入为AAAAAAAA%1$n时,0xffffc694的内容被改为为0x8
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值