SECCON Beginners CTF 2022 writeup

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で以下のことを順番に行うようだ。

  1. ファイルをfopenでオープン
  2. fopenの返り値がNULLでないかのチェック
  3. ファイルから最大62バイトをfgets
  4. b06aa2f5a5bdf6caa7187873465ce970d04f459dを生成
  5. b06aa2f5a5bdf6caa7187873465ce970d04f459dと入力文字列をもとにRC4で暗号化?
  6. 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?}

Powered by Hugo & Kiss.