首页 > 代码库 > 详谈Format String(格式化字符串)漏洞
详谈Format String(格式化字符串)漏洞
格式化字符串漏洞由于目前编译器的默认禁止敏感格式控制符,而且容易通过代码审计中发现,所以此类漏洞极少出现,一直没有笔者本人的引起重视。最近捣鼓pwn题,遇上了不少,决定好好总结了一下。
格式化字符串漏洞最早被Tymm Twillman在1999年发现,当时并未引起重视。在tf8的一份针对wu-ftpd格式化字符串漏洞实现任意代码执行的漏洞的报告之后(详情可参阅 《黑客攻防技术宝典-系统实战篇》),才让人们意识到它的危害,至此而发现了大量的相关漏洞。
格式化字符串漏洞的产生根源主要源于对用 户输入未进行过滤,这些输入数据都作为数据传递给某些执行格式化操作的函数,如printf,sprintf,vprintf,vprintf。恶意用户 可以使用"%s","%x"来得到堆栈的数据,甚至可以通过"%n"来对任意地址进行读写,导致任意代码读写。
//正确的使用方式test1main(){ printf("%x",1);}//错误的使用方式test2main(){ printf("%x")} //导致漏洞test3main(int argc,char * argv[]){ printf(argv[1])}
如果可以对printf(如test3)内进行任意输入,将会是一件很糟糕的事情。将会导致彻底的系统威胁。
在说明原理之前,先介绍几个格式控制符:
%d 用于读取10进制数值
%x 用于读取16进制数值
%s 用于读取字符串值
%n 用于讲当前字符串的长度打印到var中,例 printf("test %hn",&var[其中var为两个字节]) printf("test %n",&var[其中var为一个字节])
具体原理:当printf在输出格式化字符串的时候,会维护一个内部指针,当printf逐步将格式化字符串的字符打印到屏幕,当遇到%的时候,printf会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。甚至由于%n的问题,可导致任意地址读写。
talk is simple,Let‘s show by code:
#include <stdlib.h>#include <unistd.h>#include <stdio.h>#include <string.h>int target;void printbuffer(char *string){ printf(string);} void vuln(){ char buffer[512]; fgets(buffer, sizeof(buffer), stdin); printbuffer(buffer); if(target == 0x01025544) { printf("you have modified the target :)\n"); } else { printf("target is %08x :(\n", target); }}int main(int argc, char **argv){ vuln();}
这份代码来自Protostar format3,编译取消所有保护措施。
在试图利用格式化字符串漏洞之前,你需要知道格式化字符串会在调用printf之前先压入堆栈中。所以当发现一个格式化字符串漏洞时,首先你需要找到格式化字符串距离当前位置的偏移
可以利用%08x进行查找,如图
#关于偏移值,可能会由于环境和程序名称的不同而不同,甚至会产生不处于4的倍数的位置,面对这种情况,可以通过在最前方加入垫子进行取整。
可以得到偏移值为12,然后尝试通过利用%12$x(这种使用方法可以直接省去前面11个%08x的使用)进行直接读取,如图
至此,可以通过使用%Nx(N为任意长度的十进制数字)来控制字符串长度,字符串长度为len(‘AAAA‘)+N,通过将长度写入到偏移地址中来进行对任意地址进行任意读写。其中若使用%hn,只需要进行两次写操作,可以节省时间,但会消耗极大的空间。而若使用%n,则需要进行4次读写,但可以节省空间,对此,由于部分环境下,由于可能对缓冲区长度有所限制,导致exploit失败,所以更加偏向于使用%n。
很多地址均为不可见字符串,所以利用python和管道讲之前代码进行转化。如图
现在,尝试target低位进行写操作,即0x80496f4进行读写(使用IDA或者gdb进行查看target地址),如图:
此时,对0x080496f5进行写操作,此时只需要在后面添加长度为0x55-0x44的字符串即可,同时,需要对%.64进行-4操作,对12的偏移值+1即如图:
对0x080496f6,0x080496f6进行操作,这个时候发现0x04-0x55为负数,无法继续下去,可以从上一位借1.如图
成功对任意地址进行了任意读写。当然,顺序进行读写由于借位可能对更高位产生影响,本题为0x080496f8。若想解决这个问题,可以尝试从最小的开始进行写,本体0x080496f7,0x080496f6,0x080494f4,0x080496f3。对于,%hn的使用,可以直接参照魔术公式(虽然,%n也可以总结出来魔术公式,但是就显的比直接写更加复杂了)。详情参照《灰帽黑客》第四版。
格式化字符串最近出现的频率极为稀少,较近的可能为CVE-2012-0809 sudo_debug格式化字符串漏洞,和CVE-2012-3569 VMware OVF Tool格式化字符串漏洞。分别处于windows和linux环境。
顺便讲一下选择那些地址进行读写:
主流的读写位置如下:
FINI_ARRAY区:程序初始化和结束需要经过这里,可以写这里的析构函数。
全局偏移表:
全局函数指针:
atexit处理函数:
堆栈值(主要指返回地址):
虚表指针:
理论上只要程序在后面会调用的,可进行写操作的位置均可进行写,从而改变程序执行,进行成功的exploit
详谈Format String(格式化字符串)漏洞