花与堆之Unlink
Overview
- unlink俗称脱链,就是将链表头处的free堆块从unsorted bin中脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或向后合并),再放入到unsorted bin中。
- 危害原理:通过伪造free状态的fake_chunk,伪造
fd和bk指针,通过绕过unlink的检测实现unlink使其往p所在的位置写入p-0x18,从而实现任意地址写的漏洞。 - 漏洞产生原因:
Offbynull、offbyone、堆溢出,原因是修改了堆块的使用标志位。
源码解读
1 | |
进行判断:看当前堆块中
p这个标志位,如果p设置为0则为free状态,则进行unlink,否则反之;先提取prev_size,然后当前size+prev_size,此时指针会指向当前chunk的前一个堆块,合并后的指针地址为:free的堆块地址 - 前一个chunk大小,此时
p指针则会从现在的堆块跳到前一个堆块;
1 | |
- 最后是将这个堆块和相邻的(这里是上一个)一起unlink。
1 | |
unlink函数是如何定义的:
从合并后新指针地址中提取出
fd指针和bk指针作为临时变量;这里有一个check,检查FD的bk和BK的fd是否指向当前堆块,若不通过则不进行unlink;
1 | |
- 通过后会把BK赋值给FD的bk,把FD赋值给BK的fd。

unlink的绕过和利用
我们伪造如下信息:
- chunk = 0x0602280 (P是将要合并到的堆地址,P存在chunk中,相当于
*chunk = P) - P_fd = chunk - 0x18 = 0x0602268
- P_bk = chunk - 0x10 = 0x0602270
我在学习的过程中此处卡住了,对于为什么是减去
0x18和0x10这两个值我们在此复习一下为什么是减去0x18和0x10,在 glibc 的 malloc 实现(ptmalloc)中,在释放前、不在 bin 中时,chunk 结构为:
1
2
3
4
5
6
7struct malloc_chunk {
size_t prev_size; // 0x00 偏移(如果前一个块空闲,才有用)
size_t size; // 0x08 偏移(包含标志位)
struct malloc_chunk* fd; // 0x10 偏移(仅在 bin 中使用)
struct malloc_chunk* bk; // 0x18 偏移
// ... 更后面还有 fd_nextsize, bk_nextsize(large bin)
};而回顾上面的内容有这样的一条:”通过后会把BK赋值给FD的bk,把FD赋值给BK的fd。”
绕过技巧
1 | |
- 绕过检查可以总结成:$FD->bk == P$ 和 $BK->fd == P$,等价于: $(P_fd + 0x18) == P$ 和 $(P_bk + 0x10) == P$
- 可以构造成
1 | |
即:
1 | |
总结起来就是:让 P->fd 指向 P - 0x18,P->bk 指向 P - 0x10,就能绕过 FD->bk == P 和 BK->fd == P 检查,并使 \*P 被覆写为 P - 0x18。
2014 HITCON stkof
- 堆布局
- 伪造 fake chunk
- fd/bk = ptr-0x18/ptr-0x10
- 修改 next chunk 的 prev_size/size
- unlink 写全局指针
- 写 GOT 表项
- 先 leak 后 getshell
EXP:
1 | |
HITCON Training Lab - Unlink
Checksec
1 | |
NO PIE
分析
main函数
1 | |
然后ida反编译查看main函数,各功能一目了然。注意到每次输入choice后,都要通过atoi()函数来将其转为整型,这是漏洞利用的关键之一;
show_item:
1 | |
- 这里存在offbyone,但对于考察unlink的题目一般不会利用;
&unk_6020c8位于bss节,是items的基址
add_item:
1 | |
add_item函数中,先输入一个长度v2,然后遍历bss中的空间(基址为0x6020c8),如果有空,则申请一块v2大小的chunk(这里所说的chunk大小不包括chunk头),将其地址写入bss。再输入一个字符串,将前v2个字节作为item名称写到chunk中。line 30的read函数返回实际读取的字节数,加上该字符串基址就是字符串的末尾,结尾置0表示字符串结束;
change_item:
1 | |
change_item函数负责给编号为v1的item改名,方法和add_item中完全一致。这也是堆溢出所在,因为我们输入的length如果超过该chunk的大小,就可以溢出到其他chunk中;
remove_item:
1 | |
这个函数中存在free()功能。
之后按照如下思路:
- 堆布局
- 伪造 fake chunk
- fd/bk = ptr-0x18/ptr-0x10
- 修改 next chunk 的 prev_size/size
- unlink 写全局指针
- 写 GOT 表项
- 先 leak 后 getshell
给一下自己的一些辅助学习方法:
Unlink过程
- FD = 0x2000 (fake_chunk.fd)
- BK = 0x2008 (fake_chunk.bk)
- FD->bk = BK → *(0x2000 + 0x18 = 0x2018) = 0x2008
- BK->fd = FD → *(0x2008 + 0x10 = 0x2018) = 0x2000
- 最终: 0x2018 = 0x2000 (itemlist[0]被修改)

EXP
1 | |
References
CTF-Wiki
CSDN
博客园
Blogs
先知
看雪
Bilibili