0×00 背景


本文写给对堆溢出无的放矢的童鞋,分为如下几部分:

一.经典的unlink利用方法简介
二.在当今glibc的保护下如何绕过进行unlink利用

建议阅读本文之前先对glibc的malloc.c有所了解

你可以在这里在线看到所有的malloc.c的源码

0×01 第一部分


首先简要介绍一下堆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的大小,第二部分是本chunksize,因为它的大小需要8字节对齐,所以size的低三位一定会空闲出来,这时候这三个位置就用作三个Flag(最低位:指示前一个chunk是否正在使用;倒数第二位:指示这个chunk是否是通过mmap方式产生的;倒数第三位:这个chunk是否属于一个线程的arena)。之后的FD和BK部分在此chunk是空闲状态时会发挥作用。FD指向下一个空闲的chunk,BK指向前一个空闲的chunk,由此串联成为一个空闲chunk的双向链表。如果不是空闲的。那么从fd开始,就是用户数据了。(详细信息请参考glibcmalloc.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的开头加上下个chunksize就可以引导到下下个chunk。但是我们已经把下个chunksize覆盖为了-4,那么它会认为下个chunkprev_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已经不这么简单了,为了使堆溢出不那么容易就被利用,它加入了许多新的保护措施,如何绕过也就是要在第二部分中讨论的内容。

0×02 第二部分


以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,由此全部利用完成。