本博客已经弃用,我的新博客地址:http://jujuba.me/
How printf works——浅谈格式化串漏洞
0x00 简介
格式化字符串漏洞由来已久,其主要原因是程序员在使用printf时未将格式化串的个数
和变量的个数相匹配,给了黑客们可乘之机。
我们可以先来看看printf函数的声明
int printf (const char *__format, ...)
很奇怪吧,printf的argument中有一个...
,这表示printf的argument的个数
是没有限制的,写多少个都没事。
比如下面这段代码是可以成功运行的,用gcc编译不会出现任何错误。
int main(){
char * s = "hello";
printf(s,s,s);
}
0x01 原理
printf函数和一般函数不太相同,因为它的argument(包括格式化串和变量)可以是任意
个,那么当程序被编译成二进制代码之后,CPU会知道格式化串和变量的分界线吗?肯定是
不知道的。假如程序员在写程序时使用了printf(something)
这种写法的话,相
当于没有限定自己的格式化串,那么我们就可以自己输入格式化串,此时CPU就会被误导,
以为自己要往高地址方向读很多个变量,从而我们就可以读出栈上原本我们看不到的数据。
int id = 1000,age = 25;
char * name = "Bob Smith";
printf("ID: %d, Name %s, Age %d\n",id,name,age);
在这段代码里,我们可以看到格式化串共有三个,分别是%d,%s和%d,在printf
函数执行的过程中,会有一个指针依次遍历这三个格式化串。当指针指向第一个%d的同时
另一个指针指向id,也就是图中的1000;当指针指向%s时,另一个指针指向图中的0x5000
这里我们将0x5000假设为字符串Bob Smith
的首地址。
可以看出,格式化串和变量的个数应该是一一对应的,但是假如格式化串的个数多于变量
的个数呢,那么栈中的指针会继续往内存的高地址方向指,甚至一直读到这个函数的返回
地址。
比如我们定义的是
printf("ID: %d, Name %s, Age %d\n",id,name);
那么当程序运行时,它就会读出与name相邻的数据,而在正常情况下,这个数据我们是读
不出来的。
0x02 应用
1.读内存
我们可以通过一个小例子来实践一下:
#include <stdio.h>
int main(int argc,char** argv)
{
printf(argv[1]);
return 0;
}
编译并运行:
a.exe %p,%p,%p,%p
00000000,7FFDE000,00400080,0029FF1C
可以看到,我们成功地打印出了栈中原本我们看不到的信息。
2.写内存
在printf函数中,有一个鲜为人知的控制符%n,它可以将输出的内容写回到一个变量中去。
具体例子:
int main(){
int b = 0;
printf("hello%n",&b);
printf("\n%d\n",b);
}
这段代码执行之后,可以看到b的值被修改成了5,这是因为%n控制符会将字符串hello的
长度写回到变量b中。
可以看出,格式化串漏洞可以改写内存中的任意数值,这对于修改函数返回地址,劫持
进程,执行shellcode等都是具有很大的帮助的。
但是,由于字符串漏洞的原因比较单一,而且容易被静态检查,在编译器不断优化的今天,
如vs就默认禁用了printf中的%n控制符,所以格式化串漏洞的应用相对较少。
参考资料
2.《0day安全:软件漏洞分析技术》