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のデバッグ機能追加が無ければ心が折れたかもしれない。今後の機能強化も期待したい。