Unlink漏洞原理及利用(Unsortedbin)
什么是unlink?
unlink 俗称脱链,就是将链表头处的 free 堆块 unsorted bin中脱离出来,然后和物理地址相邻的新 free的堆块合并成大堆块(向前合并或者向后合并)再放入unsorted bin中。
危害原理:通过伪造free状态的fake_chunk,伪造fd指针和bk指针,通过绕过unlink的检查实现unlink,unlink就会往p所在的位置写入p-0x18,从而实现任意地址写的漏洞。
漏洞产生原因:offbynull,offbyone,堆溢出修改了堆块的标志位
我们所称的unlink其实就是在双向链表中取出一个chunk的操作,那么什么时候会用到unlink呢?
1)malloc
- 在恰好大小的large chunk处取chunk时
- 在比请求大小大的bin中取chunk时
2)free
- 后向合并,合并物理相邻低物理地址空闲chunk时
- 前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)
3)malloc_consolidate
- 后向合并,合并物理相邻低地址空闲chunk时。
- 前向合并,合并物理相邻高地址空闲chunk时(top chunk除外)
4)realloc
- 前向扩展,合并物理相邻高地址空闲chunk(top chunk除外)
堆复习
在学习unlink之前再复习一下堆
我们之前学习到,P=1表示物理相邻的前一个堆块正在被使用,当P=0时两个堆块就会合并成一个大堆块,并放在unsorted bin这个链表中

- 下面用图介绍一下
main_arena并给出一些在本题中比较重要的一些东西:- unlink过程主要脱的是bins中管理的堆块链表
- 当unlink结束后新合成的堆块会与top_chunk的地址相连还会与top_chunk合并
通过实例介绍unlink
简单了解free后bin的存放
- 对下面的程序进行动态调试,并思考以下问题
- 1)申请的堆块释放后会被哪个bins管理?2)p1和p2会发生合并吗?3)p2和p3会发生合并吗?
1 |
|
程序进行编译之后用gdb进行调试并ni单步执行到<main+64>
用heap指令查看堆块,直接看堆块的前缀标识
- Allocated chunk:表示这个堆块是正在被使用的(已分配,未被 free);
- Top chunk:是堆的 “顶部空闲块”,属于空闲状态(未被分配,用于后续 malloc 扩展)。
发现一共申请了四个堆块并且四个堆块都在使用中
再次单步执行到第一个free之后,第二个free之前看一看堆块情况
发现第一次被释放的chunk0被归入到了unsortedbin
再次执行到第二个free之后第三个free之前看一看堆块的情况
发现第二个chunk也被放入了unsortedbins
查看unsortedbin发现bin上有两个链条且没有合并,按照后进先出的原则
看一下第三次free之后的情况,发现后两个堆块被合并到Top chunk中,topchunk的addr也发生变化
因此这些堆块释放后都会被unsortedbin管理
p1与p2两个指针的堆块是不会合并的,并且只有物理地址相邻且空闲的堆块会被合并
p2与p3这两个指针指向的堆块是会合并的
unlink的检查过程
通过分析glibc源码,并使用图描述unlink的过程,具体了解unlink的检查过程
在http://ftp.gnu.org/gnu/glibc/下载gilbc-2.23.tar.xz文件,来到malloc文件夹中的malloc.c
这里直接给代码放出来
我们这里简单了解前八行的内容:
在unlink中只是进行了脱链的操作,并没有修改堆块的size位,而堆块的size位是在malloc_consolidate这个函数中所修改的
1 | define unlink(AV, P, BK, FD) { \ |
**tip)**unlink的过程是指 chunk加入unsortedbin之前进行的,所以他们的fd、bk指针都是根据物理地址的高低来指向的
- fd 指向当前chunk的下一个空闲块,通常是物理内存地址较高的那个块
- bk指向当前chunk的上一个空闲块,通常是物理内存地址较低的那个块
- 当前chunk的指针fd&bk会被设置成NULL,表示没有后续和前面的空闲块
1 | FD = P->fd |

1 | FD->bk = BK; |
大概了解了脱链的一个步骤,我们再来看一看判断条件的代码
1 | if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ |
意思是在脱链之前,会检查加粗红线的链表是否指向要脱掉的链,可以防止双向链表的破坏,
防止FD中的bk被修改或者BK的fd指针被修改

unlink getshell的过程与实现
- glibc没办法判断这个位置是不是chunk,他是根据prev_size和size确定堆块的
tip)glibc如何识别chunk
从某个起始地址(比如当前 chunk 的末尾)读取prev_size和size的值;
完全按照这两个值来计算 “下一个堆块的起始地址”“前一个堆块的起始地址”,并执行后续操作。
unlink欺骗glibc过程
1 | # 64位系统,初始堆内存布局(地址为示例,核心看偏移) |
堆溢出篡改后的内存布局(代码块格式)
1 | # 篡改后堆内存布局(你通过change(0)写入payload的结果) |
当你调用remove(1)(即 glibc 执行free(chunk1)),glibc 会按固定逻辑检查前块是否可合并,步骤如下:
- 第一步:读取 chunk1 的 prev_size,找前块地址
- glibc 从 chunk1 起始地址 0x10A0 读取 prev_size,原始值是 0xa0;
- 计算前块地址:0x10A0 - 0xa0 = 0x1000(这是 chunk0 的合法起始地址);
- 读取 0x1008(chunk0 的 size),值为你伪造的 0x91,检查 P 位 = 1(表示前块被使用)
- 第二步:被伪造的 chunk1 prev_size 误导,重新计算
- glibc 会额外检查 “当前 chunk1 的 prev_size 是否与前块 size 匹配”;
- 它读取你伪造的 0x1090 位置的 prev_size=0x90(本应是 chunk0 用户区的内存,却被当成 chunk1 的 prev_size);
- 重新计算前块地址:0x10A0 - 0x90 = 0x1010( 这样就会把 chunk0 用户区的地址当成了 “前块起始地址”)。
- 第三步:把伪造地址当合法 chunk,执行 unlink
- glibc 认为 0x1010 是一个合法的 free chunk 起始地址;
- 读取 0x1010 的 fd(你伪造的 0x6020A8 )和 0x1018 的 bk(你伪造的 0x6020B0);
- 执行 unlink 核心逻辑:c运行
1 | // BK->fd = FD → 0x6020B0 + 0x8 = 0x6020B8(目标地址)写入0x6020A8 |
1 | - <font style="color:rgb(0, 0, 0);">此时</font>`<font style="color:rgb(0, 0, 0);">0x6020B8</font>`<font style="color:rgb(0, 0, 0);">(即你要篡改的</font>`<font style="color:rgb(0, 0, 0);">ptr=0x6020C0+0x8</font>`<font style="color:rgb(0, 0, 0);">)被写入指定值,实现任意地址写。</font> |
核心本质总结
- glibc 全程没有验证 “0x1010是不是系统分配的合法 chunk 起始地址”,只看内存中prev_size/size的数值;
- 你通过堆溢出把 “用户区内存” 伪装成 prev_size/ size字段,glibc 就 “机械地” 按这些伪造值计算地址、执行操作;
- 最终把本应操作堆块链表的写入,转移到了你指定的任意地址,这就是 unlink 攻击的核心。
unlink 攻击的巧妙之处:用真实堆块的头部触发逻辑,用假堆块的头部执行攻击,
两者的 prev_size 写法不同,正是因为它们的作用对象完全不同。
hitconTraining_unlink
main函数很清晰的标注整个链表能显示开辟了十个字节的堆空间,之后进行增删改查
先通过运行程序把伪代码逆向一下看看
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
1 |
|
1 | __int64 add_item() |
1 | unsigned __int64 change_item() |
基本思路
- 已知程序在开头申请了0x10个字节大小的堆块
- 我们需要申请三个堆块,并且第1,2个堆块尽量在free后能够放入unsorted bin链表
- 第三个堆块随便申请(防止free第二个堆块时,第二个堆块与第一个堆块合并之后再合并入top_chunk)
- 用change函数修改堆块造成堆溢出
- gdb动态调试查看,先申请一个堆块,使用
x/20gx 0x6020C0查看这个结构体数组,发现该指针在0x6020c0+0x8的这个位置
1 | pwndbg> x/20gx 0x6020C0 |
- 所以就可以利用如下所示伪造堆块、堆溢出、unlink_attack直接任意地址写

1 | from pwn import * |
更新: 2026-04-22 17:29:46
原文: https://www.yuque.com/idcm/wnemg9/zv9xwpzcugfee7pd