14.7.3 格式化输出的弱点
输出格式化函数是一些恶心 bug 的源头。标准库里有一些这种格式化函数;表格 14-1 列出了这些函数。
Table 14-1.String Format Functions
Function | Description |
---|---|
printf | 输出格式化后的字符串 |
fprintf | 将 printf 的结果写入到文件 |
sprintf | 打印到一个字符串 |
snprintf | 打印到字符串的同时检查长度 |
vfprintf | 把 va_arg 结构打印到文件 |
vprintf | 把 va_arg 打印到 stdout |
vsprintf | 把 va_arg 打印到字符串 |
vsnprintf | 把 va_arg 打印到字符串同时检查长度 |
列表 14-26 展示了一个例子。假设用户输入了少于 100 个符号。你能让这个程序挂掉或者产生其它有意思的效果么?
Listing 14-26.printf_vuln.c
#include <stdio.h>
int main(void) {
char buffer[1024];
gets(buffer);
printf( buffer );
return 0;
}
这种弱点并不是因为使用了 gets 函数,而是由于对用户输入的格式化而导致的。用户的输入中可能会包含格式化占位符,从而导致有趣的行为。我们会提到一些潜在的意外行为。
"%x" 占位符和其类似的符号能够用来查看栈内容。前五个 "%x" 将从寄存器器中取参数(rdi 已经被 format 字符串地址占据了),紧跟着的几个会展示出栈内容来。我们把列表 14-26 中的例子编译一下,看看对这个程序输入 "%x %x %x %x %x %x %x %x %x %x %x" 会发生什么情况。
> %x %x %x %x %x %x %x %x %x %x
b1b6701d b19467b0 fbad2088 b1b6701e 0 25207825 20782520 78252078 25207825
正如我们所见,输出结果是四个有些共同点的数值,一个 0 然后和两个其它的数值。按照我们的假设,最后两个数字已经是从栈上获取的了。
进入 gdb 来探索一下 printf 调用之后的栈顶内容,我们可以得到证明观点的证据。列表 14-27 展示了 gdb 中的输出。
Listing 14-27.gdb_printf
(gdb) x/10 $rsp
0x7fffffffdfe0: 0x25207825 0x78252078 0x20782520 0x25207825
0x7fffffffdff0: 0x78252078 0x20782520 0x25207825 0x00000078
0x7fffffffe000: 0x00000000 0x00000000
- "%s" 这个格式化占位符用来打印字符串。因为字符串使用其起始位置的地址来定义,即使用指针来对内存寻址。所以传入非法指针的话,非法的指针也会被解引用。
■Question 266 列表 14-26 中的代码,如果输入 "%s %s %s %s %s" 的话,执行结果是什么呢?
- "%n" 格式化占位符虽然可能比较吸引人,但却对程序有害。该符号允许用户将一个 integer 写入到内存。printf 函数接收指向 integer 的指针,该 integer 会被当前已经写过的符号数量所重写 (在 "%n" 之前)。列表 14-28 展示了 "%n" 的一个使用的例子。
Listing 14-28.printf_n.c
#include <stdio.h>
int main(void) {
int count;
printf( "hello%n world\n", &count);
printf( "%d\n", count );
return 0;
}
这个程序会输出 5,因为在 "%n" 之前有五个符号输出。这个并不是一个莫名其妙的字符串长度,因为在前面可能还有其它的格式化占位符,而这些占位符可能影响到变量的长度输出(e.g.打印一个 integer 可能会产生七或十个符号)。列表 14-29 展示了一个例子、
Listing 14-29.printf_n_ex.c
int x;
printf("%d %n", 10, &x); /* x = 3 */
printf("%d %n", 200, &x); /* x = 4 */
为了避免这样的结果,不要使用用户输入的字符串作为格式化字符串。使用 printf("%s", buffer) 在 buffer 非 NULL 且使用 0 结束的情况下总是安全的。不要忘记使用 puts,fputs 这些既快速又安全的函数。