Format String(格式化字符串)漏洞

作者:zts

原文地址:https://secvul.com/topics/1125.html


前言

格式化信息有某种字符串来描述,称为格式化字符串。格式化字符串漏洞的根源在于未对用户提供的输入进行验证。

原理

格式化信息有某种字符串来描述,称为格式化字符串。(格式化字符串是用一种功能有限的数据处理语言来描述,以便于描述输出的格式)
格式化字符串漏洞的根源在于未对用户提供的输入进行验证。参数个数不定的常见函数集合是printf函数族:printf,sprintf,snprintf,fprintf,vprintf等。

C 库函数 – printf():C 库函数 int printf(const char format, …) 发送格式化输出到标准输出 stdout。
printf()函数的调用格式为:printf(“<格式化字符串>”, <参量表>);
int printf(const char format, …)
format – 这是字符串,包含了要被写入到标准输出 stdout 的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier,具体讲解如下:

《Format String(格式化字符串)漏洞》
具体原理:当printf在输出格式化字符串的时候,会维护一个内部指针,当printf逐步将格式化字符串的字符打印到屏幕,当遇到%的时候,printf会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。甚至由于%n的问题,可导致任意地址读写。

案例 – 编译一道简单的Pwn的题目(X64编译)

源代码如下:

《Format String(格式化字符串)漏洞》
使用gcc编译一下,生成pwn.out文件:

《Format String(格式化字符串)漏洞》
虽然爆出了一个警告,但是仍然可以使用。好像是因为gcc版本高一点的会在编译的时候检查里面的语法,好像也可以关闭这种检查…有强迫症的可以自行查一下0.0.

-fstack-protector:启用堆栈保护,不过只为局部变量中含有char数组的函数插入保护代码; -fstack-protector-all:启用堆栈保护,为所有函数插入保护代码; -fno-stack-protector:禁用堆栈保护;

顺便提一下我编译的环境:

《Format String(格式化字符串)漏洞》
运行一下:

《Format String(格式化字符串)漏洞》
运行没有问题,我们发现输入的数据都被解释成了格式化字符串,这个时候就要测试一下有没有Format String漏洞,我们静态看一下:

《Format String(格式化字符串)漏洞》
看汇编不明显,我们用堆栈图表示:

《Format String(格式化字符串)漏洞》
这里我们还原一下c语言的关键点:

int a int* p(p是指向int型变量的指针) a = 0 p = &a(p指向a变量的int型指针,修改*p就相当于修改a)

在这里我们看到一个比较(cmp),如果a=2000,就能拿到flag。前面提到了修改p就相当于修改a,我们没有办法直接修改a的值(原因:)所以我们就只能修改p(p地址里面的数据),我们算一下我们需要多少个参数才可以到这个位置:

(112-8)/8=13

通常利用格式化字符串漏洞写入内存的说明符是%n,它可以向给定变量的地址写入当前已写入的字符个数,作为相应的参数,所以我们的利用代码应该是:
%2000x%13$n
但是最后的效果不是很好。

《Format String(格式化字符串)漏洞》
Segmentation fault (core dumped)多为内存不当(空指针、野指针的读写操作,数组越界访问,破坏常量等)操作造成。
想想不知道哪里出问题了,就只能一个一个来打印一下试试:

《Format String(格式化字符串)漏洞》
(%x:从栈中读取数据,每次四个字节,%s:打印参数指向地址里面的内容。%p:不仅显示栈,还会显示程序是32位还是64位,打印出参数的内容)
看不出来什么情况…动态调试一下,当我们调试到:

《Format String(格式化字符串)漏洞》
我们看到现在寄存器的值如下:

《Format String(格式化字符串)漏洞》
最终运行的结果如下:

《Format String(格式化字符串)漏洞》

0x1 0x7fe4c61e9790 0xa (nil) 0x7fe4c6409700 0x7025702570257025 0x702570257025

这个时候才想起来,在X86-64中,所有寄存器都是64位,%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数…如果函数的参数个数超过6个,则超过的参数直接使用栈来传递。所以应该是13+6=19个参数,所以利用的代码应该是:%2000x%19$n,我们试验一下:

《Format String(格式化字符串)漏洞》
嗯,到此为止,已经可以了。

后记

网上出现的都是X86编译的,X64编译的有一点坑需要踩…
补救措施也很简单:
printf(“s%”, a)
PS:尽量不要使用printf族的函数

最后给一个来源于《软件安全24宗罪》的提示:
要使用固定的格式化字符串,或者来自可信源的格式化字符串。
要检查并限定locale的请求为有效值。
要注意编译器给出的警告和错误(我前面在编译的时候出现过那个警告,虽然也编译成功了,但是很多不注重安全,只注重效果的大佬们可能赶项目,只注重功能的开发)
不要将用户输入直接作为格式化字符串传给格式化函数。
考虑使用受此漏洞影响较小的高级语言。

点赞

发表评论