详谈Format String(格式化字符串)漏洞

  格式化字符串漏洞由于目前编译器的默认禁止敏感格式控制符,而且容易通过代码审计中发现,所以此类漏洞极少出现,一直没有笔者本人的引起重视。最近捣鼓pwn题,遇上了不少,决定好好总结了一下。

   格式化字符串漏洞最早被Tymm Twillman在1999年发现,当时并未引起重视。在tf8的一份针对wu-ftpd格式化字符串漏洞实现任意代码执行的漏洞的报告之后(详情可参阅 《黑客攻防技术宝典-系统实战篇》),才让人们意识到它的危害,至此而发现了大量的相关漏洞。

  格式化字符串漏洞的产生根源主要源于对用 户输入未进行过滤,这些输入数据都作为数据传递给某些执行格式化操作的函数,如printf,sprintf,vprintf,vprintf。恶意用户 可以使用"%s","%x"来得到堆栈的数据,甚至可以通过"%n"来对任意地址进行读写,导致任意代码读写。

//正确的使用方式test1
main()
{

    printf("%x",1);

}

//错误的使用方式test2
main()
{
    printf("%x")
} 
 
//导致漏洞test3
main(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

posted @ 2016-09-25 02:58  0xJDchen  阅读(16124)  评论(0编辑  收藏  举报