Reversing問題のwriteupです。
Quiz
1$ strings quiz | grep ctf4b
2ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}
WinTLS
exeファイルが渡される。exeを実行すると、テキストボックスとcheckボタンが現れる。checkボタンを押すと、スレッドが2つ立ち上がってそれぞれテキストボックスの入力文字列を加工する。 それぞれのスレッドで加工された文字列とexeファイルに埋め込まれた文字列を比較し、一致していれば「Correct Flag!」と表示されるようだ。
文字列の比較はcheck(0x401550)のstrncmp(0x401582)で行われる。strncmpにブレークポイントを張ると、第1引数(rcx)に加工済みの答え、第2引数(rdx)に加工済みの入力文字列が格納されている。 それぞれのスレッドから呼び出された際に第2引数のアドレスが指す文字列は以下の2つである。
c4{fAPu8#FHh2+0cyo8$SWJH3a8X
tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}
2つの文字列を並び替えて結合すればフラグは得られそうだ。2つの文字数はそれぞれ28文字と32文字なので、フラグは60文字と推測できる。 第2引数に加工済みの入力文字列が格納されるため、60文字分のパターン文字列を入力すれば、どのように並び替えられているか分かりそうだ。
そこで、以下のパターン文字列をテキストボックスに入力し、checkボタンをクリックした。ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890
すると、それぞれのスレッドから呼び出された際にstrncmpの第2引数に格納されたアドレスが指す文字列は以下の2つであった。
ADFGJKMPSUVYZbehjknoqtwyz3469
BCEHILNOQRTWXacdfgilmprsuvx125780
これでどのように並び替えられているのかが分かった。 あとは先程得られた第2引数が指す文字列を、パターン文字列の順番に読んでいけばフラグが得られそうである。
1pat = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
2pat1 = 'ADFGJKMPSUVYZbehjknoqtwyz3469'
3pat2 = 'BCEHILNOQRTWXacdfgilmprsuvx125780'
4
5flag1 = 'c4{fAPu8#FHh2+0cyo8$SWJH3a8X'
6flag2 = 'tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}'
7
8ans = []
9pat = pat[:len(flag1)+len(flag2)]
10for p in pat:
11 a = pat1.find(p)
12 b = pat2.find(p)
13 if a != -1:
14 assert(b == -1)
15 ans += flag1[a]
16 else:
17 assert(a == -1)
18 ans += flag2[b]
19
20print(''.join(ans))
ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}
Recursive
実行をするとFLAGの入力を求められる。 入力文字数が0x26であれば、checkサブルーチン(0x1280)に入り、その返り値が0であれば「Correct!」と表示するようだ。
checkの中身を見ると、再帰的にcheckサブルーチンを呼び出すようだが、最終的には0x12a8の基本ブロックが返り値を決定しているようである。
1.text:00000000000012A8 mov eax, [rbp+var_2C]
2.text:00000000000012AB cdqe
3.text:00000000000012AD lea rdx, table
4.text:00000000000012B4 movzx edx, byte ptr [rax+rdx]
5.text:00000000000012B8 mov rax, [rbp+s]
6.text:00000000000012BC movzx eax, byte ptr [rax]
7.text:00000000000012BF cmp dl, al
8.text:00000000000012C1 jz loc_138F
9.text:00000000000012C7 mov eax, 1
10.text:00000000000012CC jmp locret_1394
0x12B8でraxレジスタへ代入する[rbp+s]
が入力文字列を指すアドレスである。
0x12BCと0x12BFより[rbp+s]
の1バイトがedxレジスタと比較される。この基本ブロックは何度も実行され、その度にraxレジスタには入力文字列の2文字目、3文字目……が代入される。
この基本ブロックが最終的な返り値を決定しているため、rdxレジスタにはフラグの2文字目、3文字目……が代入されるはずである。
したがって、0x12C1にブレークポイントを張り、edxレジスタを1文字ずつ記録していけばフラグが得られるはずである。 しかし、入力文字列が間違っていると、0x12C1でループを終了する基本ブロックへジャンプしてしまう。それを防止する方法として2つ考えられる。
- EIPレジスタを変更する
- IDAであれば、デバッグ中に0x138Fを右クリックしてSet IPをクリックすれば良いが、フラグの文字数分それを繰り返す必要がある。
- パッチを当ててJZ命令に変更する
今回はパッチを当て、JZ命令に変更することにした。
1$ cmp -l recursive recursive_patched
2 4803 204 205
ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}
Ransom
pcapとlockファイルとELFファイルが渡される。pcapを見るとHTTPで文字列が送信されていることが分かる。
1$ tcpdump -Ar tcpdump.pcap
214:15:51.979121 IP 192.168.0.221.44914 > 192.168.0.225.http-alt: Flags [P.], seq 1:18, ack 1, win 502, options [nop,nop,TS val 362141777 ecr 3817428952], length 17: HTTP
3E..E".@.@............r..+.o5:I.......F.....
4...Q..W.rgUAvvyfyApNPEYg.
これが何かを調べるため、writeに注目してmainを眺める。
1.text:0000000000001742 mov rdx, [rbp+stream]
2.text:0000000000001749 lea rax, [rbp+s]
3.text:0000000000001750 mov esi, 100h ; n
4.text:0000000000001755 mov rdi, rax ; s
5.text:0000000000001758 call _fgets
6
7.text:0000000000001781 mov [rbp+var_130], rax
8.text:0000000000001788 mov rdx, [rbp+var_130]
9.text:000000000000178F lea rcx, [rbp+s]
10.text:0000000000001796 mov rax, [rbp+buf]
11.text:000000000000179D mov rsi, rcx
12.text:00000000000017A0 mov rdi, rax
13.text:00000000000017A3 call sub_157F
14
15.text:0000000000001903 mov rcx, [rbp+buf]
16.text:000000000000190A mov eax, [rbp+fd]
17.text:0000000000001910 mov rsi, rcx ; buf
18.text:0000000000001913 mov edi, eax ; fd
19.text:0000000000001915 call _write
上には記していないが、[rbp+fd]
は192.168.0.225
へ接続するソケットである。
sub_157F
の引数に[rbp+buf]
が渡されている。sub_157F
を見ると、sub_1381
がある。
sub_1381
のCFGを眺めていると何となくRC4っぽく見えたので、pcapに記録された文字列をキーと仮定してRC4で復号してみたらフラグが導出できた。
ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}
please_not_debug_me
引数にFLAGのファイルパスを渡すようだ。 最初にunpack処理が行われるようだが、unpackedされたバッファをwriteしている。
1.text:000000000000125D mov eax, cs:binary_len
2.text:0000000000001263 mov edx, eax ; n
3.text:0000000000001265 mov eax, [rbp+fd]
4.text:0000000000001268 lea rsi, binary ; buf
5.text:000000000000126F mov edi, eax ; fd
6.text:0000000000001271 call _write
したがって、writeしたファイルディスクリプタをreadすれば解凍後のファイルを取得できる。
0x1271にブレークポイントを張って、ediレジスタの値を記録してステップオーバーする。 ほとんどの環境では3が代入されているはずなので、ファイルディスクリプタ3をreadする。
1$ cat /proc/6378/fd/3 > unpacked
2$ file unpacked
3unpacked: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers
unpackedを調べると、initでデバッガを検知しているようなので、1B29のnopで1B46にEIPをセットする(1B30のループを飛ばす)ようにEIPレジスタを書き換えればよい。
1LOAD:0000000000001B29 nop dword ptr [rax+00000000h]
2LOAD:0000000000001B30
3LOAD:0000000000001B30 loc_1B30: ; CODE XREF: init+54↓j
4LOAD:0000000000001B30 mov rdx, r14
5LOAD:0000000000001B33 mov rsi, r13
6LOAD:0000000000001B36 mov edi, r12d
7LOAD:0000000000001B39 call ds:(funcs_1B39 - 3D50h)[r15+rbx*8]
8LOAD:0000000000001B3D add rbx, 1
9LOAD:0000000000001B41 cmp rbp, rbx
10LOAD:0000000000001B44 jnz short loc_1B30
11LOAD:0000000000001B46
12LOAD:0000000000001B46 loc_1B46: ; CODE XREF: init+35↑j
13LOAD:0000000000001B46 add rsp, 8
mainにはint 3
(ソフトウェアBP)を検知する条件分岐が複数見られた。
17C1のサブルーチンでは、switch-caseで以下のことを順番に行うようだ。
- ファイルをfopenでオープン
- fopenの返り値がNULLでないかのチェック
- ファイルから最大62バイトをfgets
b06aa2f5a5bdf6caa7187873465ce970d04f459d
を生成b06aa2f5a5bdf6caa7187873465ce970d04f459d
と入力文字列をもとにRC4で暗号化?- memcmpで比較する
手順5では、第1引数(RDIレジスタ)にb06aa2f5a5bdf6caa7187873465ce970d04f459d
、第2引数(RSIレジスタ)に入力文字列、第3引数(RDXレジスタ)にRC4で暗号化されたものと思われるバイナリが出力される。
1LOAD:0000000000001A50 lea rdx, [rbp+s2]
2LOAD:0000000000001A54 lea rax, [rbp+s]
3LOAD:0000000000001A5B mov rsi, rax
4LOAD:0000000000001A5E lea rdi, unk_4020
5LOAD:0000000000001A65 call sub_14F2
6
7LOAD:00000000000014F6 push rbp
8LOAD:00000000000014F7 mov rbp, rsp
9LOAD:00000000000014FA sub rsp, 130h
10LOAD:0000000000001501 mov [rbp+var_118], rdi
11LOAD:0000000000001508 mov [rbp+var_120], rsi
12LOAD:000000000000150F mov [rbp+var_128], rdx
ここで、memcmpで比較する文字列が、手順5の第3引数で暗号化された入力文字列[rbp+s2]
と、実行ファイルの4060に埋め込まれたバイナリであった。
ということは、実行ファイルの4060に埋め込まれたバイナリがフラグと思われる。
1LOAD:0000000000001AAF lea rax, [rbp+s2]
2LOAD:0000000000001AB3 mov edx, 3Fh ; '?' ; n
3LOAD:0000000000001AB8 mov rsi, rax ; s2
4LOAD:0000000000001ABB lea rdi, unk_4060 ; s1
5LOAD:0000000000001AC2 call memcmp
手順5より、4060はb06aa2f5a5bdf6caa7187873465ce970d04f459d
を鍵としてRC4で暗号化したものと思われるため、その鍵で4060のバイナリをRC4で復号するとフラグが得られた。
ctf4b{D0_y0u_kn0w_0f_0th3r_w4y5_t0_d3t3ct_d36u991n9_1n_L1nux?}