TSG LIVE! 8 CTF - dns_ropob
TL;DR
- ptraceが邪魔なのでLD_PRELOADで偽装した共有オブジェクトを噛ませて無効化する
- 元のコードがバラバラになるような難読化が施されているので集めて元に戻す
- 先頭から1文字ずつ正解かどうかを見ているので、総当たりするGDBスクリプトを書く
Prerequisite
- LD_PRELOADによる関数の差し替え
- GDBのスクリプティング
Writeup
実行すると、次のように入力を促してくる。
$ ./dns_ropob
FLAG >
適当に入力すると、"wrong!"
と言われてしまうのでここにフラグを入れると正解のメッセージが返って来そうである。
頼みのGhidraで眺めても解析が難しいのか芳しいデコンパイル結果は得られない。というわけでgdbやらstraceやらltraceやらで動きを追ってみようとするが、ptraceによってptraceが制限されているのか、こいつらが上手く動いてくれない。加えてデコンパイル結果を見る限りではptraceはどこからも呼ばれていないように見えるため、呼ばれている箇所をNOPで潰すようなことが出来ない。
というわけで何もしない偽のptraceを実行する共有オブジェクトを作ってLD_PRELOAD
にそれを指定することでこのチェックをバイパスする。次のページが参考になった。
gdbで適当に動きを追ってみるとpushf
やpopf
を駆使した謎の挙動を見せる。常に同じような事をしているように思えるが、よく観察してみると確かに周期的に命令が実行されているものの、main
セクションにて、ret
の後に実行される命令は毎度異なっていることがわかる。
Ghidraだと、関数のret
以降は指定しないとディスアセンブルしてくれないが、ret
以降のアドレスが実行されており、さらに自己書き換え等で元の命令列から書き換えられてもいないためObjdumpでディスアセンブルしてみると普通に命令列が得られる1。
ここからret
の後の命令だけ抜き取ってみると次のような結果が得られる。
1b9f: 48 89 e5 mov rbp,rsp
1bd8: 48 83 ec 40 sub rsp,0x40
1c12: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
1c51: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
1c8b: 31 c0 xor eax,eax
1cc3: c7 45 cc 00 00 00 00 mov DWORD PTR [rbp-0x34],0x0
1d00: b9 00 00 00 00 mov ecx,0x0
1d3b: ba 01 00 00 00 mov edx,0x1
1d76: be 00 00 00 00 mov esi,0x0
1db1: bf 00 00 00 00 mov edi,0x0
1dec: b8 00 00 00 00 mov eax,0x0
1e27: e8 84 e8 ff ff call 6b0 <ptrace@plt>
1e62: 48 83 f8 ff cmp rax,0xffffffffffffffff
1e9c: 0f 85 ac 00 00 00 jne 1f4e <main+0x41b>
1ed8: bf 01 00 00 00 mov edi,0x1
1f13: e8 b8 e7 ff ff call 6d0 <exit@plt>
1f4e: 48 8d 3d 45 07 00 00 lea rdi,[rip+0x745] # 269a <_IO_stdin_used+0x2a>
1f8b: e8 f0 e6 ff ff call 680 <puts@plt>
1fc6: 48 8d 45 d0 lea rax,[rbp-0x30]
2000: 48 89 c6 mov rsi,rax
2039: 48 8d 3d 61 06 00 00 lea rdi,[rip+0x661] # 26a1 <_IO_stdin_used+0x31>
2076: b8 00 00 00 00 mov eax,0x0
20b1: e8 0a e6 ff ff call 6c0 <__isoc99_scanf@plt>
20ec: 8b 55 cc mov edx,DWORD PTR [rbp-0x34]
2125: 48 8d 45 d0 lea rax,[rbp-0x30]
215f: 89 d6 mov esi,edx
2197: 48 89 c7 mov rdi,rax
21d0: e8 62 e6 ff ff call 837 <func>
220b: 89 45 cc mov DWORD PTR [rbp-0x34],eax
2244: 83 7d cc 00 cmp DWORD PTR [rbp-0x34],0x0
227e: 0f 85 24 01 00 00 jne 23a8 <main+0x875>
22ba: 48 8d 3d e5 03 00 00 lea rdi,[rip+0x3e5] # 26a6 <_IO_stdin_used+0x36>
22f7: b8 00 00 00 00 mov eax,0x0
2332: e8 69 e3 ff ff call 6a0 <printf@plt>
236d: e9 e9 00 00 00 jmp 245b <main+0x928>
23a8: 48 8d 3d 00 03 00 00 lea rdi,[rip+0x300] # 26af <_IO_stdin_used+0x3f>
23e5: b8 00 00 00 00 mov eax,0x0
2420: e8 7b e2 ff ff call 6a0 <printf@plt>
245b: b8 00 00 00 00 mov eax,0x0
2496: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8]
24d0: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28
250f: 74 71 je 2582 <main+0xa4f>
2547: e8 44 e1 ff ff call 690 <__stack_chk_fail@plt>
2582: c9 leave
25b9: c3 ret
関数のプロローグもエピローグもしっかり存在し、よく見るような構造のアセンブリコードが得られた。
これをざっくり読んでみるとfunc
という関数に入力を渡しているように思える。というわけでfunc
を見てみると、main
と同じような難読化が施されていたので同じようにしてret
直後の命令を取り出す。
86e: 48 89 e5 mov rbp,rsp
8a7: 48 83 ec 50 sub rsp,0x50
8e1: 48 89 7d b8 mov QWORD PTR [rbp-0x48],rdi
91b: 89 75 b4 mov DWORD PTR [rbp-0x4c],esi
954: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
993: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
9cd: 31 c0 xor eax,eax
a05: 48 8b 05 6c 1c 00 00 mov rax,QWORD PTR [rip+0x1c6c] # 2678 <_IO_stdin_used+0x8>
a42: 48 8b 15 37 1c 00 00 mov rdx,QWORD PTR [rip+0x1c37] # 2680 <_IO_stdin_used+0x10>
a7f: 48 89 45 d0 mov QWORD PTR [rbp-0x30],rax
ab9: 48 89 55 d8 mov QWORD PTR [rbp-0x28],rdx
af3: 48 8b 05 8e 1b 00 00 mov rax,QWORD PTR [rip+0x1b8e] # 2688 <_IO_stdin_used+0x18>
b30: 48 8b 15 59 1b 00 00 mov rdx,QWORD PTR [rip+0x1b59] # 2690 <_IO_stdin_used+0x20>
b6d: 48 89 45 e0 mov QWORD PTR [rbp-0x20],rax
ba7: 48 89 55 e8 mov QWORD PTR [rbp-0x18],rdx
be1: 0f b6 05 b0 1a 00 00 movzx eax,BYTE PTR [rip+0x1ab0] # 2698 <_IO_stdin_used+0x28>
c1e: 88 45 f0 mov BYTE PTR [rbp-0x10],al
c57: c6 45 ca 63 mov BYTE PTR [rbp-0x36],0x63
c91: c6 45 cb 71 mov BYTE PTR [rbp-0x35],0x71
ccb: c7 45 cc 00 00 00 00 mov DWORD PTR [rbp-0x34],0x0
d08: e9 1d 0c 00 00 jmp 192a <func+0x10f3>
d43: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
d7c: 99 cdq
db3: c1 ea 1f shr edx,0x1f
dec: 01 d0 add eax,edx
e24: 83 e0 01 and eax,0x1
e5d: 29 d0 sub eax,edx
e95: 83 f8 01 cmp eax,0x1
ece: 0f 85 29 05 00 00 jne 13fd <func+0xbc6>
f0a: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
f43: 48 63 d0 movsxd rdx,eax
f7c: 48 8b 45 b8 mov rax,QWORD PTR [rbp-0x48]
fb6: 48 01 d0 add rax,rdx
fef: 0f b6 00 movzx eax,BYTE PTR [rax]
1028: 0f be d0 movsx edx,al
1061: 0f b6 45 ca movzx eax,BYTE PTR [rbp-0x36]
109b: 89 d1 mov ecx,edx
10d3: 31 c1 xor ecx,eax
110b: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
1144: 48 63 d0 movsxd rdx,eax
117d: 48 8d 05 9c 1e 20 00 lea rax,[rip+0x201e9c] # 203020 <SEED1>
11ba: 0f b6 14 02 movzx edx,BYTE PTR [rdx+rax*1]
11f4: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
122d: 48 98 cdqe
1265: 0f b6 44 05 d0 movzx eax,BYTE PTR [rbp+rax*1-0x30]
12a0: 31 d0 xor eax,edx
12d8: 0f b6 c0 movzx eax,al
1311: 39 c1 cmp ecx,eax # check character 1
1349: 0f 84 a1 05 00 00 je 18f0 <func+0x10b9>
1385: c7 45 b4 01 00 00 00 mov DWORD PTR [rbp-0x4c],0x1
13c2: e9 d9 05 00 00 jmp 19a0 <func+0x1169>
13fd: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
1436: 48 63 d0 movsxd rdx,eax
146f: 48 8b 45 b8 mov rax,QWORD PTR [rbp-0x48]
14a9: 48 01 d0 add rax,rdx
14e2: 0f b6 00 movzx eax,BYTE PTR [rax]
151b: 0f be d0 movsx edx,al
1554: 0f b6 45 cb movzx eax,BYTE PTR [rbp-0x35]
158e: 89 d1 mov ecx,edx
15c6: 31 c1 xor ecx,eax
15fe: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
1637: 48 63 d0 movsxd rdx,eax
1670: 48 8d 05 a9 19 20 00 lea rax,[rip+0x2019a9] # 203020 <SEED1>
16ad: 0f b6 14 02 movzx edx,BYTE PTR [rdx+rax*1]
16e7: 8b 45 cc mov eax,DWORD PTR [rbp-0x34]
1720: 48 98 cdqe
1758: 0f b6 44 05 d0 movzx eax,BYTE PTR [rbp+rax*1-0x30]
1793: 31 d0 xor eax,edx
17cb: 0f b6 c0 movzx eax,al
1804: 39 c1 cmp ecx,eax # check character 2
183c: 0f 84 ae 00 00 00 je 18f0 <func+0x10b9>
1878: c7 45 b4 01 00 00 00 mov DWORD PTR [rbp-0x4c],0x1
18b5: e9 e6 00 00 00 jmp 19a0 <func+0x1169>
18f0: 83 45 cc 01 add DWORD PTR [rbp-0x34],0x1
192a: 83 7d cc 1f cmp DWORD PTR [rbp-0x34],0x1f # check index
1964: 0f 8e d9 f3 ff ff jle d43 <func+0x50c>
19a0: 8b 45 b4 mov eax,DWORD PTR [rbp-0x4c]
19d9: 48 8b 75 f8 mov rsi,QWORD PTR [rbp-0x8]
1a13: 64 48 33 34 25 28 00 xor rsi,QWORD PTR fs:0x28
1a52: 74 71 je 1ac5 <func+0x128e>
1a8a: e8 01 ec ff ff call 690 <__stack_chk_fail@plt>
1ac5: c9 leave
1afc: c3 ret
(※適当にコメントを付けている)
非常にゴチャゴチャしているが、やっていることは入力を1文字ずつ取り出して、複雑な処理を行い、その結果が目的の値と一致しているかを見るだけである。その結果がある値と一致しているかが、0x1311と0x1804のどちらかのcmp ecx, eax
で判定され、そうでなかったら即座に失敗としてリターンしている。これが0x20文字分行われている。
ということは、先頭からここのcmp
で正解するまで文字を総当りすることで正解の文字を当てることが出来る。ただ人力でやると非常に骨が折れるのでスクリプトの力を借りる。偉大な先人(チームメイト)が書いてくれた他の問題のWriteupを読みながら、上記のcmp
にブレークポイントを張って、止まった時にこれらのレジスタを覗いて値を得てからそれが等しいかを判定するというコードを書いてフラグを先頭から確定させていく。
Code
import gdb
import string
gdb.execute("file ./dns_ropob")
gdb.execute("set environment LD_PRELOAD ./ptrace.so")
gdb.execute("b *0x555555401804")
gdb.execute("b *0x555555401311")
candidates = string.ascii_letters + string.digits + "_{}"
flag = ""
TEST = False
if not TEST:
for i in range(0x20):
for c in candidates:
with open("input.txt", "w") as f:
f.write(flag+c)
gdb.execute("run < input.txt")
for _ in range(i):
gdb.execute("c")
rcx = int(gdb.execute("p $rcx", to_string=True).split(" = ")[-1])
rax = int(gdb.execute("p $rax", to_string=True).split(" = ")[-1])
print(rax, rcx)
if rcx == rax:
flag = flag + c
print(f"[+] {flag=}")
break
Flag
TSGCTF{I_am_inspired_from_ROPOB}
Resources
このWriteupを書いている途中にGhidraでも範囲指定してディスアセンブルが出来た事を思い出した