目录

什么是格式化字符串?

背景知识

格式化字符串,是一些程序设计语言在格式化输出API函数中用于指定输出参数的格式与相对位置的字符串参数,例如C、C++等程序设计语言的printf类函数,其中的转换说明(conversion specification)用于把随后对应的0个或多个函数参数转换为相应的格式输出;格式化字符串中转换说明以外的其它字符原样输出

维基百科:格式化字符串

在Linux系统中命令行输入 man 3 printf 我们可以看到在C语言中的所有格式化字符串函数

各个格式化字符串函数的简介如下:

表1

函数名 函数作用
printf 格式化输出字符串到 stdout
fprintf 输出到文件
sprintf 输出到字符串
snprintf 输出指定长度的内容到字符串
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到文件
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表输出指定长度的内容到字符串

格式化字符串漏洞原理介绍

printf() 和 scanf() 函数使您弄够与程序通信。他们被称为输入/输出函数,或者简称为I/O函数

C Primer Plus 第四章 字符串和格式化输入/输出 4.4

以上格式化字符串函数如果不正确使用都会导致发生安全问题

什么是格式化字符串漏洞?

我们以 printf 函数为例, 当请求 printf() 打印变量的指令取决于变量的类型。例如,在打印整数时使用 %d 符号,而在打印字符时使用 %s 符号。这些符号被称为转换说明(converion specification), 因为他们指定了如何把数据转换成可现实的形式。

C Primer Plus 第四章 字符串和格式化输入/输出 4.4.1

为了简化问题,一下几个表将只列出与本文讲述的漏洞相关的知识点,其他详细内容请查看 Man 手册: man 3 printf

表2 中将列出转换说明和这些转换说明符打印的输出类型

表2

转换说明 输出
%x 使用十六进制数字0-f的无符号十六进制整数
%n 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量
%d 有符号十进制整数
%c 一个字符
%s 字符串

可以在 % 和定义转换字符之前插入修饰符对基本的转换说明加以修改。 表3表4 列出了可以插入的合法字符。如果使用了一个以上的修饰符,那么他们应该与其表3中出现的顺序相同,并不是所有的组合都是可能的。

表3 printf() 修饰符

修饰符 意义
标志 五种标志( -, +, 空格, # 和 0) 都将在表4中描述,可以使用零个或多个标志,示例:”%-10d”
digit(s) 字段宽度的最小值。如果该字段不能容纳要打印的数或者字符串,系统就会使用更宽的字段,示例:”%4d”
.digit(s) 精度。也就是保留小数点后的位数,示例:”%5.2f” 打印一个浮点数,宽度为5个字符,小数点后有两个数字

表4 printf() 的标志

标志 意义
0 对于所有的数字格式,用前导零而不是空格填充字段宽度,示例:”%08x”
n$ n是用这个格式说明符(specifier)显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出 示例:”printf("%2$d %1$d", 111, 222) 输出 222, 111, %2$d 可以理解为将第二个参数格式化为符号十进制数形式输出 “

涉及该漏洞的知识点交代完毕,接下来看看具体的例子

什么是格式化字符串漏洞?

以下代码在 Exploit Education 出的漏洞练习靶机 Protostar 上运行

我们先来看看下面的一段的 C 程序代码:

代码1

void main() {
    printf("%d %s %x", 1337\n,"h4cx0r", 1337);
}

以上代码保存为demo1.c,使用 gcc 编译生成可执行文件

$ gcc demo1.c -i demo1

程序输出如下:

代码1 中的 prinf 函数:

  • 把数字 1337 以整数形式进行格式化输出
  • 把字符串 h4cx0r 以字符串形式进行格式化后输出
  • 把数字 1337 以十六进制进行进行格式化后输出。

如果你学过 C,以上代码很好理解。

我们在来看下面的代码

代码2

#include <stdio.h>

int main(int argc, char **argv) {
    printf(argv[1]); # 输出程序第一个参数
    printf("\n");    # 输出一个换行符
    return 1;
}

以上是一个简单的程序,也就是不做任何处理直接输出程序的第一个参数,第一个参数是什么,就输出什么

程序运行结果如下

这时我们设置参数为%x %x %x %x 试试,看会输出什么

输出的结果为:

输出的 b7ff1040 804843b b7fd7ff4 8048430 这是什么玩意儿?

为了一探究竟,我们使用 GDB 调试器对该代码进行调试

用到的代码如下:

$ gdb ./demo2 -q               # 使用 gdb 加载 demo2 文件, -q 参数为静默模式
$ set disassembly-flavor intel # 设置反编译的汇编代码格式为 Intel
$ disassemble main             # 反编译 main 函数
$ break *main+20               # 在 main 函数开始的第20个汇编指令处设置断点,也就是在 prinf 函数上设置断点
$ run "%x %x %x %x"            # 执行程序,也就是在 gdb 中执行 demo2,第一个参数为 "%x %x %x %x"
$ x /8wx $esp                  # 以十六进制输出8个字长度的$esp寄存器,也就是栈中的信息
$ continue                     # 继续执行程序

调试过程如下:

真相出现了!!!

原来,代码2 中输出的是栈中的数据,四个 %x 依次十六进制格式输出了内存中也就是栈中的 4 个地址信息,这就是字符串格式化漏洞

这时字符串格式化漏洞的概念也就出来了:

字符串格式化漏洞(Format String Vulnerability)发生在当输入的字符串被程序当作转换说明符来执行时,攻击者可以获取正在运行的程序的内存信息,或修改内存中的信息,或使程序崩溃

格式化字符串漏洞利用

从上文中我们看到,利用格式化字符串漏洞可以查看栈中的信息,站在程序的角度说就是会泄漏内存,除了这个,还可以

崩溃程序,使其拒绝服务

我们再看上文的 demo2程序,我们指定参数为多个%s看看会发生什么。

Segmentation fault (分段故障)

会让程序发生分段故障,使程序崩溃退出

这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。这一利用,虽然攻击者本身似乎并不能控制程序,但是这样却可以造成程序不可用。比如说,如果远程服务有一个格式化字符串漏洞,那么我们就可以攻击其可用性,使服务崩溃,进而使得用户不能够访问。

至于为什么会这样,还有怎么利用此类漏洞,请听我下回分解。