3.1.5 格式化串漏洞案例及分析

3.1.5 格式化串漏洞案例及分析

1.格式化字符串漏洞简史

格式化字符串(Format string)漏洞最早是在1999年被Tymm tillman发现的,他在著名的Bugtraq邮件列表(一个专业的计算机安全邮件列表服务机构,有许多国内外的安全研究人员在上面公布漏洞,有时也提供漏洞细节与利用代码)中公布了一篇关于proftpd软件漏洞的文章Exploit for proftpd1.2.Opre6(http:seclistsorg/bugtraq/1999Sep/328),这就是最早关于格式化字符串漏洞的公开描述。虽然早在1999年就被发现存在格式化字符串漏洞,但在当时并没有被圈内人士重视。直到2000年,tf8在Bugtraq上公布了一份利用wu-ftpd格式化字符串漏洞实现任意代码执行的漏洞(WUFTPD:Providing*remote*root since at least1994,http://seclistsorg/bugtraq2000Jun/297),才使格式化字符串这类漏洞被广为人知,人们也逐步意识到它所带来的安全危害。之后,很多软件的格式化字符串漏洞被发现。

相对缓冲区溢出而言,格式化字符串更容易在源码和二进制分析中被发现,也比较容易在自动化检测过程中被发现,可能正是因为如此,才导致格式化字符串漏洞的“产量”远不如缓冲区溢出漏洞。虽然这类漏洞的数量不多,但在软件开发过程中还是有可能出现的,因此掌握和了解这类漏洞是很有必要的。

2.格式化输出函数

格式化输出函数是由一个格式字符串和可变数目的参数构成的。在效果上,格式化字符串提供了一组可以由格式化输出函数解释执行的指令。因此,用户可以通过控制格式字符串的内容来控制格式化输出函数的执行。

各个格式化输出函数由于历史不同,实现上也有显著的差异。C99标准中定义的格式化输出函数如下。

(1)printf按照格式字符串的内容将输出写入流中。流、格式字符串和变参列表一起作为参数提供给函数。

(2)printf()等同于fprintf(),除了前者假定输出流为stdout外。

(3)sprintf()等同于fprintf(),但是输出不是写入流而是写入数组中。C99规定在写入的字符末尾必须添加一个空宇符(null character)。

(4)snprintf()等同于sprintf(),但是它指定了可写入字符的最大值n。当n非零时,输出的字符超过n-1的部分会被舍弃而不会写入数组中。并且,在写入数组的字符末尾会添加一个空字符。

(5)vfprintf()、vprintf()、vsprintf()、vsnprintf()分别对应于fprintf()、printf()、sprintf()和snprintf(),只是它们将后者的变参列表换成了va_list类型的参数。当参数列表是在运行时决定时,这些函数非常有用。

格式化输出函数是一个变参函数,也就是说它接受的参数个数是可变的。变参函数在C语言中实现的局限性导致格式化输出函数在使用中容易产生漏洞。

3.格式字符串

格式字符串是由普通字符(ordinary character)(包括%)和转换规范(conversion specification)构成的字符序列。普通字符被原封不动地复制到输出流中。转换规范根据与实参对应的转换指示符对其进行转换,然后将结果写入输出流中。

转换规范通常按照从左向右的顺序解释。大多数转换规范都需要单个参数,但有时也可能需要多个或者完全不需要。程序员必须根据指定的格式提供相应个数的参数。当参数过多时,多余的将被忽略,而当参数不足时,则结果是未定义的。

一个转换规范是由可选域(标志、宽度、精度以及长度修饰符)和必需域(转换指示符)按照下面的格式组成:

%[标志][宽度][.精度][{长度修饰符}]转换指示符

例如,对转换规范%-10.8ld来说,-是标志位,10代表宽度,8代表精度,l是长度修饰符,d是转换指示符。这个转换规范将一个long int型的参数按照十进制格式打印,在一个最小宽度为10个字符的域中保持最少8位左对齐。每一个域都是代表特定格式选项的单个字符或数宇。最简单的转换规范仅仅包含一个“%”和一个转换指示符(如%s)。

4.案例及分析

格式化串漏洞是数据输出函数中对输出格式解析的缺陷。以最熟悉的printf函数为例,其参数应该含有两部分:格式控制符和待输出的数据列表。例如

对于上述代码,第一个printf调用是正确的,第二个调用中则缺少了输出数据的变量列表,那么第二个调用将引起编译错误还是照常输出数据?如果输出数据又将是什么类型的数据呢?按照实验环境将上述代码编译运行,实验环境[3]如表3.1所示。

表3.1 实验环境

其运行结果如图3.19所示

图3.19 运行结果

第二次调用没有引起编译错误,程序正常执行,只是输出的数据有点出乎预料。使用Ollydbg调试一下,得到“a=4218928,b=44”的原因就真相大白了。第一次调用printf的时候,参数按照从右向左的顺序入栈,栈中状态如图3.20所示。

图3.20 printf函数调用时的内存布局

当第二次调用发生时,由于参数中少了输入数据列表部分,故只压入格式控制符参数,这时栈中状态如图3.21所示。

图3.21 格式化串漏洞原理

虽然函数调用时没有给出“输出数据列表”,但系统仍然按照“格式控制符”所指明的方式输出了栈中紧随其后的两个dword。现在应该明白输出“a=4218928,b=44”的原因了:4218928的十六进制形式为0x00406030,是指向格式控制符“a=%d,b=%d\n”的指针;44是残留下来的变量a的值。