UTCTF 2020 writeup - Wasm Fans Only
某CTFチームのAdvent Calendar 2020向けの記事です。
例年のごとく年末の締めくくりとして今年開催のWeb系のWriteupを読み漁っていたところ、WebAssemblyの問題にいくつか遭遇した。WebAssemblyは比較的Rev問に近く苦手意識があるものの解けるようになりたいと思っていた。そんな折にChromeのWebAssemblyのデバッグ機能が強化された記事を見かけた。
これで少し手が出しやすくなってきた気がしたので、今年の過去問をサンプルに解いてみることにした。
問題
UTCTF 2020のWASM Fans Onlyを解く。
この大会は2020年3月開催であったにもかかわらず、嬉しいことにまだ問題サーバが生きている。
問題サーバはこちら。
https://wasmfans.ga/
また、githubに問題ファイルが公開されているのも嬉しい。
元のCのソースコードも公開されており、答え合わせをしながら解析できるため、初心者にピッタリ。
github.com
またWriteupもあるのも嬉しい。
https://ctftime.org/writeup/18640
なお、後のネタバレになってしまうが、実は問題サーバのホスト名(wasmfans.ga)がキーになっているため、もし上記のgithubのリポジトリから落としてきて自分のサーバで試したい場合は、wasmfans.gaのホスト名で自サーバにアクセスできるよう/etc/hostsやC:\Windows\System32\drivers\etc\hostsを設定すること。
Writeup
HTML/JavaScript解析
まずURLにアクセスすると以下の画面が表示される。
UsernameとPasswordを適当に入力してLog in
ボタンを押下するとTry again!。
なおPasswordの項目IDはflag
である。
Log in
ボタンを押下するとページ内のcheckFlag
関数を呼び、checkFlag
関数はModule._verify_flag();
を呼んでいる。
_verify_flag
はverifyFlag.js
ファイルに定義されており、更にWebAssemblyのverify_flag関数を呼んでいる。
早速ここからWebAssemblyの世界へ突入するが、その前にもう少しverifyFlag.js
ファイルを見てみる。
verifyFlag.js
は非常に大きいファイルで正直わけわからん状態であるが、エラーメッセージの「Try again!」で検索すると、意味がわかるロジックが見つかる。これら3つの関数はWebAssemblyから呼ばれる関数なので覚えておく。
wasmのざっくり静的解析
verifyFlag.wasm
ファイルをverify_flag
で検索するとすぐに関数が見つかる。
ザっと目につく処理を書き出してみる。
0x01826~0x018e8
変数の宣言と初期化?0x018ec~0x01985
ループ処理で何かを55
とXORしている
0x019ac
$env.getString
をcallしている
0x019b8~0x01a23
変数の宣言と初期化?0x01a26~0x1abc
ループ処理で何かの変数を96
とXORしている
0x01ae3
$env.getString
をcallしている
0x01b26~0x01bf3 変数の宣言と移送か何かをしている
0x01c3e~0x01de0
$func23
を呼んでから巨大な処理ブロック内へ。$func23
の返り値が0の場合は$label6
を抜けて$env.lose
を呼ぶ。その後$label9
を抜けて終了。→NG$func23
の返り値が0でない場合は$aes_encrypt_block
を呼ぶ。その後ループ内で何らかの配列を1文字ずつ比較。- 一致しない場合は
$env.lose
を呼ぶ。→NG - ループを抜けた=全ての変数が一致したら
$env.win
を呼ぶ。→OK
- 一致しない場合は
$func23
の解説は省略。フラグ文字列の文字数とprefixとsuffixのチェックである。
8のうち、ポイントとなる処理だけ残して以下に転記した。
call $func23 local.set $var121 block $label9 block $label6 local.get $var121 i32.eqz br_if $label6 (snip) local.get $var133 local.get $var128 local.get $var125 call $aes_encrypt_block local.get $var2 local.get $var122 i32.store offset=12 block $label7 loop $label10 i32.const 16 (snip) local.get $var159 local.get $var160 i32.and local.set $var161 block $label8 local.get $var161 i32.eqz br_if $label8 call $env.lose br $label9 end $label8 (snip) end $label10 unreachable end $label7 local.get $var2 i32.load offset=136 local.set $var165 local.get $var165 call $env.win br $label9 end $label6 call $env.lose end $label9
何となく処理の流れは掴めたので、あとはChromeのDebuggerで変数の値を確認しながら実際に動かしてみる。
wasmの動的解析
※この解析には冒頭で紹介したChromeのWebAssemblyのデバッグ機能を使いたいためChromeのCanary版を使用している。(2020/12/22現在)
verify_flag
の先頭にブレイクポイントを設定し、usernameにaaaaaaaaaaaaaaaa
、Passwordにbbbbbbbbbbbbbbbb
を入力して実行する。
そして、Chromeの新機能を発動するべく右側のペインのenv.memoryで右クリックしてInspect memory
を選択する。
下側のペインにメモリの状態が表示された!ASCII表示もあるぞ!
これまで右側のペインで縦に並んだ変数1つずつしか見れなかったのに、これは嬉しい。
また、変数にカーソル当てると、変数の中がホバーで表示されるようになった!
「え、今までそうじゃなかったの?」と疑問に思う人がいるかもしれないが、2020/12/22現在の安定板のChromeではできない。
このホバーで表示された変数をコピーし、先ほどのメモリインスペクタのアドレス欄に貼り付ける。
(10進数を貼り付けると、自動的に16進数に変換してくれる。)
すると、何かのバイトデータが格納されていることがわかる。読める、読めるぞ。
上図は最初の変数の初期化が終わるあたりまで処理を進めた後の状態であるが、次に55
とXORをとっているループ処理を何周か進めてみると、JavaScriptのDOMへのアクセスっぽい文字列が出てきた。
ループを最後まで進めると、HTMLのflag
項目(Passwordのテキストボックスに入力した文字列)を取得するためのJavaScript呼出し文字列であるdocument.getElementById("flag").value
が出現した。
そして、その次の$env.getString
にこのJavaScript文字列を渡し、Passwordに入力した文字列を取得しメモリに格納された。
同じように次の$env.getString
まで処理を進めると、window.location.hostname
の文字列を渡してホスト名wasmfans.ga
をメモリに格納していることを確認できた。
$func23
まで進めると、返り値0が返ってきてそのままlose処理へ。
$func23
を突破するため、24文字かつprefixがutflag{
、suffixが}
の条件を満たす文字列をPassword項目に入力してリトライ。
無事に突破して$aes_encrypt_block
の呼出しまで進め、渡している変数を確認する。
ここでも変数のホバー表示とメモリインスペクションが活躍する。
渡している3つの変数は以下のとおり。
- Password項目に入力した
utflag{AAAAAAAAAAAAAAAA}
のA×16文字の部分。
- いつの間にかメモリに格納されていた
nasmfans.ga
という文字列。どうやら静的解析の7の処理で生成していたようだ。AESの鍵にあたる。
- 0x00。たぶんAESのIV。
そろそろゴールは近い。
入力文字列をAES暗号化したデータとどのデータと比較しているかだが、ステップ実行しながら変数とメモリインスペクションを確認していくと、以下のバイトデータと比較していそうなことがわかる。
残念ながらメモリインスペクションからコピペができないため、手動で書き出す。
0f ae f8 59 84 b1 28 67 28 18 88 17 64 d3 25 2a
あとはAES復号するだけ。
from Crypto.Cipher import AES encrypted = [0x0f, 0xae, 0xf8, 0x59, 0x84, 0xb1, 0x28, 0x67, 0x28, 0x18, 0x88, 0x17, 0x64, 0xd3, 0x25, 0x2a] key = [] for c in 'nasmfans.ga': key.append(ord(c)) while len(key) != 16: key.append(0) cipher = AES.new(bytes(key), AES.MODE_ECB) print(cipher.decrypt(bytes(encrypted)))
実行する。
root@kali:/mnt/hgfs/CTF/Contest/wasm# python3 solve.py b'fPRv38aICAz31Ix7'
フラグ文字列感は無いが、試しに画面入力してみる。prefixとsuffixは忘れずに。
初めてのWebAssembly問ということで、元のCのソースやWriteupがあるにもかかわらず時間をかけたが、おかげで多少慣れた。直近のChromeのデバッグ機能追加が無ければ心が折れたかもしれない。今後の機能強化も期待したい。