SECCON Beginners CTF 2023 Writeup

SECCON Beginners CTF 2023のWriteup

ほぼgdbによるゴリ押しで、Rev問を全完。

反省

  • ghidraのバイナリ書き換えの活用
  • ghidraのデコンパイル結果に含まれるin_<register>を読めるようにする
  • デバッガ頼りでなく、solverを書く練習をする

Half

stringsコマンド。

Three

ghidraでバイナリを見ると、3つの文字列データからflagを構成していると推測できる。

各データをコピーして、手元で上記のアルゴリズム実装する。

Poker

バイナリを見ると99回勝つとループから抜けられて、flagを出力してくれるっぽいことがわかる(関数名は書き換わってる)。

gdbでその条件処理を書き換えて(99を0にとか)進めばいいと思うが、strippedなバイナリのためmain関数を認識してくれない。が、以下の方法でgdbで解ける。

  1. gdbで一回runをし、適当に処理を抜ける。(gdbを抜けるのではなく、実行状態を抜ける => よくわかってない)
  2. プログラムを一回走らせると、gdb__libc_start_mainを認識してくれるようになる
    • > info symbol __libc_start_main
  3. __libc_start_mainは関数内でmain()を実行するので、ここにブレークポイントを貼り処理を進めることで、無事上記の条件部分までたどり着く。あとは書き換えてループを抜けるだけ。

Leak

簡単な事前解析では以下のようなことが分かった。

  • バイナリをghidraでみると、/tmp/flagの中身を暗号化して送っていることが分かる
  • 暗号化は、① テーブル文字列の初期化、② xorで暗号化 という単純な手順
  • pcapでtcpストリームを見ると、暗号化されたバイナリっぽいものが流れている

(リバエン能力の不足から)テーブル文字列の処理内で変数にレジスタin_RCXin_RDX)を用いていて、solverを実装するのが面倒なことから、以下のような手順で解いた。

  1. /tmp/flagにpcapに流れる暗号化文字列と同等の長さの適当な文字列を仕込む
  2. gdbを起動し、xor部分にブレークポイントを貼る
  3. 暗号化文字列の長さ分ループして、テーブル文字列の中身を一文字ずつ解読する
  4. 抽出したテーブル文字列と暗号化バイナリを一文字ずつxorして復号する

Heaven

以下の点から、1byte目が鍵、残りが暗号化メッセージであることが分かる

  • heavenを実行して、適当にaaaを実行してみると1byte飛ばして、残りの3byteが同じ文字列になること
  • heavenをghidraでみると、最初の一文字と残りの文字で出力する関数(__printf_chkprint_hexdump)が違うこと

また、暗号化の手順は以下となる

  • 鍵とメッセージをcalc_xor()にかける
  • calc_xor()の出力がsboxのインデックスとなり、暗号化後の文字が決まる

暗号化文字列とsboxで逆算してflagを得られるのでは?と思ってコネコネしていると、全然解けず、calc_xor()はxorをしているようで違うことがわかった。

ということで、gdbを用いて以下の手順で英数字と暗号化のhex文字を入れ替える換字表を作成し、flagを算出した

  • 入力は、網羅的な換字表が作れるように[a-z0-9_{}]を用いる
  • gdbでcalc_xor()部分にブレークポイントを貼り、以下を繰り返す。
    1. calc_xor()までcontinue
    2. RSI(鍵)をlog.txtに書かれてる鍵(0xca)に設定する
    3. nextでcalc_xor()を行う
    4. RAXに処理結果が格納される
    5. 処理結果をsboxのインデックスとして暗号化後の文字列を特定する(処理を少し進めればどちらにせよわかる)

以下は換字表を作る過程。calc_xor()の処理結果もメモしていて、無駄が多かったです。

おわりに

去年は、大会が終わってからrev問を解いたが、今回は時間内にしっかりrev全完ができた。もっと早く解けるようになって、他の種目も全完できる時間的余裕を作れるようになりたい…