pwn_格式化字符串漏洞
pwn_格式化字符串漏洞初步
格式化字符串漏洞指的是在类似于:
1 | char test[100]; |
这样的漏洞其核心就在于,printf函数的错误使用使得攻击者可以输入带有格式化字符串的内容,导致内存的泄露,可能造成任意读,任意写的结果,影响巨大。
0x01 格式化字符串语法基础
一、常见语法
c语言中常见的格式化输出:
- %d - 十进制 - 输出十进制整数
- %s - 字符串 - 从内存中读取字符串
- %x - 十六进制 - 输出十六进制数
- %c - 字符 - 输出字符
- %p - 指针 - 指针地址
- %n – 把前面打印过的字符长度输出到指定地址
- %N$ - 第 N 个参数
二、危险函数
容易出现漏洞的函数:
函数 | 基本介绍 |
---|---|
printf | 输出到 stdout |
fprintf | 输出到指定 FILE 流 |
vprintf | 根据参数列表格式化输出到 stdout |
vfprintf | 根据参数列表格式化输出到指定 FILE 流 |
sprintf | 输出到字符串 |
snprintf | 输出指定字节数到字符串 |
vsprintf | 根据参数列表格式化输出到字符串 |
vsnprintf | 根据参数列表格式化输出指定字节到字符串 |
setproctitle | 设置 argv |
syslog | 输出日志 |
err, verr, warn, vwarn 等 | 。。。 |
0x02 格式化字符串漏洞的利用
一、程序崩溃
1 | %s%s%s%s%s%s%s%s%s%s%s%s%s%s |
这个就是在栈上一个一个往后读,视为一个一个地址去找字符串,如果遇到一个不合法地址,程序就会崩溃。
二、读栈内容
在正常的进行printf调用时,比如语句是这样的:
printf("hello %d %d %s",num1,2,str)
,其栈区内容会包括:
- printf函数返回地址;——低地址
- “hello %d %d %s”这个字符串的地址;
- num1的值;
- 数字2;
- str的地址;——高地址
但在不正常的调用时,printf函数会把栈上原有的内容当作参数。
也就是,我们可以一点一点把栈上的内容打印出来(先别管有啥用,问就是泄露),但是这样我们不太满意,比如说,我们只想要”hello %d %d %s”这个字符串的地址往后五个那个位置的内容,怎么办?
我们可以使用%n$x
语法,其中n是一个数字,我们要输出第五个,填5就行,x表示输出的格式是十六进制数,总之,这样的语法的意思就是把第五个参数按照十六进制数输出。
因此,最直接的漏洞就是我们可以输出栈上的内容,比如:
- 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
- 利用 %s 来获取变量所对应地址的内容,只不过有零截断,若对应地址没有内容则程序会崩溃。
- 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。
由于这样读参数的方法(计算参数order的方法),是从低地址向高地址计算,与栈的增长方向相反(我们遍历参数的方向是向高地址遍历,而栈的增长是向低地址增长,所以在这之前记录在栈上的内容是全都能读的),也就是,我们拥有了读栈上一切内容的能力,但是仅仅这样是不够的。
三、任意读
我们要做到知道一个地址就能读,而不是只能在栈上的那一点偏移上读,怎么办。
我们换一个思路,如果要读一个地址,我们肯定要输入一个地址,那我们能控制的输入就只有那个格式化字符串,那个地址肯定是存在格式化字符串里的。
要实现任意写,我们使用的payload是:
1 | addr%k$s |
具体利用思路如上图所示,左边为正常调用时的栈情况,右边是我们在做任意读的时候的利用方式,我们只需要用某种手段,确定图中的k值,也就是prinf函数的第一个参数,与其字符串储存位置的偏移,就可以基于这个偏移,把addr当作我们的第k个参数,然后将其作为一个字符串格式化字符串,打印出addr处的内容,实现任意读。
我们要确定这个k值,只需要使用和格式化输入[tag]-%p-%p-%p-%p-%p-%p..
来判断偏移,从地址开始依次向上输出栈中的内容,直到输出了我们的tag内容,就能确定经过的k偏移了。
这里举一个printf泄露libc的例子:
1 | from pwn import |
四、在栈上写(假任意写)
要对栈上的内容进行覆盖,我们使用的payload是:
1 | [addr of c]%012d%6$n |
我们的目的是要把变量c的值写为16(例子)。如右图,第要被写入的变量c的地址被视为第 k个参数,然后我们使用n指令进行写入,但是如果放任不管,那我们写入的就是4,也就是c的值会被写为4。(之所以是4,是因为c地址转化为字符串长度是4)
然而我们添加%012d,就是在这之前先输出一个长为12的整数,%012d表示输出长为12的整数,不足补0。
代码如下:
1 | def forc(): |
仔细想一想,我们实现的并不是在栈上写,只要拥有地址和偏移量,我们好像就可以实现任意写,但是这里存在的问题是,我们写入的数据是极其有限的,比如我们上面的例子中,如果我们要写0、1、2、3就不行,因为初始的地址长度就是4,又或者我们想写入的也是一个地址(即一个很大很大的整数),这时候我们是否要先输出几十万位的填充位来凑n呢,很明显不太考究。
五、真任意写
简单的讨论一下,如何解决上面提到的数值过小和过大的情况:
写小数字:
简单的说,思路就是把地址放在%n后面就可以了,偏移自己算;
写大数字:
不想看了,直接贴脚本
1 | def fmt(prev, word, index): |
1 | def forb(): |
剩下的区域未来在探索吧…