实验环境: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.
开启gdb
或pwngdb
调试
从红色划线位置(当前位置)执行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
显示的为函数调用关系,即gdb
的bt
命令,当前函数的当前一行记录着当前函数的起始地址,下面一行即记录着当前函数的恢复点(return address)
当前栈空间示意图如下图:
啰嗦几句:(关于地址写在栈右侧的原因,上文提到过,此可执行文件为LSB小端序,低字节放在地址低位,对于栈底部的return address:0x080491eb
来说,以字节(byte)为单位,0x08
、0x04
、0x91
、0xeb
分别在地址0xffffc6af
、0xffffc6ae
、0xffffc6ad
、0xffffc6c
,为避免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个%x
(0x2578
(ps:我们看到的是0x7825
,这是因为在小端序LSB的布局中数值的低位放在地址高位,上图中的地址高位在左侧而非右侧)举个例子,当输入为ABCD时栈的内容如下
(字符串的内容为一个byte一个byte读入内存,而整数是每4byte读入内存,因此可以看到下图所示栈中内容中,每四个byte的空间里,字符串的内容是从右向左的顺序为正确语义,而数字的内容直接可以从左向右读,就是正确的地址信息。)
A在最右侧)
上面提到:(上上张图)str
中的第一个%x就处于print
f第一个参数的位置,str
中的第二个%x
处于printf
第二个参数的位置,以此类推,按照第一个参数打印栈顶esp+4
(0xffffc684
)起1个word(4byte)的内容(第二个0xffffc694
),第二个参数打印esp+8
(0xffffc688
)起1word的内容(0xf7d97482
)。因此输入为8个%x
时,输出一直能输出到0xffffc6a0
起的4byte的0x78257825
。
(2)使用形如%x%x%x%x%x%x%x%x
的格式化字符串有什么样的限制
但是这也差不多就是极限了,再多输入一个%x
,就占满ebp
(0x0xffffc6a8
)之前的所有的栈空间,注意,字符串数组默认结尾会有1byte的’\0’(0x00
),再剩下的1byte会按照内存对齐规则对齐。
而如果输入为10个%x。在执行printf函数时,栈空间如下:
原先栈底(ebp
(0xffffc6a8
))的值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
40xffffc684−0xffffc680=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
40xffffc6a8−0xffffc680=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
40xffffc6ac−0xffffc680=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