
wasm2c サンドボックスエスケープ。信頼されていない WebAssembly モジュールが生成された C サンドボックスから脱出し、ホスト上で任意のシェルコマンドを実行します。
ブログを読む: trustsig.eu/blog
wasm2c/wasm-rt-impl-tableops.inc 内の wasm_rt_allocate_funcref_table() は、
table->size をモジュールが宣言した要素数から設定し、calloc() の戻り値を無視します。
割り当てが失敗すると、テーブルは data == NULL のまま、宣言された完全な size を持つため、
すべての境界チェックは依然として通過し、table->data[i] は絶対アドレス
i * sizeof(wasm_rt_funcref_t) になります。
要素数はゲストから来るため、ゲストが割り当てサイズを選択し、失敗を強制できます。
git clone https://github.com/trustsig-eu/wasm2c-tableflip.git
cd wasm2c-tableflip
docker build -t wabt-w2c-poc .
docker run --rm wabt-w2c-poc
このイメージは、wabt を上流のタグ 1.0.41 でクローンし、wat2wasm と wasm2c をビルドし、
ゲストモジュールとプレーンなエンベッダをコンパイルして実行します。
期待される出力:
running guest
guest returned 0
--- file on host ---
goodbye sandbox
最後の行は /tmp/pwned.txt の内容です。このファイルは、サンドボックス化されたモジュールが実行される前には存在しませんでした。
linux/arm64 と linux/amd64 で検証済みです。モジュールにはアーキテクチャ固有の要素はありません:
インスタンスアドレス、GOT スロット、libc オフセットはすべて、ビルド時に、ビルドされたばかりのバイナリとそのイメージの libc から解決されます。
Linux で clang、cmake、ninja、binutils がインストールされている場合:
git clone --depth 1 --branch 1.0.41 --recurse-submodules --shallow-submodules \
https://github.com/WebAssembly/wabt ~/wabt
cmake -S ~/wabt -B ~/wabt/out -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=OFF -DBUILD_LIBWASM=OFF -DWITH_WASI=OFF
ninja -C ~/wabt/out wat2wasm wasm2c
python3 tableflip_poc.py --wabt-src ~/wabt --wat2wasm ~/wabt/out/wat2wasm \
--wasm2c ~/wabt/out/wasm2c --run
cat /tmp/pwned.txt
macOS はこの PoC のサポート対象外です。Darwin は RLIMIT_AS を強制しないため、
過大な calloc が成功し、バグが発動しません。arm64 macOS ビルドは常に位置独立であり、
Mach-O にはリーク手順に使える ELF GOT がありません。欠陥自体はプラットフォーム非依存ですが、
このエクスプロイトチェーンだけが Linux 固有です。
他のバージョンとコマンド:
docker build --build-arg WABT_REF=main -t wabt-w2c-poc .
docker run --rm wabt-w2c-poc bash -c \
"python3 tableflip_poc.py --run --command 'id > /tmp/pwned.txt' && cat /tmp/pwned.txt"
(table $t 2147483648 funcref) を宣言します。68 GB の calloc は失敗し、data は NULL、
size は 2147483648 のままで、テーブルインデックスは絶対アドレスになります。got_slot/32 にある table.get は、壊れたテーブルを通してエンベッダの GOT を読み取り、
table.set はその結果をモジュール自身のグローバルに保存します。wasm コードはそれを整数として読むことができます。
これにより libc の malloc アドレスがリークし、system は対象の libc 内の固定距離から導出されます。wasm_rt_funcref_t を構築します: func_type は呼び出しサイトの型ハッシュのコピーを指します
(func_types_eq_slowpath はそれを memcmp で比較するため、ゲストが制御したバイトがチェックを通過します)、
func = system、module_instance = コマンド文字列(これもグローバルに保持されます)。globals_addr/32 で call_indirect を実行します。wasm2c は
((t)entry.func)(entry.module_instance, ...) を生成するため、これは system(command) を呼び出します。モジュールに埋め込まれているレイアウト上の唯一の事実はモジュールインスタンスのアドレスです。これはグローバルであるため、 非 PIE のエンベッダでは固定です。ASLR は有効のままで、libc のアドレスは実行時にリークされます。
テーブルの割り当てが失敗する必要があります。PoC は ulimit -v 1000000 を使用します。これは、
信頼できないコードを実行するホストが設定する種類のアドレス空間制限です。また、32 ビットホストでは割り当てをまったく満たせないため失敗し、
vm.overcommit_memory=2 の場合や、十分なメモリプレッシャーの下でも失敗します。
標準の 64 ビット Linux でデフォルトの overcommit ヒューリスティックを使用すると、割り当ては成功し、 触られることはありません。そのため、このバグは通常のテストを生き残ります。
wasm2c テーブルを同梱するすべてのリリース。チェックされていない calloc はコミット
ab9e0b55 (#813) に由来します。リリース済みの 1.0.41 と現在の main に対して検証済みです。
同じランタイム内のメモリアロケータは、このケースを次のように処理します:
memory->data = (MEMORY_CELL_TYPE)calloc(byte_length, 1);
if (byte_length != 0 && !memory->data) {
abort();
}
テーブルアロケータにも同じチェックが必要です。
Dockerfile は wabt をビルドし、PoC を実行します。tableflip_poc.py はゲストモジュールを生成し、エンベッダをビルドし、nm と readelf を使ってビルド済みバイナリと対象の libc から
3 つの定数を解決して実行します。生成されるエンベッダにはエクスプロイトサポートコードは含まれません。