格式化字符串漏洞本身并不算缓冲区溢出漏洞,这里作为比较典型的一类漏洞进行简单介绍。为了能够将字符串、变量、地址等数据按照指定格式输出,通常使用包含格式化控制符的常量字符串作为格式化串,然后指定用相应变量来代替格式化串中的格式化控制符。例如,prinf就是一个使用格式化串进行标准输出的函数,其参数包含两部分:printf 的第一个参数是格式化串,在下面例子就是"a=%d,b=%d",其中%d就是用于格式化输出的控制符;printf从第二个参数开始是与格式化控制符对应的参数列表,如a、b等。
#include <stdio.h>
int main(void){
int a=10,b=20,key=0;
printf(“a=%d,b=%d”,a,b);//使用格式化串进行输出
return 0;
}
如果对上述例子中的代码语句进行如下修改:
printf(“a=%d,b=%d”,a,b)改为printf(“a=%d,b=%d”)
那么当程序再次编译后,运行时发现输出结果不再是"a=10,b=20"了,这是为什么呢?
printf函数进行格式化输出时,会根据格式化串中的格式化控制符在栈上取相应的参数,按照所需格式进行输出。即使函数调用没有给出输出数据列表,但系统仍按照格式化串中指明的方式输出栈中数据。
在例子中,修改前,参数a,b正常人栈,所以输出正常;修改后,printf的参数不包括a,b,未能在函数调用时将其入栈,所以当 printf在栈上取与格式化控制符%d相对应的变量时,就不能找到a、b,而是错误地把栈上其他数据当做a、b的值进行了输出。
格式符除了常见的d、f、u、o、x之外,还有一些指针型的格式符:
s————参数对应的是指向字符串的指针;
n——这个参数对应的是一个整数型指针,将这个参数之前输出的字符的数量写入该格式符对应参数指向的地址中。
int a=0; printf("1234567890%n",&a);
对于上面代码,格式化串中指定了%n,此前输出了1~0这10个字符,因此这里将会修改a的值,即向其中写入字符数10。
类似地,恰当利用%p、%s、%n等格式符,一个精心构造的格式化串即可实现对程序内存数据的任意读、任意写,从而造成信息泄露、数据篡改和程序流程的非法控制这类威胁。
除了printf函数之外,其他该系列函数也有可能产生格式化串漏洞:printf,fprintf,sprintf,snprintf,vprintf,vfprintf,vsprintf,wprintf等。格式化串漏洞的利用可以通过如下方法实现:
①通过改变格式化串中输出字符数的多少实现修改要在指定地址写人的值:可以修改填充字符串长度实现;也可以通过改变输出的宽度实现,如%8d。
②通过改变格式化串中格式符的个数,调整格式符对应参数在栈中位置,从而实现对栈中特定位置数据的修改。如果恰当地修改栈中函数返回地址,那么就有可能实现程序执行流程的控制。也可以修改其他函数指针,改变执行流程。
相对于修改返回地址,改写指向异常处理程序的指针,然后引起异常,这种方法猜测地址的难度比较小,成功率较高。
格式化串漏洞是一类真实存在、并且是危害较大的漏洞,但是相对于栈溢出等漏洞而言,实际案例并不多。并且格式化串漏洞的形成原因较为简单,只要通过静态扫描等方法,就可以发现这类漏洞。此外,在VS2005以上版本中的编译级别对参数进行了检查,且默认情况下关闭了对%n控制符的使用。