格式化字符串漏洞
漏洞原理
格式化字符串是一种很常见的漏洞,其产生根源是printf函数设计的缺陷,即printf()函数并不能确定数据参数arg1,arg2…究竟在什么地方结束,也就是说,它不知道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数format后面地址的内容
格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配
。
如图所示,执行的命令为printf("%s %d %d %d %x\n",buf, 1, 2, 3)
,紧随格式化字串后压入栈上的参数为4个,但格式化字串有五个参数,printf
在解析第五个参数%x
时,会继续往栈上读取,造成了信息泄露:
常见格式化字符串漏洞函数
格式化字符串符号说明:
转换指示符
字符 | 类型 | 使用 |
---|---|---|
d | 4-byte | Integer |
u | 4-byte | Unsigned Integer |
x | 4-byte | Hex |
s | 4-byte | ptr String |
c | 1-byte | Character |
长度
字符 | 类型 | 使用 |
---|---|---|
hh | 1-byte | char |
h | 2-byte | short int |
l | 4-byte | long int |
ll | 8-byte | long long int |
%n:将%n之前printf已经
打印的字符个数
赋值给偏移处指针所指向的地址位置,如%100x%10n表示将0x64写入
偏移10处保存的指针所指向的地址(4字节),而hn表示写入的地址空间为2字节,%hhn表示写入的地址空间为1字节,lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%hn或hhn来适时调整。
%n是通过格式化字符串漏洞改变程序流程的关键
方式,而其他格式化字符串参数可用于读取信息或配合%n写数据
#include <stdio.h> int main(void) { int a; printf("aaaaaaa%n\n",&a); printf("%d\n",a); return 0; }复制代码
判断是否存在漏洞
拿到一个程序之后可以通过输入若干个%s
来进行判断是否存在漏洞
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s复制代码
因为如果存在格式化字符串漏洞。在输入一串%s后,就会把对应地址的内容当做指针 来打印该指针指向的地址空间的字符串。然而并不是所有空间都具有可读权限,这样的话,就会导致程序崩溃。
泄露内存
由于格式化字符串变长参数的特性,在实际运行中,如果Format String的符号说明个数超过待匹配的参数个数,即有更多的符号说明需要被匹配时,printf会根据解析结果和调用约定去取栈上(reg)相应的值并输出。
//leakmem #include <stdio.h> int main() { char s[100]; int a = 1, b = 0x22222222, c = -1; scanf("%s", s); printf("%08x.%08x.%08x.%s\n", a, b, c, s); printf(s); return 0; }复制代码
我们输入:%08x.%08x.%08x
得到的结果如下图:
我们来调试一下:
首先第一个printf的参数如下
%08x.%08x.%08x
是作为%s相对应的参数的,所以他的打印结果如下:
第二个printf的参数对应如下:
很奇怪的是 当程序结束后才会打印出来
我们不只是可以%x%x%x来泄露数据,还可以用%p
如果还想输出特定位置的内容还可以用%n$x
,这里的n为一个自然数,代表第n位。
输入%3$x
,得到:
任意地址泄露
任意地址读需要用到printf的另外一个特性,操作符.这个操作符可以输出指定位置的参数.利用操作符.这个操作符可以输出指定位置的参数.利用x这样的字符串就可以获得对应的第n+1个参数的数值(因为格式化参数里边的n指的是格式化字符串对应的第n个输出参数,那么相对于输出函数来说就成了第n+1个).
代码:
//gcc test.c -o test -m32 #include <stdio.h> int main(void) { char str[100]; scanf("%s",str); printf(str); return 0; }复制代码
首先测一下字符串开头的偏移量:
➜ / ./test AAAA%1$x AAAAffdb0848# ➜ / ./test AAAA%2$x AAAAc2# ➜ / ./test AAAA%3$x AAAAf7e998fb# ➜ / ./test AAAA%4$x AAAAffdefece# ➜ / ./test AAAA%5$x AAAAffa838dc# ➜ / ./test AAAA%6$x AAAA41414141#复制代码
point : 实际偏移量
注意64位算偏移时要先将调用约定中寄存器的数量加进去,并且payload里地址之前的其他格式化字符串也会在栈上占去位置,会导致实际偏移量增加,需要进行新的计算,假如无法确定%K$n中的K到底是多少,可以多输出几个%p来确定。
如果将AAAA替换成某个函数的got地址,那么程序就会打印这个函数的真实地址
内存覆盖
内存覆盖的思路和任意地址泄露的思路相似,一种可行的方法是同上面一样,构造payload,在格式化字符串中包含想要写入的地址,此时该地址会随格式化字符串放在栈上,然后用格式化字符串的'%K$n'来实现写功能。
利用%n改写
函数:
#include <stdio.h> int main() { int flag = 30;# 0x1e 有很多文章都将flag设成了0,但是系统中有很多为0的内存 容易混淆 int *p = &flag; char a[100]; scanf("%s",a); printf(a); if(flag == 2000) { printf("good!!\n"); } return 0; } #编译后程序是附件test32复制代码
在图中 明显的参数0x1e为第5个
所以我们输入的是:%.2000x%5$n
结果如下:
%.2000x
是以2000对其的方式输出某地址%n
是写入,本身也是记录输出个数;%5$n
是写入第5个常数。
所以%2000x%5$n
是将栈中第5个刚好修改为2000-参考文献
作者:HBhan
链接:https://juejin.cn/post/7015846184522776613