# 堆溢出的unlink利用方法

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-07-22T06:18:00Z
Canonical: https://wepostx.com/topics/915

> 乌云历史资料归档
>
> **原始作者：** 藏在灌木丛中
> **原始编号：** superkieran-wooyundrops:653
> **原始发布时间：** 2015-07-22 14:18
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 背景

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

```text
一.经典的unlink利用方法简介 二.在当今glibc的保护下如何绕过进行unlink利用
```
建议阅读本文之前先对glibc的malloc.c有所了解
[你可以在这里在线看到所有的malloc.c的源码](http://code.woboq.org/userspace/glibc/malloc/malloc.c.html)

# 0x01 第一部分

首先简要介绍一下堆chunk的结构
我们可以在malloc.c中找到关于堆chunk结构的代码

```text
#!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是如下的结构

```text
#!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部分，在此不再多做解释。)
首先，为了方便，我直接引用一位外国博主的漏洞示例程序，以便继续解释

```text
#!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部分。
简单给出此程序的堆结构

```text
#!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

```text
#!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中解引用。
具体如下

```text
#!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已经不这么简单了，为了使堆溢出不那么容易就被利用，它加入了许多新的保护措施，如何绕过也就是要在第二部分中讨论的内容。

# 0x02 第二部分

以glibc中的代码作为示例，首先拿出最新版本的unlink宏。

```text
#!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 /*
```
我们可以看到我们最大的阻碍是下面的这部分代码

```text
#!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这部分也是同样的道理。
在第一部分的利用方法中，我们修改了

```text
#!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指向堆上的某个地方)的，通过堆溢出，我们可以做如下的修改。

```text
#!c p->fd=ptr-0x18 p->bk=ptr-0x10
```
布置好如此结构后，再触发unlink宏，会发生如下情况。

```text
#!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出现的一个逻辑非常简单的程序作为漏洞示例程序,[可以在此下载](https://www.dropbox.com/s/ifjua544gd4js41/shellman?dl=0)
首先简单介绍这个Binary的功能以及基本情况

##### 开启的保护

```text
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH shellman
```

##### 基本功能

```text
1.显示已经建立的堆块中存储的内容 2.建立一个新的堆块，大小和内容又用户决定 3.对一个已经分配的堆块做编辑，这个地方没有限制大小，若太长可造成堆溢出 4.释放一个已经分配的堆块
```

##### 存放的堆块的基本逻辑结构

```text
.bss:00000000006016C0 ; __int64 usingFLAG[] .bss:00000000006016C0 usingFLAG dq ? ; DATA XREF: main+38o .bss:00000000006016C0 ; .text:0000000000400A90o ... .bss:00000000006016C8 ; __int64 LEN[] .bss:00000000006016C8 LEN dq ? ; DATA XREF: new+B5w .bss:00000000006016C8 ; delete+79w .bss:00000000006016D0 ; __int64 content[] .bss:00000000006016D0 content dq ? ; DATA XREF: new+BCw
```
程序有一个全局数组会存储好每一个经过malloc分配的堆块返回的指针。以及在全局数组中存储长度以及本块是否正在使用的标志。

##### 如何利用
按照前文所介绍的，我们希望使用Unlink的方法去利用这个堆溢出漏洞。首先，我们要找一个指向堆上某处的指针。因为存储malloc返回指针的全局数组的存在，这让我们的利用变得异常的简单。因为bss段的地址也是固定的，我们可以知道，从而设置满足需要的bk和fd指针，下面介绍具体步骤。
1.我们可以首先分配两个长度合适的堆块。(如下图所示)

```text
chunk0 malloc返回的ptr chunk1 malloc返回的ptr | | | | +-----------+---------+---+---+-------------+------+------+----+----+------+ | | | | | | | | | | | | | | | | | prev | size&| | | | | prev_size |size&Flag| | | | size | flag | | | | | | | | | | | | | | | | | | | | | | | | | | +-----------+---------+---+---+-------------+------+------+----+----+------+
```
这时候这两块的fd和bk区域其实都是空的，因为他们都是正在使用的
2.对第一块进行编辑，编辑的过程中设置好第零块的bk和fd指针并溢出第一块，改好第一块的chunk头的控制信息(如下图所示)

```text
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)。

```text
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，由此全部利用完成。

## Replies
