本文写给对堆溢出无的放矢的童鞋,分为如下几部分:
一.经典的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,由此全部利用完成。