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で解ける。
- gdbで一回runをし、適当に処理を抜ける。(gdbを抜けるのではなく、実行状態を抜ける => よくわかってない)
- プログラムを一回走らせると、gdbが
__libc_start_main
を認識してくれるようになる> info symbol __libc_start_main
__libc_start_main
は関数内でmain()を実行するので、ここにブレークポイントを貼り処理を進めることで、無事上記の条件部分までたどり着く。あとは書き換えてループを抜けるだけ。
Leak
簡単な事前解析では以下のようなことが分かった。
- バイナリをghidraでみると、
/tmp/flag
の中身を暗号化して送っていることが分かる - 暗号化は、① テーブル文字列の初期化、② xorで暗号化 という単純な手順
- pcapでtcpストリームを見ると、暗号化されたバイナリっぽいものが流れている
(リバエン能力の不足から)テーブル文字列の処理内で変数にレジスタ(in_RCX
、in_RDX
)を用いていて、solverを実装するのが面倒なことから、以下のような手順で解いた。
/tmp/flag
にpcapに流れる暗号化文字列と同等の長さの適当な文字列を仕込む- gdbを起動し、xor部分にブレークポイントを貼る
- 暗号化文字列の長さ分ループして、テーブル文字列の中身を一文字ずつ解読する
- 抽出したテーブル文字列と暗号化バイナリを一文字ずつxorして復号する
Heaven
以下の点から、1byte目が鍵、残りが暗号化メッセージであることが分かる
- heavenを実行して、適当にaaaを実行してみると1byte飛ばして、残りの3byteが同じ文字列になること
- heavenをghidraでみると、最初の一文字と残りの文字で出力する関数(
__printf_chk
とprint_hexdump
)が違うこと
また、暗号化の手順は以下となる
- 鍵とメッセージをcalc_xor()にかける
- calc_xor()の出力がsboxのインデックスとなり、暗号化後の文字が決まる
暗号化文字列とsboxで逆算してflagを得られるのでは?と思ってコネコネしていると、全然解けず、calc_xor()はxorをしているようで違うことがわかった。
ということで、gdbを用いて以下の手順で英数字と暗号化のhex文字を入れ替える換字表を作成し、flagを算出した
- 入力は、網羅的な換字表が作れるように
[a-z0-9_{}]
を用いる - gdbでcalc_xor()部分にブレークポイントを貼り、以下を繰り返す。
- calc_xor()までcontinue
RSI
(鍵)をlog.txtに書かれてる鍵(0xca
)に設定する- nextでcalc_xor()を行う
RAX
に処理結果が格納される- 処理結果をsboxのインデックスとして暗号化後の文字列を特定する(処理を少し進めればどちらにせよわかる)
以下は換字表を作る過程。calc_xor()の処理結果もメモしていて、無駄が多かったです。
おわりに
去年は、大会が終わってからrev問を解いたが、今回は時間内にしっかりrev全完ができた。もっと早く解けるようになって、他の種目も全完できる時間的余裕を作れるようになりたい…