*printf()格式化串安全漏洞分析(上)
测试平台:RedHat 6.1, RedHat 6.2 (Intel i386)
前言:
=====
最近一段时间,一种新的安全漏洞正开始引起人们注意,就是诸多的*printf()函
数的格式
化串问题。其实这个问题应该说并不鲜见,只是一直没有人注意它,直到最近才
开始进行
一些深入的讨论。格式化串的问题实际上是由于程序员编程时的疏漏所导致的,
下面我们
就来看看具体是怎么回事。
关于格式化串
============
*printf()函数包括printf, fprintf, sprintf, snprintf, vprintf,
vfprintf,
vsprintf, vsnprintf等函数,它们可以将数据格式化后输出。以最简单的
printf()为例:
int printf(const char *format, arg1,arg2,...);
通过定制format的内容(%s,%d,%p,%x...),用户可以将数据按照某种格式输出。问
题是,
*printf()函数并不能确定数据参数arg1,arg2...究竟在什么地方结束,也就是说
,它不知
道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数
format后面地址
的内容。先来看一个简单的例子:
<- begin -> fmt_test.c
#include <stdio.h>
int main(void)
{
char string[]="Hello World!";
printf("String: %s , arg2: %#p , arg3: %#p/n", string);
return 0;
}
<- end ->
上面的例子中我们其实只提供了一个数据参数"string",但在格式串中有三个打印
格式,
我们看一下运行的结果:
[warning3@redhat-6 format]$ gcc -o fmt_test fmt_test.c
[warning3@redhat-6 format]$ ./fmt_test
String: Hello World! , arg2: 0x6c6c6548 , arg3: 0x6f57206f
我们来看一下arg2,arg3显示的是哪里的内容:
[warning3@redhat-6 format]$ gdb ./fmt_test
<...>
(gdb) b printf
Breakpoint 1 at 0x8048308
(gdb) r
Starting program: /home/warning3/format/./fmt_test
Breakpoint 1 at 0x40064f5c: file printf.c, line 30.
Breakpoint 1, printf (
format=0x80484c0 "String: %s , arg2: %#p , arg3: %#p/n") at
printf.c:30
30 printf.c: No such file or directory.
(gdb) x/10x $ebp
0xbffffc88: 0xbffffca8 0x08048403 0x080484c0
0xbffffc98
0xbffffc98: 0x6c6c6548 0x6f57206f 0x21646c72
0x08049500
0xbffffca8: 0xbffffcc8 0x400301eb
我们看到printf()的第一个参数地址是$ebp+8,里面的内容是0x080484c0,
(gdb) x/s 0x080484c0
0x80484c0 <_IO_stdin_used+60>: "String: %s , arg2: %#p , arg3: %
#p/n"
这是我们的格式化串的地址
再来看我们要格式化输出的数据($ebp+12):
(gdb) x/s 0xbffffc98
0xbffffc98: "Hello World!"
我们看到,紧接着下来的两个字的内容就是刚才的程序中显示的结果:
$ebp+16: 0x6c6c6548 "Hell"
$ebp+20: 0x6f57206f "o Wo"
从下面的示意图上可以看得更清楚一些:
栈顶
+------------+
| ...... |
+------------+
0xbffffc88| 0xbffffca8 | --------> 保存的EBP -- printf()
+------------+
| 0x08048403 | --------> 保存的EIP -- printf()
+------------+ format
format-> | 0x080484c0 | --------> "String: %s , arg2: %#p , arg3: %
#p/n"的地址
+------------+ arg1
| 0xbffffc98 | --------> "Hello World!"的地址
+------------+
| 0x6c6c6548 | --------> string[] = "Hell
+------------+
| 0x6f57206f | --------> o Wo
+------------+
| 0x21646c72 | --------> rld!"
+------------+
| 0x08049500 | --------> '/0'xxx
+------------+
0xbffffca8| 0xbffffcc8 | --------> 保存的EBP -- main()
+------------+
| 0x400301eb | --------> 保存的EIP -- main()
+------------+
| ...... |
+------------+
栈底
我们可以看到,arg2,arg3所显示的其实是main()中数组strings中前两个字的内
容。
从上面这个简单的例子我们可以看到, *printf()只根据format中打印格式(%)的
数目来依次
显示堆栈中format参数后面地址的内容,每次移动一个字(4个字节).
由于我们上面的例子中出现了三个(%)号,所以它会依次打印三个地址的内容:
format+4, format + 8, format + 12.
(注意:并不是所有的%格式都是移动4个字节,例如%f就每次移动8个字节。如果
要覆盖的地址
距离比较远(比如2048字节),而%的个数又有所限制的话,使用%f可以较快的到达
"目的地",
只需要256个%f就可以了,%E也是如此)
正常情况下,由于format串通常是程序员自己来定制,很少出现上面那种情况,
而且即使
出现了,也并不会有什么大的安全问题。然而,如果format串是由用户提供的话
,那么就
非常危险了!这种情况往往是由于程序员的疏忽导致的。最常见的情况是当需要
利用
vsprintf()等来构造自己的类printf()函数时,例如
mylog(LEVEL, "username = %s", username);
如果引用mylog时错误的使用了mylog(LEVEL,user_buf),而user_buf的内容又是用
户可以控
制的话,那么真正的危险就来了。
1. 问题一:格式化串导致的传统缓冲区溢出
==========================================
我们以不久前发现的QPOP 2.53的例子来做一下详细的说明。
QPOP 2.53中pop_uidl.c中有个函数pop_euidl (p),用来完成EUIDL命令的功能,
它错误的
使用了pop_msg()函数:
.......
pop_euidl (p)
POP * p;
{
char buffer[MAXLINELEN]; /* Read buffer */
char *nl, *bp