イントロ
最近 CTF を辞めたので、いつぞや開催された HITCON CTF 2020 の rev 問であるSOPを解いていく。 rev 問、やっぱ、むずい。けど、ほえ〜となる良い問題でした。
問題概要
バイトコードとそのインタプリタ本体が与えられる。本体の方はバイトコードを 8byte ずつ読み取り、そのオペコードに応じて RDI/RSI/RDX/R10/R8/R9 及び RAX に値を格納し、syscall を呼び出す。ひたすらこれを繰り返すプログラムである。
python で実装したエミュレータは以下の通り。
emulator.py 1#!/usr/bin/env python
2#encoding: utf-8;
3
4from pwn import *
5import sys
6
7# this file is some thing like:
8# 0 read
9# 1 write
10# 2 open
11# 3 close
12with open('syscalls.txt', 'r') as f:
13 lines = f.readlines()
14syscalls = []
15for l in lines:
16 syscalls.append(l.split("\t")[1])
17
18def ex(inst, n):
19 a = "0b0"
20 for i in range(n):
21 a = a+"1"
22 return inst & int(a, 2)
23
24# read byte_code
25with open('sop_bytecode', 'rb') as f:
26 bytecode = f.read()
27
28ip = 0 # pc in interpreter
29rax = 0 # rax
30regs = [0, 0, 0, 0, 0, 0] # rdi, rsi, rdx, r10, r8, r9
31access_prctl = [] # for debug
32inst_ex = [] # for debug
33prctl_arg = [] # for debug
34prctl_names = {0x28: "GET_TID_ADDR", 0x26: "SET_NO_NEW_PRIVS", 0x16: "PR_SET_SECCOMP", 0xf: "PR_SETNAME", 0x10: "PR_GET_NAME"}
35
36set_tid = 0 # previously set clear_child_tid
37#workspace = 0x60 # workspace on stack, used in sig_action handler
38workspace = 0x50 # workspace on stack, used in sig_action handler
39original_workspace = workspace
40
41# represents memory, both on stack and .bss region
42class mem:
43 def __init__(self):
44 self.mem = {}
45 self.initmem(self.mem, 0x217000, 0x100) # .bss
46 self.initmem(self.mem, 0, 0x8*100) # stack
47
48 def initmem(self, mem, start, size):
49 for i in range(size):
50 mem[start + i] = 0
51
52 def show(self):
53 for addr, value in self.mem.items():
54 if addr%8 == 0:
55 print("\n{}\t".format(hex(addr)[2:].rjust(8,'0')), end=" ")
56 print(hex(value)[2:].rjust(2,'0'), end=" ")
57 print("")
58
59 def getmem(self, addr, size=8):
60 val = 0
61 for i in range(size):
62 val += self.mem[addr+i] << (8*i)
63 return val
64
65 def setmem(self, addr, value, size=8):
66 for i in range(size):
67 self.mem[addr+i] = (value>>(0x8*i)) & 0xFF
68
69 def setstr(self, addr, yourstr):
70 for i in range(len(yourstr)):
71 self.mem[addr+i] = ord(yourstr[i])
72
73m = mem()
74
75
76flag = "A"*0x20
77#flag = "hitcon{SysCallOP57289ca4ce57585}"
78
79# print regs
80def pregs():
81 global regs
82 if syscalls[rax] not in inst_ex:
83 inst_ex.append(syscalls[rax])
84
85 if syscalls[rax] != "prctl":
86 print("{} {}({})".format(hex(ip//8)[2:].rjust(3,'0'), syscalls[rax].rjust(17,' '), hex(rax)), end=" ")
87 else:
88 print("{} {}[{}]".format(hex(ip//8)[2:].rjust(3,'0'), syscalls[rax].rjust(17,' '), prctl_names[regs[0]]), end=" ")
89 for reg in regs:
90 print(hex(reg), end=" ")
91 print("")
92
93comm = [0, 0] # current->comm(char[0x10])
94
95
96######## start of emulation ######################
97
98while True:
99 # handle ########################
100 def handle():
101 global rax
102 global regs
103 global set_tid
104 global comm
105 global mem
106 global workspace
107
108 if syscalls[rax] == "set_tid_address":
109 set_tid = regs[0]
110
111 if syscalls[rax] == "prctl":
112 if regs[0] == 0x28: # gettid
113 if set_tid != 0:
114 m.setmem(regs[1], set_tid)
115 access_prctl.append(regs[1])
116 elif regs[0] == 0xf: # setname
117 comm = [m.getmem(regs[1]), m.getmem(regs[1] + 8)]
118 elif regs[0] == 0x10: # getname
119 m.setmem(regs[1], comm[0])
120 m.setmem(regs[1] + 8, comm[1])
121
122 if syscalls[rax] == "getgid":
123 res = regs[0] & regs[1]
124 res &= 0xFFFF
125 m.setmem(workspace, res, 2)
126 workspace += 2
127
128 if syscalls[rax] == "getuid":
129 res = regs[1] >> regs[0]
130 res &= 0xFFFF
131 m.setmem(workspace, res, 2)
132 workspace += 2
133
134 if syscalls[rax] == "getpid":
135 res = regs[1] + regs[0]
136 res &= 0xFFFF
137 m.setmem(workspace, res, 2)
138 workspace += 2
139
140 if syscalls[rax] == "getegid":
141 res = regs[1] - regs[0]
142 res &= 0xFFFF
143 m.setmem(workspace, res, 2)
144 workspace += 2
145
146 if syscalls[rax] == "getpgrp":
147 res = regs[1] * regs[0]
148 res &= 0xFFFF
149 m.setmem(workspace, res, 2)
150 workspace += 2
151
152 if syscalls[rax] == "getppid":
153 res = regs[1]
154 if regs[0] > 0x32:
155 res = 0
156 else:
157 for i in range(regs[0]):
158 res <<= 1
159 res &= 0xFFFF
160 m.setmem(workspace, res, 2)
161 workspace += 2
162
163 if syscalls[rax] == "geteuid":
164 res = regs[1] ^ regs[0]
165 res &= 0xFFFF
166 m.setmem(workspace, res, 2)
167 workspace += 2
168
169 if syscalls[rax] == "read":
170 assert(regs[0] == 0) # stdin
171 assert(len(flag)==0x20)
172 print("entering your input (maybe flag??) size 0x20\n")
173 m.setstr(regs[1], flag)
174
175 if syscalls[rax] == "rt_sigaction":
176 print("rt_sigaction is called now")
177 #m.show()
178
179 # end handle ########################
180
181 inst = u64(bytecode[ip: ip+8])
182 if inst == 0:
183 break
184 #for i in range(len(regs)):
185 # regs[i] = 0
186
187 rax = ex(inst, 8)
188 inst = inst>>0x8
189
190 for i in range(6):
191 addr = 0
192 opc = ex(inst, 2)
193 inst = inst >> 2
194
195 if(opc == 0):
196 addr = ex(inst, 4)
197 inst = inst >> 4
198 regs[i] = m.getmem(addr * 8)
199 elif(opc == 1):
200 offset = ex(inst, 4)
201 inst = inst >> 4
202 regs[i] = offset * 8 # stack memory base is regarded as 0 in this parser
203 elif(opc == 2):
204 tmp = ex(inst, 5)
205 inst = inst >> 5
206 regs[i] = ex(inst, tmp+1)
207 inst = inst >> (tmp+1)
208 else:
209 break
210
211
212 ip += 8
213 pregs() # show registers
214 handle() # handle syscall
215
216 # end of bytecode
217 if(ip >= len(bytecode)):
218 # dump memory
219 m.show()
220 print(inst_ex)
221 # dump message
222 for i in range(0x30):
223 print(chr(m.getmem(0x217050 + i, 1)), end="")
224 print("")
225 exit()
226
227 if ip//8 == 0x370:
228 m.show()
229 for i in range(0x80):
230 print(chr(m.getmem(original_workspace + i, 1)), end="")
231 if(ip % 0x100 == 0 and ip!=0):
232 print("PC: {}".format(hex(ip)))
bytecode
.txt1001 set_tid_address(0xda) 0x217000 0x0 0x0 0x0 0x0 0x0
2002 prctl(0x9d) 0x28 0x0 0x0 0x0 0x0 0x0
3003 mmap(0x9) 0x217000 0x1 0x7 0x22 0x0 0x0
4004 read(0x0) 0x0 0x217000 0x20 0x22 0x217000 0x217000
最初に set_tid_address(0x21700)
を呼んでいる。これは、current->clear_child_tid
を指定した値に設定するシスコールである。
直後の prctl(GET_TID_ADDRESS)
では先程格納した current->clear_child_tid
を第 2 引数で指定したユーザランド領域にコピーする。
これによって、直接的に mov
命令を呼び出すことなく syscall 経由で値をメモリ中に移すことができる。今回の場合は、アドレス 0x0
に対して値 0x217000
を mov
したことになる。
なお、プログラム中ではスタック中の一部の領域を作業領域として割り当てているが、python パーサにおいてはこのアドレスを 0x0
としている。
その後、アドレス 0x217000
を RWX で mmap()
し、割り当てた領域に対してユーザから 0x20
だけ read()
している。
1005 set_tid_address(0xda) 0x217050 0x217000 0x217000 0x217000 0x217000 0x217000
2006 prctl(0x9d) 0x28 0x60 0x217000 0x217000 0x217000 0x217000
3007 set_tid_address(0xda) 0x217020 0x60 0x217000 0x217000 0x217000 0x217000
4008 prctl(0x9d) 0x28 0x0 0x217000 0x217000 0x217000 0x217000
5009 set_tid_address(0xda) 0x217054 0x0 0x217020 0x217020 0x217020 0x217020
600a prctl(0x9d) 0x28 0x60 0x217020 0x217020 0x217020 0x217020
700b set_tid_address(0xda) 0x0 0x60 0x217020 0x217020 0x217020 0x217020
800c prctl(0x9d) 0x28 0x0 0x217020 0x217020 0x217020 0x217020
900d set_tid_address(0xda) 0x217058 0x0 0x217020 0x217020 0x217020 0x217020
1000e prctl(0x9d) 0x28 0x68 0x217020 0x217020 0x217020 0x217020
1100f set_tid_address(0xda) 0x4000004 0x68 0x217020 0x217020 0x217020 0x217020
12010 prctl(0x9d) 0x28 0x0 0x217020 0x217020 0x217020 0x217020
13011 set_tid_address(0xda) 0x21705c 0x0 0x4000004 0x4000004 0x4000004 0x4000004
14012 prctl(0x9d) 0x28 0x18 0x4000004 0x4000004 0x4000004 0x4000004
15013 set_tid_address(0xda) 0x0 0x18 0x4000004 0x4000004 0x4000004 0x4000004
16014 prctl(0x9d) 0x28 0x4 0x4000004 0x4000004 0x4000004 0x4000004
17015 set_tid_address(0xda) 0x217060 0x4 0x4000004 0x4000004 0x4000004 0x4000004
18016 prctl(0x9d) 0x28 0x18 0x4000004 0x4000004 0x4000004 0x4000004
19017 set_tid_address(0xda) 0x217044 0x18 0x4000004 0x4000004 0x4000004 0x4000004
20018 prctl(0x9d) 0x28 0x4 0x4000004 0x4000004 0x4000004 0x4000004
その後はひたすら get_tid_address/prctl
を繰り返すことでメモリ中に値を書き込んでいく。
これがインタプリタ中の IP==0x52
まで続き、その時点でのメモリは以下のような感じ。
(最初の入力として "A"*0x20
を与えた場合)
.txt
1053 rt_sigaction(0xd) 0x1f 0x217050 0x0 0x8 0xcccc050f 0x217050
この後、rt_sigaction
が呼ばれる。int signum
は 0x1F==SIG_SYS
であり、struct sigaction *act
は 0x217050
になっている。
sa_sigaction()
は以下のような命令列である。
一番最初に RCX に入れる値が壊れているが、後々以下のように書き換えられる。
.S12a2 set_tid_address(0xda) 0x30 0x0 0xffffffff 0xffffffff 0xffffffff 0xffffffff
22a3 prctl(0x9d) 0x28 0x217022 0xffffffff 0xffffffff 0xffffffff 0xffffffff
32a4 set_tid_address(0xda) 0xffffffff 0x217022 0xffffffff 0xffffffff 0xffffffff 0xffffffff
42a5 prctl(0x9d) 0x28 0x0 0xffffffff 0xffffffff 0xffffffff 0xffffffff
52a6 set_tid_address(0xda) 0x2172840000 0x0 0xffffffff 0xffffffff 0xffffffff 0xffffffff
62a7 prctl(0x9d) 0x28 0x8 0xffffffff 0xffffffff 0xffffffff 0xffffffff
結果として、先程のsig_action
は以下のように書き換えられる。
.c
1rcx = workspace; # R9レジスタの場所の次
2*(short*)rcx = *(short*)(rsi+4);
3*(long*)(0x217022) += 2;
書き換えられた結果、先程のアドレスはスタック上のメモリ領域になっていることが分かる。
ここは、インタプリタ内部の R9 レジスタの次の領域である。
この領域を特別に workspace と名付けておくことにする。
2 番目のコードによってworkspaceに 2byte 分データが書き込まれた後、
3 番目のコードによって 1 番目のコード自体のオペランド部分をを書き換えている。
これによって rcx=workspace
の命令は rcx=workspace+2
となり、
このハンドラを実行する度に workspace + 2*n
番地に値を書き込むようになる(mutable instruction)。
1295 prctl[PR_SET_SECCOMP] 0x16 0x2 0x217050 0x217274 0x217274 0x217274
また、rt_sigaction
の直後に上のように seccomp が設定される。
RSI==0x2
より、seccomp_mode
はSECCOMP_MODE_FILTER
、
設定されるフィルタは0x217050
においてある。
生成される BPF をディスアセンブルすると以下の通り。
(書いてて気づいたけど、この問題の author、seccomp-tool の author じゃん。。。。)
fork/write/getgid/getpid/gettid/getegid/getpgrp/getppid/geteuid
に対してフィルタがかかっていて、呼び出した際の RDI/RSI に対して演算を行うようになっている。
- getgid -> &演算
- getuid -> 右シフト演算
- gettid -> OR 演算
- getpid -> 加算
- getegid -> 減算
- getpgrp -> 乗算
- getppid -> 左シフト演算
- geteuid -> XOR 演算
- fork -> 除算
演算の後、return (演算結果) | SECCOMP_RET_TRAP
をすることによってSIGSYS
を raise している。
これによって、処理は先程のsig_action()
へと移り、そこでworkspace+2*n
に対して演算結果を格納するようになっている。
seccomp をした後は以下のように続く:
.S 1296 set_tid_address(0xda) 0x69a33fff 0x2 0x217050 0x0 0x0 0x0
2297 prctl[GET_TID_ADDR] 0x28 0x10 0x217050 0x0 0x0 0x0
3298 set_tid_address(0xda) 0x468932dc 0x10 0x217050 0x0 0x0 0x0
4299 prctl[GET_TID_ADDR] 0x28 0x18 0x217050 0x0 0x0 0x0
529a set_tid_address(0xda) 0x2b0b575b 0x18 0x217050 0x0 0x0 0x0
629b prctl[GET_TID_ADDR] 0x28 0x20 0x217050 0x0 0x0 0x0
729c set_tid_address(0xda) 0x1e8b51cc 0x20 0x217050 0x0 0x0 0x0
829d prctl[GET_TID_ADDR] 0x28 0x28 0x217050 0x0 0x0 0x0
929e prctl[PR_SETNAME] 0xf 0x217000 0x217050 0x0 0x0 0x0
1029f prctl[PR_GET_NAME] 0x10 0x30 0x217050 0x0 0x0 0x0
112a0 set_tid_address(0xda) 0xffffffff 0x30 0x217050 0x0 0x0 0x0
重要なのは prctl[PR_SETNAME]
のところで、引数が最初に read()
でユーザから入力した値になっている。
これによって、current->comm
がユーザ入力値になる。(comm
はchar[TASK_COMM_LEN==0x10]
だから入力値の半分だけがプロセス名になる)
そのあと、入力値をメモリ(これは、スタック上に確保されるメモリ)のオフセット0x30
に prctl[PR_GETNAME]
している。
このアドレスは、先程のsig_action()
の一番最初の命令で読み込まれるアドレスであった。
これ以降は、先程 seccomp で設定したシスコールを呼ぶことで諸々の演算を行ったり、
上に示したようにGETNAME
で値を 8byte まるごとコピーしたりしながら続いていく。
一番最後に以下のように write
を呼んで終わり:
170a write(0x1) 0x217020 0x217050 0x24 0x0 0x0 0x0
270b write(0x1) 0x217020 0x217000 0x21 0x0 0x0 0x0
え、配布コード間違えてね?と一瞬思ったけどそんなはずもなく、ただただ自分の無力を恨みながら寝ることにします、おやすみなさい
あとは上の bpf の演算表を用いて計算していくだけのような気がしたんですが、結局どういう状態になれば正解でどういう状態になると不正解なのかわかりませんでした。
一番最後の write 及びその直前は、以下のようなコードになっています:
.S1705 prctl[GET_TID_ADDR] 0x28 0x217070 0x10 0x0 0x0 0x0
2706 set_tid_address(0xda) 0x217020 0x217070 0x10 0x0 0x0 0x0
3707 prctl[GET_TID_ADDR] 0x28 0x10 0x10 0x0 0x0 0x0
4708 set_tid_address(0xda) 0xa 0x10 0x10 0x0 0x0 0x0
5709 prctl[GET_TID_ADDR] 0x28 0x217020 0x10 0x0 0x0 0x0
670a write(0x1) 0x217020 0x217050 0x24 0x0 0x0 0x0
770b write(0x1) 0x217020 0x217000 0x21 0x0 0x0 0x0
おそらく最後のwrite
2 つは、入力値が正しくないと seccomp 中の fd!=0
の条件(ここでfd
はメモリ[0x10]
)で死ぬということだと思ったんですが。
但し、その直前の706/707
でメモリ[0x10]
に0xA
(改行)を入れる操作をしているため、
最後の write は必ず条件を満たさず死ぬことになります。
ここで作問者の writeup にあるバイトコード生成プログラムを見てみると、最後のwrite
の直前で以下のようなことをしています:
1put_val("\n".ord, INPUT_AT + INPUT_SIZE)
これによって flag の最後に改行を加えます。このput_val
のコードを見てみると:
1srand(333)
2# val must be 32-bit
3def put_val(val, addr)
4 idx = rand(14) # 0~13
5 set_reg(idx, addr)
6 scall(:set_tid_address, val)
7 scall(:prctl, PR_GET_TID_ADDRESS, Reg.new(idx))
8end
rand で帰ってきた値のインデックスを持つレジスタを媒介にして操作を行っています。本来はここでreg[2]
を除外するべきなような感じが。
結果、生成されたバイトコードだとreg[2]
を中継として使用することになっているので、reg[2]=0xA
となり、如何なる入力値を与えたとしても最後のwrite
で死ぬようになっています。
ただ、このコードなかでrand
は 1 回しか使われておらず、srand(333)
の時の最初のrand(14)
の値は12
になるはずなので、
なんで2
がバイトコードなかで使われているのかちょっと不思議でした。
まぁ、Ruby の乱数の仕組みよく知らんので知らんけど。
と思ったんですが、10+チームということは guessing 要素がある確率は低いと思うので、多分僕の勘違いだと思います。 というわけで、結局 author’s writeup を見ても、結局何がどういう条件になれば OK なのかわかりませんでした。シェルを取れば良いんでしょうか。誰か rev の解き方を教えてください。
なんか話が途中になりましたが、結局何がどうなれば正解なのかを突き止めるのに 2 時間くらい費やしてしまってもう眠くなったので寝ます。writeup は作問者様の github にあります。あとは、ひたすらやってる演算を逆演算するだけっぽいです。rev むずいです。
良い問題でした。けど rev 問はやっぱよくわからん。 おやすみなさい。