本文写给对堆溢出无的放矢的童鞋,分为如下几部分:
一.经典的unlink利用方法简介 二.在当今glibc的保护下如何绕过进行unlink利用
建议阅读本文之前先对glibc的malloc.c有所了解
首先简要介绍一下堆chunk的结构
我们可以在malloc.c中找到关于堆chunk结构的代码
#!c 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; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };
这指明了一个heap chunk是如下的结构
#!c +-----------+---------+------+------+-------------+ | | | | | | | | | | | | | prev_size |size&Flag| fd | bk | | | | | | | | | | | | | | +-----------+---------+------+------+-------------+
如果本chunk前面的chunk是空闲的,那么第一部分prev_size会记录前面一个chunk的大小,第二部分是本chunk的size,因为它的大小需要8字节对齐,所以size的低三位一定会空闲出来,这时候这三个位置就用作三个Flag(最低位:指示前一个chunk是否正在使用;倒数第二位:指示这个chunk是否是通过mmap方式产生的;倒数第三位:这个chunk是否属于一个线程的arena)。之后的FD和BK部分在此chunk是空闲状态时会发挥作用。FD指向下一个空闲的chunk,BK指向前一个空闲的chunk,由此串联成为一个空闲chunk的双向链表。如果不是空闲的。那么从fd开始,就是用户数据了。(详细信息请参考glibc的malloc.c部分,在此不再多做解释。)
首先,为了方便,我直接引用一位外国博主的漏洞示例程序,以便继续解释
#!c /* Heap overflow vulnerable program. */ #include <stdlib.h> #include <string.h> int main( int argc, char * argv[] ) { char * first, * second; /*[1]*/ first = malloc( 666 ); /*[2]*/ second = malloc( 12 ); if(argc!=1) /*[3]*/ strcpy( first, argv[1] ); /*[4]*/ free( first ); /*[5]*/ free( second ); /*[6]*/ return( 0 ); }
这个程序在[3]处有很明显的堆溢出漏洞,argv[1]中的内容若过长则会越界覆盖到second部分。
简单给出此程序的堆结构
#!c +---------------------+ <--first chunk ptr | prev_size | +---------------------+ | size=0x201 | +---------------------+ <--first | | | allocated | | chunk | +---------------------+ <--second chunk ptr | prev_size | +---------------------+ | size=0x11 | +---------------------+ <--second | Allocated | | chunk | +---------------------+ <-- top | prev_size | +---------------------+ | size=0x205d1 | +---------------------+ | | | | | | | TOP | | | | CHUNK | | | +---------------------+
此处不赘余介绍exploit具体代码,只介绍利用方法.
只要我们通过溢出构造,使得second chunk
#!c prev_size=任意值 size=-4(因为最低位的flag没有设置,所以prev_size是什么值是无所谓了) [email protected]([email protected]�定技术”) bk=shellcode地址
在我们的payload将指定位置的数值改好后。下面介绍在[4][5]行代码执行时发生的详细情况。
第四行执行free(first)发生如下操作
1).检查是否可以向后合并
首先需要检查previous chunk是否是空闲的(通过当前chunk size部分中的flag最低位去判断),当然在这个例子中,前一个chunk是正在使用的,不满足向后合并的条件。
2).检查是否可以向前合并
在这里需要检查next chunk是否是空闲的(通过下下个chunk的flag的最低位去判断),在找下下个chunk(这里的下、包括下下都是相对于chunk first而言的)的过程中,首先当前chunk+当前size可以引导到下个chunk,然后从下个chunk的开头加上下个chunk的size就可以引导到下下个chunk。但是我们已经把下个chunk的size覆盖为了-4,那么它会认为下个chunk从prev_size开始就是下下个chunk了,既然已经找到了下下个chunk,那就就要去看看size的最低位以确定下个chunk是否在使用,当然这个size是-4,所以它指示下个chunk是空闲的。
在这个时候,就要发生向前合并了。即first chunk会和 first chunk的下个chunk(即second chunk)发生合并。在此时会触发unlink(second)宏,想将second从它所在的bin list中解引用。
具体如下
#!c BK=second->bk(在例子中bk实际上是shellcode的地址) FD=second->fd ([email protected] - 12) FD->bk=BK /*shellcode的地址被写进了FD+12的位置,[email protected],[email protected]*/ BK->fd=FD
执行unlink宏之后,再调用free其实就是调用shellcode,这时就可以执行任意命令了。
但是,在现如今,glibc已经不这么简单了,为了使堆溢出不那么容易就被利用,它加入了许多新的保护措施,如何绕过也就是要在第二部分中讨论的内容。
以glibc中的代码作为示例,首先拿出最新版本的unlink宏。
#!c 1413 /* Take a chunk off a bin list */ 1414 #define unlink(AV, P, BK, FD) { 1415 FD = P->fd; 1416 BK = P->bk; 1417 if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) 1418 malloc_printerr (check_action, "corrupted double-linked list", P, AV); 1419 else { 1420 FD->bk = BK; 1421 BK->fd = FD; 1422 if (!in_smallbin_range (P->size) 1423 && __builtin_expect (P->fd_nextsize != NULL, 0)) { 1424 if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) 1425 || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) 1426 malloc_printerr (check_action, 1427 "corrupted double-linked list (not small)", 1428 P, AV); 1429 if (FD->fd_nextsize == NULL) { 1430 if (P->fd_nextsize == P) 1431 FD->fd_nextsize = FD->bk_nextsize = FD; 1432 else { 1433 FD->fd_nextsize = P->fd_nextsize; 1434 FD->bk_nextsize = P->bk_nextsize; 1435 P->fd_nextsize->bk_nextsize = FD; 1436 P->bk_nextsize->fd_nextsize = FD; 1437 } 1438 } else { 1439 P->fd_nextsize->bk_nextsize = P->bk_nextsize; 1440 P->bk_nextsize->fd_nextsize = P->fd_nextsize; 1441 } 1442 } 1443 } 1444 } 1445 1446 /*
我们可以看到我们最大的阻碍是下面的这部分代码
#!c if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P);
这段代码被添加到了unlink宏中,所以现在再调用unlink宏的时候,chunk指针P->fd->bk(即代码中的大写FD->bk)应该还是p指针自己。对于BK->fd != p这部分也是同样的道理。
在第一部分的利用方法中,我们修改了
#!c p->[email protected] p->bk=shellcode_adress
我们在此记FD=p->fd , BK=p->bk,再去看FD->bk已经是[email protected]了,这部分是不能满足要求的。再看BK->fd已经是shellcode+16了,所以如上文的利用方法已经不能成功了。之所以还加以介绍,是因为这会使我们理解第二部分变得又快又好。
如果绕过还是要根据这段保护代码来谈。我们势必需要构造合适的条件的来过掉这行代码,那么就要找一个指向p的的已知的地址,然后根据这个地址去设置伪造的fd和bk指针就能改掉原p指针。
以64bit为例,假设找到了一个已知地址的ptr是指向p(p指向堆上的某个地方)的,通过堆溢出,我们可以做如下的修改。
#!c p->fd=ptr-0x18 p->bk=ptr-0x10
布置好如此结构后,再触发unlink宏,会发生如下情况。
#!c 1.FD=p->fd(实际是ptr-0x18) 2.BK=p->bk(实际是ptr-0x10) 3.检查是否满足上文所示的限制,由于FD->bk和BK->fd均为*ptr(即p),由此可以过掉这个限制 4.FD->bk=BK 5.BK->fd=FD(p=ptr-0x18)
这时候再对p进行写入,可以覆盖掉p原来的值,例如我们用合适的payload将[email protected]写入。p就变成了[email protected],那么再改一次p,把[email protected]改为shellcode的地址或者说system的地址都可以。之后再调用free功能,就可以任意命令执行。
为了方便,在这边拿出一个最近的wargame出现的一个逻辑非常简单的程序作为漏洞示例程序,可以在此下载
首先简单介绍这个Binary的功能以及基本情况
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH shellman
1.显示已经建立的堆块中存储的内容 2.建立一个新的堆块,大小和内容又用户决定 3.对一个已经分配的堆块做编辑,这个地方没有限制大小,若太长可造成堆溢出 4.释放一个已经分配的堆块
.bss:00000000006016C0 ; __int64 usingFLAG[] .bss:00000000006016C0 usingFLAG dq ? ; DATA XREF: main+38o .bss:00000000006016C0 ; .text:0000000000400A90o ... .bss:00000000006016C8 ; __int64 LEN[] .bss:00000000006016C8 LEN dq ? ; DATA XREF: new+B5w .bss:00000000006016C8 ; delete+79w .bss:00000000006016D0 ; __int64 content[] .bss:00000000006016D0 content dq ? ; DATA XREF: new+BCw
程序有一个全局数组会存储好每一个经过malloc分配的堆块返回的指针。以及在全局数组中存储长度以及本块是否正在使用的标志。
按照前文所介绍的,我们希望使用Unlink的方法去利用这个堆溢出漏洞。首先,我们要找一个指向堆上某处的指针。因为存储malloc返回指针的全局数组的存在,这让我们的利用变得异常的简单。因为bss段的地址也是固定的,我们可以知道,从而设置满足需要的bk和fd指针,下面介绍具体步骤。
1.我们可以首先分配两个长度合适的堆块。(如下图所示)
chunk0 malloc返回的ptr chunk1 malloc返回的ptr | | | | +-----------+---------+---+---+-------------+------+------+----+----+------+ | | | | | | | | | | | | | | | | | prev | size&| | | | | prev_size |size&Flag| | | | size | flag | | | | | | | | | | | | | | | | | | | | | | | | | | +-----------+---------+---+---+-------------+------+------+----+----+------+
这时候这两块的fd和bk区域其实都是空的,因为他们都是正在使用的
2.对第一块进行编辑,编辑的过程中设置好第零块的bk和fd指针并溢出第一块,改好第一块的chunk头的控制信息(如下图所示)
chunk0 malloc返回的ptr chunk1 malloc返回的pt | | | | +-----------+---------+----+----+----+----+----+------+------+----+----+------+ | | |fake|fake|fake|fake| D | fake | fake | | | | | | |prev|size| FD | BK | A | prev | size&| | | | | prev_size |size&Flag|size| | | | T | size | flag | | | | | | | | | | | A | | | | | | | | | | | | | | | | | | | +-----------+---------+----+----+----+----+----+------+------+----+----+------+ |--------new_size--------|
我们为了欺骗glibc,让它以为堆块零malloc返回的指针(我们后文中简记为p)出就是chunk0指针,所以我们伪造了prev_size和size的部分,然后溢出堆块1,改掉第1个堆块的prev_size,数值应该是上图所示new_size的大小;另外第1块的size部分还要把prev_inuse的flag给去掉。如此就做好了unlink触发之前的准备工作
3.删掉chunk1,触发unlink(p),将p给改写。
在删除堆块1时,glib会检查一下自己的size部分的prev_inuse FLAG,发现到到比较早的一个chunk是空闲的(实际是我们伪造的),glibc希望将即将出现的两个空闲块合并。glibc会先将chunk0从它的Binlist中解引用,所以触发unlink(p)。
1).FD=p->fd(实际是0x6016D0-0x18,因为全局数组里面指向p的那个指针就是0x6016D0) 2).BK=p->bk(实际是6016D0-0x10) 3).检查是否满足上文所示的限制,由于FD->bk和BK->fd均为*6016D0(即p),由此可以过掉这个限制 4).FD->bk=BK 5).BK->fd=FD(p=0x6016D0-0x18)
4.对p再次写入,[email protected]
[email protected],[email protected]实地址,进而算出libc的基址来过掉ASLR。
6.根据已经算出的libc基址再次算出system函数的真实[email protected]� (如果没有libc,可以考虑简历多个chunk,[email protected]��面的函数,这样在list时,我们可以得到两个libc函数的真实地址,根据其偏移,便可以找出服务器上的libc,若保护再够复杂无法改got,我们还可以构造ropchain,同样利用这样的方式,把ropchain丢进全局数组中)
7.因为free已经变成了system,只要再建立一个内容为/bin/sh的块,再删掉,就可以得到shell,由此全部利用完成。
上一篇
下一篇