バランスを取りたい

よくCTFの記事を書きます

WebAssemblyちょっとやる

これはCTF Advent Calendar 2018の2日目の記事です。

ここ1年くらいで、いくつかのプログラミング言語がWebAssembly形式でのバイナリ出力に対応したり、ブラウザ側での対応も本格的になったりしていて、いよいよWebAssemblyが普及してきている感じがしますね。
CTFにも当然この流れは来ていてWebAssemblyに関する問題が出題されるようになっています。

というわけで、この記事はWebAssemblyの基本部分と取り組み方を適当に書きなぐることにします。

WebAssembly全般

WebAssemblyの問題が出たときに自分が試したり考えていたりすることを紹介します。

以降、WebAssemblyは長いので大文字でWASMと表記します。

ファイル形式

WASMの実行ファイル形式はご存知の通りwasmです。 このwasmはELFとかPEに相当するバイナリで、これを人間が読みやすい形式に変換したものがwatです。 WebAssembly Textの略?っぽいですが正確なソースが見当たりませんでした。 以前はwastと呼ばれていましたがいつの間にか変更されたようです。

大雑把に通常のアセンブリと対比するとこんな感じですね。

環境 読めるやつ 実行形式
WASM wat (wast) wasm
x86 アセンブリ ELF, PE, ...

wasmとwatの相互変換はwabtを使うのが一般的だと思います。

github.com

wasm/watの中身

wasmの中身はバイナリで一部の変な人向けなのでwatのほうから構造を把握しましょう。

watのファイルはS式で記述されています。 ただ、S式で記述されていると命令部分が異常に深くなってしまうためか、その部分だけはカッコなしのインデント形式で記述されています。
昔はカッコ付きで書かれていたので最近のwatは読みやすくて素晴らしいですね。

ルートの要素は (module ...) となっていてその中にtypeやらimportやらfuncやらの情報が入ってきます。

引数 用途
type 関数の型 関数の型宣言
import モジュール名, フィールド名, WASM内での定義 外部(JS)から関数やデータをインポート
func シグネチャ. ローカル変数, コード 関数を定義
global 型, 初期値 グローバル変数の宣言
export フィールド名, 値 外部(JS)に関数やデータをエクスポート
elem テーブルインデックス, 関数番号, ... テーブルの値を定義
data 基点アドレス, データ メモリの初期値を定義

この辺の説明をちゃんとするのは非常に難しいので真面目にやりたい人は以下を参照してください。

肝心の命令部分の解説がまだですが、早速小さめのwatファイルを見てみます。

JVMとかMSILを読んだことがあれば同じスタックマシンなので命令部分はなんとなくわかるでしょう。
それでもわからない部分はWASMの仕様を読みつつ頑張ってください。
design/Semantics.md at master · WebAssembly/design · GitHub

見た目はヤバそうなwasmでも他のアセンブリとやることは大差ありません。

wasmを読む

wasmを読むときのポイントを紹介します。

SSAに慣れる

WASMのコンパイラの多くがLLVMを経由しているため、LLVM IRの静的単一代入(SSA)の影響を受けたコードが出力されることが非常に多いです。

これは1つの変数に1度しか代入できないようにすることで実行時の最適化を図るものですが、最適化前のそのままのコードは読みにくいことこの上ないです。
一度使ったローカル変数はその後再代入されないので関数ごとにリストを作って何が入っているのかを管理しましょう。

ちなみに、WASMの方にSSAの制限はありません。

読み方

まず、watファイルを気合で読む方法です。
テキスト内にメモを書いて読み進める方法がメインになると思います。

次に、wabt内で提供されるwasm2cを使う方法です。
wasmをC言語コンパイル可能な形式に変換してくれるので、そのままCコードとして読んだり、実際に内部の関数を呼び出してみて挙動を推測したりすることができます。

さらに、wasm2cで出力されたコードをコンパイルして逆コンパイラで読む方法もあるみたいです。
コンパイラがなくてもSSA表現が実際にレジスタ割付されると読みやすくなる気がします。

使えるもの、使えないもの

現在のWASMはJSに比べて使える機能が大幅に制限されています。
一時期「JSがWASMに全て置き換わる!」と持て囃されていましたが、それは遠い未来の話です。

WASMに現在存在している型は整数型と浮動小数型のみなので、当然DOMとかいう高級な機能は触れません。
ただ、現在GCを導入しようという提案が上がっていて、その中の使用例の1つにDOM操作が挙げられています。 もしかしたら将来触れるようになるかもしれません。

次に、入出力がかなり制限されています。
WASMが直接触れる入出力先はWASM内のMemoryまでが限界で、外からはそのMemoryに対してJSコードで読み書きをしてデータのやり取りをすることになります。
他の方法としては、JS側の関数をWASM内に公開して処理の終了などをcallbackとして受け取ることができます。 それでも関数の引数には即値しか与えられないので、ポインタを渡して自力でMemoryから読み出すことになります。

RustとかCのライブラリでwasmからDOMが触れる風のやつは全部この方法で実装されています。

ちなみに、ここで言っているMemoryはWASM内のMemoryで、JS側からはArrayBufferとしてアクセスできます。

Emscripten世界

WASMの基本は調べればたくさん情報が出てくるので概略のみを説明しました。(がそれでも長くなってしまった……)
ここからは†闇†の世界に入っていきます。

Emscriptenとは

CとかC++のコードをLLVM経由でJSに変換する黒魔術です。
普通のコードをそのまま変換すると当然printfとかの入出力を行う関数が使えなくなってしまうのでPOSIXレイヤーも巻き込んで変換してくれます。気持ち悪いですね。

当初はJSが対象でしたが、時代の流れでasm.jsやWASMにも対応しています。

Emscriptenの怖いところ

POSIXレイヤーを巻き込むと説明しましたが、その部分だけ巻き込んでも仕方がないのでライブラリの部分も一緒に巻き込みます。

つまり、printfを呼び出すとprintf自体やそこから呼び出される関数群もまるごと巻き込んでWASMとしてコンパイルして、システムコール部分だけimportした関数経由でJSレイヤーに送るというコードが自動生成されることになります。

ということは……?

wasmのファイルが肥大化して読みにくくなります。

つらい。
しかも基本的には関数名がわからない形で埋め込まれているので、strippedなstatic-linkバイナリを読むのと同じくらい負荷が大きくなります。

初めてWASMの中身を見ようとして一瞬で嫌になる大きな理由がこれな気がします。

Emscriptenとの戦い方

詳しい人がいたら教えてください。
wasm2c→コンパイル→IDAで逆コンパイルが強そう。

真面目に読もうとするとサイズが大きくなるにつれて†闇†に飲み込まれていきます。
読むべき部分を絞れるようになるしかありません。

ちなみにSECCON CTF 2016 FinalsではEmscriptenでJSにコンパイルされたUnityゲームを解析する問題が出題されました。
簡単なチートをするだけの単純な問題が、17MBのJSファイルと戦う問題に変貌してしまったのでこれがWASMだったら絶望ものですね……。

次回予告

本当は問題ベースの解説記事を書こうと思っていたはずが、WASM実はこんなんだぞという説明を書いてたら長くなってしまいました。 というわけで問題解説は分割して別な日に書くことにします。 扱う問題は多分DEFCON 26 Finals bewとかBCTF 2018 easywasmとか自作問とかから適当に選ぶと思います。

ついでにAdvent Calendarの方の次回予告もします。
明日は@bata_24さんの「Linuxカーネルexploit問におけるTIPS ※ハマったところをまとめたメモ」です。