PWN堆unlink
- 参考博客:PWN入门(3-5-1)- unsortedbin的unlink(基础) (yuque.com)
- 参考视频:星盟安全unlink
- 参考视频:b站up意大利的猫,堆溢出——unlink漏洞
前提介绍
- unlink,俗称拖链,将
unsorted_bin中的处于free状态的堆块脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(可以是向前合并或者向后合并),合并完之后再放入unsorted_bin中。 - 漏洞产生的原因:
offbynull、offbyone、堆溢出,修改了堆块的p标志位 - 在这里,建议先了解
unlink的原理,和如何利用,再学习off_by_null和off_by_one - 在本篇文章中会标注堆的高低地址,以便会更清晰的展现unlink的过程。
堆再介绍
- 为了更快的获取堆内存空间,程序员设计了
bins这个数据结构,这个数据结构就是用来更好的,更快速的管理堆、获得堆内存。 bins只是为了管理free之后的堆块bins一共有136个bins:unsorted binssmall binslargebins- 他们的分布如下

- 接下来再贴上一个比较熟悉的图,接下来再详细介绍一下该堆块,这里再来介绍一下每个存储空间的具体含义:
-
prev_size:记录前一个堆块的大小 -
size:记录当前堆块的大小 -
N、M、P:N: NON_MAIN_ARENA,表示当前chunk是否位于非主arena中(与多线程相关)如果为1,表示该chunk位于非主arena中,通常在多线程环境中使用。如果为0,表示该chunk位于主arena中,主arena是单线程或进程默认使用的堆区域。M:IS_MMAPPED,表示当前chunk是否通过mmap系统调用分配。如果为1,表示该chunk是通过mmap分配的,这类chunk通常用于大块内存的分配,且释放时需要调用munmap。如果为0,表示该chunk是通过常规的brk/sbrk方式分配的。P:PREV_INUSE,表示前一个chunk是否已分配。如果为1,表示前一个chunk已分配,无法与当前chunk合并。如果为0,表示前一个chunk空闲,允许与当前chunk合并。
-
fd、bk:是俩个指针,主要用来free堆块后,free的堆块被bin管理时,形成的链表的指针。 -
fd、bk、user_data、以及下一个prev_size:在堆块被申请之后都是用来存放用户输入的是数据
-

- 下图是
size所表示的堆块大小范围

- 知道了
size所表示堆块大小的范围后,接下来就可以解释为什么会有N、M、P标志位了 - 由于一个堆块至少包含了
prev_size、size、fd、bk。 - 而
user_data可能会为0,当我们malloc(0x1)时,堆管理器会判断申请的堆块会不会满足fd、bk、后一个prev_size内存空间能不能存放下去,如果能存放下去,则user_data是为0的。 - 而
prev_size、size是size_t类型(无符号整数),在32位是4字节,64位8字节,fd、bk指针都一样32位4字节、64位8字节。这样32位的堆块size至少要0x10,64位堆块至少要0x20。并且堆块最后还会和8字节或32字节对齐。 - 从下图可以看到,size的最小3位都是0,都是空闲着不会改变,所以就将这3位合理利用起来,即将他们做为标志位
1 | 0x10--->0001 0000 |
- 而这边的
P位表示的是与当前堆块物理地址相邻的前一个堆块是否空闲还是在使用,这里p等于1就表示物理地址相邻的前一个堆块正在被使用(这里chunk1简单画了就没把user_data画出来)

- 当这物理地址相邻的俩个堆块
P标志位都为0的情况下,这俩个堆块就会发生合并,俩个堆块合并成一个大堆块,并放在unsorted_bin这个链表里面

- 然后就会变成这样,合并还分为前向合并(堆块从前向后合并)和后向合并(堆块从后向前合并)

- main_arena:是结构体
malloc_state的一个实例,下面是malloc_state结构体中内部具体的结构
1 | struct malloc_state |
- 下面的代码就是定义并初始化
main_arena
1 | static struct malloc_state main_arena = |
- 这里面先使用gdb动态调试查看
main_arena的结构
- 下面用图介绍一下
main_arena并给出一些在本题中比较重要的一些东西:- unlink过程主要脱的是bins中管理的堆块链表
- 当unlink结束后新合成的堆块会与top_chunk的地址相连还会与top_chunk合并

示例程序
- 注:以下程序都是在64位环境下进行的
实验1
- 对下面程序进行动态调试,思考以下问题
- 申请的堆块释放后会被哪个
bins管理? - p1和p2会发生合并吗?
- p2和p3会发生合并吗?
- 申请的堆块释放后会被哪个
1 |
|
分析1.1
- 将可执行程序编译后,使用
gdb动态调试,输入ni指令将程序执行到该语句

- 然后使用
heap指令查看堆块,发现申请了四个堆块,四个堆块都在使用中

- 然后再使用
ni指令,将程序运行到该处(第一个free之后,第二个free之前)

- 再使用
heap -v指令查看,发现第一个被释放的chunk0被归入了unsortedbin里面

- 再使用
ni指令运行到第二个free后,第三个free前

- 再使用
heap -v查看堆块,发现第二个被释放的堆块也被放入了unsortedbin,此时我们还发现第二个堆块(即Addr:0x1200110这个地址的堆块)的P位变成了0,表示了前一个堆块处于空闲

- 这时我们使用
unsortedbin指令,查看unsortedbin,发现unsortedbin这个bin上有俩条链子,但是他们并没有合并,这里还要注意一点是unsortedbin是后进先出

- 接下来
ni将程序执行到第三次free之后,再使用heap -v命令查看堆的状态,发现后俩个堆块被合并了,都合并到了Top chunk中,top chunk的Addr也发生了改变

- 回答:
- 这些堆块被释放后都会被
unsortedbin管理 p1与p2这俩个指针指向的堆块是不会合并的,只有物理地址相邻且空闲的堆块会被合并p2与p3这俩个指针指向的堆块是会合并的
- 这些堆块被释放后都会被
补充1
- 编译并调试该段代码
1 |
|
- 使用gdb动态调试,使用
ni指令将程序运行到第二个free之后,第三个free之前,使用heap -v查看堆块

- 再使用
ni指令将程序运行到第三个free之后,第四个free之前,再使用heap -v查看堆块,发现第三个申请的堆块和第四个申请的堆块在释放后被合并了,合并后也会被放入unsortedbin里面

实验2
-
查看分析glibc源码,并使用图描述unlink的过程,然后具体了解unlink的检查过程
-
Index of /gnu/glibc在该网站上找到glibc2.23,下载解压后在该目录
glibc-2.23\malloc下找到malloc.c,搜索到unlink,查找到unlink这个宏定义,这段代码就是unlink的具体过程

- 这段代码是unlink的具体代码,也是unlink的具体逻辑,源码看不习惯,我稍微调整了一下位置(没有修改代码)
分析2.1
- 其实我们初学只需要注意前8行即可,因为后面的是针对
largebin这个更复杂一点的堆块结构的操作(稍微认真看一下源码就可以知道了),而且通过查看前8行的源码就会知道在unlink中只是进行了脱链的操作,并没有修改堆块的size位,而堆块的size位是在malloc_consolidate这个函数中所修改的
1 |
|
- 接下来使用图简单描述一下unlink的过程,这里声明一下,unlink的过程是在chunk加入unsortedbin之前进行的,所以他们所以fd、bk指针都是根据物理地址的高低来指向的(建议自己动手画一遍就理解了,看别人画的图是比较乱的)
fd指向当前 chunk 的下一个空闲块,通常是物理内存地址较高的那个块。bk指向当前 chunk 的上一个空闲块,通常是物理内存地址较低的那个块。- 当前 chunk 的
fd指针会被设置为NULL,表示没有后续的空闲块。 - 当前 chunk 的
bk指针会被设置为NULL,表示没有前面的空闲块。
1 | FD = P->fd |

- 执行完
1 | FD->bk = BK; |

- 0
- 我们再来查看unlink的第四行代码和第五行代码,
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) |
也就是说在脱链之前,也就是说他会先检查下图红线加粗的链表是否指向要脱掉的链,就可以防止双向链表的破坏。防止FD中的bk指针被修改或者BK的fd指针被修改

补充2
- int_free的全部代码会在实验三的补充中给出,现在先来看看int_free中前向合并和后向合并的代码,在补充中的第162行到180行(这个必看,有助于后面的理解)
1 | /* consolidate backward */ /**/ |
- 接下来查看
malloc_consolidate这个函数的具体功能,这里聚焦到51-56行是合并前向块时更新size,合并后向块时更新size61-64行,这里有兴趣的话还可以看看第82行到第86行,这几行介绍的是合并后的堆块是top chunk,则会更新av->top并设置新的大小:
1 | static void malloc_consolidate(mstate av) |
实验3
- 前提说明,glibc没办法判断这个位置是不是chunk,他是根据
prev_size和size确定堆块的,所以可不可以将某个fd、bk指针指向别的地址,然后申请堆块堆我们所构造的地址进行写呢? - 接下来编译运行实验代码查看运行结果,想一想为什么会这样,然后进行动态调试和画图进一步理解这一过程。
- 再思考一下为什么要在申请的堆块中伪造一个堆块,利用现有的堆块难道不行吗?
1 |
|
分析3.1
- 使用gcc编译后先运行一下该程序,然后得到结果,然后思考一下为什么会得到该结果

- 接下来我们使用
gdb进行动态调试,使用ni指令将程序运行到第四次malloc之后main+102处

- 使用
heap -v指令查看堆块,现在堆块还没有被修改
- 然后再使用
ni指令,将程序运行到free之前的一个语句

- 然后再使用
heap -v查看堆块,这回我们发现,我们在p1指向的堆块内容中伪造了一个堆块,然后修改了p2指向堆块的prev_size和size位

- 使用
ni指令,再将程序运行到free之后

- 使用
heap -v查看堆块,发现bk即伪造的size的值变成了0x211,说明进行了unlink并且还修改了size,而且fake_fd和fake_bk(即图中fd_nextsize和bk_nextsize)都指向了 0x7f3bc8f38b78(即main_arena+88)

- 接下来再使用
bins命令查看,我们发现main_arena+88并不是0x9a9440而是0x9a9450即我们伪造的堆块开始的地址,这也说明了unlink

- 然后接下来再动态调试到
1 | 0x7f3ac4ad8fe0 <_int_free+640> ✔ jne _int_free+2048 <_int_free+2048> |
- 使用
x/20gx 0x600b10-0x20和unsortedbin
1 | pwndbg> x/20gx 0x600b10-0x20 |
- 最后经过调试得到,a[5]的值在该语句附近会被改变,但是
unsortedbin还是空的,所以是在链表放入unsortedbin之前被劫持的
1 | 0x7f18ae011f71 <_int_free+529> cmp rbx, qword ptr [rdx + 0x10] |
分析3.2
-
接下来画图进行分析
unlink的这个具体过程,首先先来说明一下unlink attack的具体条件- 需要有一个指向
malloc所申请的堆块的指针,并且知道该指针的地址(chunk_ptr_addr) - 还要需要对指针地址前面地址可写即(
chunk_ptr_addr-0x18),这样就可以做到绕过检查机制并实现unlink attack
- 需要有一个指向
-
接下来画图解释(地址就以分析1中得到的地址为准),所做的伪造堆块的前提准备是这样的

- 然后当free(p2)时p2先进入unlink之前,堆块结构如下

- 此时P为p2指向的堆块,这时堆块会判断是前向合并还是后向合并,
consolidate backward是向前合并(虽然有backward,但是这是从当前块合并前一个块)- 这时就会通过
chunk_at_offset()这个更新p指针,通过prevsize大小作为偏移和以及当前指针p2(也就是chunk_at_offset(p, -((long) prevsize))中旧的p指针),将p指针更新为前一个堆块的指针 - 由于我们利用溢出和伪造了一个chunk,这时我们的
p指针就会跟新到fake_chunk的位置。
- 这时就会通过
1 | /* consolidate backward */ |

- 此时检查机制就会被绕过,因为
FD的bk会指向P,然后BK的fd也会指向P,通过我们伪造的FD指针和BK指针就会绕过相关的检查。该检查如下:
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) |

- 绕过检查机制后,修改堆块执行完下面语句后就会出现a[5]就等于
0x600b10即a[2]的地址,实验过程即为这些
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) |

- 在题目中往往会有一个
指针数组用来存储对应结构体的指针,我们对堆块的增删改查都是利用这个指针数组里面存储的指针来进行操作的。 - 所以我们就可以利用
a[5]这个指针来修改a[2]、a[3]、a[4],甚至修改a[5]指针自己本身,这样我们就可以修改指针,使其指向任意地址 - 这时我们把该地址修改成
got表所在的地址,然后show一下将got表的值打印出来,这就会导致libc中的地址泄露。
- 我们已经修改了指针的值为got表,那我们就可以利用修改后的指针,去修改got表的值,将其修改为
system函数的地址。然后再构造一个/bin/sh即可getshell

补充3
int_free源码
int_free
1 | static void |
利用方法
题目1_level_1
- 该题目来自
hitconTraining_unlink - 本题编译环境要在
glibc2.23下编译(ubuntu16.04),建议使用Docker环境进行编译和进行打 - 附件:https://wwsq.lanzoue.com/iLpz32cj7opa 密码:7mka
源码
1 | // gcc bamboobox.c -o bamboobox |
分析1_1
- 先使用IDA打开并查看二进制文件,查看一下程序运行的逻辑怎么样
- 先查看main函数,发现程序先开辟了
0x10个字节的堆空间,然后就是增删改查操作

- 下面会看到
itemlist这个结构体数组,现在先来看一下这个数组和该结构体:有俩个数据类型,分别是整型和字符串指针

- 接下来还会看到一个名为
itemlist结构体数组,该数组100个长度

- 接下来查看一下增删改查操作,也就打印出
itemlist中的name

- 之后再查看增,也就是开辟用户输入的内存空间,然后将内容输入进去

- 再来查看改,这里存在堆溢出,主要是修改程序指定的堆块,重新指定长度并重新输入内容

- 删,这里
free了堆块,然后又将指针置0了,所以不存在uaf漏洞

- 综合分析:可以得到该程序存在堆溢出漏洞,但没有uaf漏洞,并且
itemlist还存在着指向申请堆块内存的指针,这样就可以
利用1_1
-
已知程序在开头就已经申请了
0x10个字节了,但是这个堆块并没有什么用 -
我们需要申请3个堆块,第1个、第2个堆块尽量都申请free后能放入
unsortedbin的这个链表 -
然后第3个随便申请一个堆块就可以了(这里申请第3个堆块的原因是,防止free第二个堆块时,第二个堆块与第一个堆块合并之后再被合并入top_chunk中)
-
这时再使用
change函数修改堆块造成堆溢出,刚好具有现成的指向第一个堆块的指针(即itemlist[0].name) -
gdb动态调试查看,先申请一个堆块,使用
x/20gx 0x6020C0查看这个结构体数组,发现该指针在0x6020c0+0x8的这个位置
1 | pwndbg> x/20gx 0x6020C0 |
- 所以就可以利用如下所示伪造堆块、堆溢出、unlink_attack直接任意地址写

- 利用:
1 | from pwn import * |
分析1_2
- 释放之后的指针就指向该位置

- 这时我们使用
change函数,修改chunk_ptr_addr的数据为atoi的got表,然后再使用show打印出来atoi的地址

利用1_2
- 泄露libc,exp:
1 | from pwn import * |
- 泄露结果:

- 接下来是接收问题了,这里不多做解释
利用1_3
- 找到libc地址后,去libc_database找偏移

- 计算偏移,然后再使用
change修改got表为System,最后传入参数/bin/sh然后getshell,程序以为调用的是atoi但实际上是调用system

- 完整exp:
1 | from pwn import * |

