WebAssembly プログラムのコントロールフローのハイジャック
WebAssembly はブラウザにとって大きな攻撃対象となることが既に証明されていますが、より多くの Web アプリケーションコードが JavaScript から WebAssembly に移行するにつれ、WebAssembly プログラム自体を調査および保護する必要性が生じてきます。WebAssembly は、C や C++ といった開発言語から継承されることがある攻撃との共通部分を排除するよう設計されていますが、それでも攻撃の可能性が完全に排除されるわけではありません。
このチュートリアルでは、WebAssembly が提供するコントロールフロー保護の保証と既知の脆弱性に加えて、コントロールフローのハイジャックに関連するリスクを軽減するために WebAssembly で Clang の Control Flow Integrity (CFI) を使う方法を紹介します。チュートリアル内では、(意図的に) 型の取り違えの脆弱性を悪用してサンプルの WebAssembly プログラムのコントロールフローをハイジャックします。一部のコードは Trail of Bits Blog の「Let’s talk about CFI (CFI の話をしよう)」 で紹介されているものを使用しています (この Trail of Bits Blog のシリーズは CFI にあまり馴染みがない方の入門編として最適です)。
本ブログは、WebAssembly のセキュリティを取り上げる2つのブログ記事の第1部です。第2部では、WebAssembly を埋め込む対象、つまり WebAssembly のゲストプログラムを実行する環境を提供するソフトウェアであるブラウザに関する興味深いセキュリティトピックを取り上げます。
本ブログには、WebAssembly の動作に関する詳しい説明は記載されていません。WebAssembly に馴染みのない方は、webassembly.org や公式デベロッパーズガイドのページを確認するか、Web で利用できる多くの初心者向けブログ投稿やチュートリアル、および動画などを検索してみてください。
WebAssembly を身近な Web アプリでも
WebAssembly は、安全で効率的なアセンブリ言語を Web に導入するための業界全体のオープンな取り組みです。WebAssembly テクノロジーは Mozilla、Google、Microsoft、Apple といった大手ブラウザベンダーと Fastly のような Web ブラウザを持たない Web テクノロジー企業によって共同開発されています。WebAssembly モジュールは現在、ほとんどのブラウザでダウンロードして実行できます。AutoCAD や QT などの大規模な開発では、デスクトップ、モバイル、ブラウザプラットフォーム全体に統一された C/C++ コードベースで構築したスピーディーで安全なアプリをデプロイするために、WebAssembly が活用される機会が増えています。WebAssembly のエコシステムは急速に成長し、新しいツールやアプリケーション、さまざまなアイディアが定期的に発表されています (いくつかの例をこちらで参照できます)。
WebAssembly は Google Native Client の論理的後継者です。Google Native Client は開発者がネイティブアプリケーションを Google Chrome に展開するための高性能なソフトウェア障害分離テクノロジーです。WebAssembly は Native Client での多くの教訓を取り入れた洗練された設計となっており、シングルページで収まる安定したシステム、コントロールフローの整合性、制限やローカルの非決定性、メモリ安全性保証などが特徴です。WebAssembly の設計に関する詳細は、2017 PLDI paper や webassembly.org を参照してください。
セキュリティ制御など、従来は JavaScript で実装されていたロジックの多くが今後数か月から数年のうちに WebAssembly で実装される可能性があります。また、このテクノロジーに注目が集まるにつれ、Web 開発者が WebAssembly で新しい可能性を切り開くことも期待できます。
WebAssembly の設計は、C、C++、およびその他のソース言語に固有の問題を防止するためにメモリ安全性をサポートします。次のセクションでは、この WebAssembly の特徴について取り上げます。
WebAssembly プログラムのメモリ安全性
WebAssembly のメモリ安全性に関するセキュリティ資料では、WebAssembly でメモリ安全性のバグの多くのクラス、およびスタック破壊や ROP などの不正技術が排除されている理由が説明されています。つまり、WebAssembly のコードをコンパイルして実行するプログラムが正しければ、これらの攻撃は不可能になります。これは非常に優れた特長で、非常に細かい点まで考慮された WebAssembly アーキテクチャの成果です。
しかし、この資料には、次のような記述もあります。
「しかし、WebAssembly のセマンティクスでは、他のクラスのバグは取り除かれていません。攻撃者が直接的なコードインジェクション攻撃を実行することはできないものの、間接的な呼び出しに対するコード再利用攻撃を使ってモジュールのコントロールフローをハイジャックすることは可能です。」
以降のセクションでは、型の取り違えを悪用したシナリオを通じて、この設計が実際に何を意味しているのか確認してみます。
攻撃対象: 型の取り違えの脆弱性サンプル
ここでは、Trail of Bits Blog の「Let’s talk about CFI」で紹介されているシンプルな C++ の仮想呼び出しにおける型の取り違えのサンプルプログラムを実行例として使用します。
この例えのコンセプトは、攻撃者が何らかの形でプログラムをだまし、間違った型のインスタンスでメソッドを呼び出すことです。これは (C++ に限らず) さまざまな形で発生しますが、信頼できないデータソース (ネットワークなど) からインスタンス (もしくはインスタンス選択ロジック) が読み込まれる際に、そのインスタンスが想定されている型かどうか確認せずに、そのインスタンスに対し何らかのオブジェクトメソッド (もしくは関数) を呼び出すというのが一般的なシナリオです。攻撃者がプログラムに対して想定外の型のインスタンスをフィードすることが可能な場合、攻撃者はプログラムをコントロールする (もしくはその他の悪事を引き起こす) ことができます。
Trail of Bits のコードに対する変更点は以降に記載します。本チュートリアルで使用したサンプルコードは GitHub で参照できます。
チュートリアルツールのセットアップ (オプション)
このセクションでは、ツールのセットアップ方法を説明します。必要ない場合は、スキップしても構いません。
脆弱なコードをネイティブや WebAssembly ターゲットにコンパイルするために Docker Ubuntu 16.04 ゲストを使用します。ホストにインストールされたツールを使用してファイルを編集し、ホスト Web ブラウザを使用して WebAssembly を実行できるようにディレクトリを共有します。
docker run -v "$(pwd):/src" -t -i ubuntu:16.04 bash
ネイティブターゲットへのコンパイルには Clang を使用し、WebAssembly のコンパイルには Emscripten を使用します。皆さんのやり方とは異なるかもしれないですが、Ubuntu ゲストにすべてのツールをインストールするために私が実行したコマンドを以下に示します。
root@2dc5f92b98cf:/src# apt-get update && apt-get install -y cmake build-essential python2.7 nodejs git wget tmux
root@2dc5f92b98cf:/src# apt-get install clang-5.0 && ln -s /usr/bin/clang-5.0 /usr/bin/clang && ln -s /usr/bin/clang++-5.0 /usr/bin/clang++
root@2dc5f92b98cf:/src# wget https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz && tar -xf emsdk-portable.tar.gz && cd emsdk-portable
root@2dc5f92b98cf:/src/emsdk-portable# ./emsdk update && ./emsdk install latest && ./emsdk activate latest
本チュートリアルのコンテンツを作成するため、Emscripten 環境を利用して tmux セッション (以降 [tmux
] と表記します) を実行しました。[tmux
] セッションは、WASM ターゲットに対して Emscripten ツールチェインを使用し、通常のゲストシェルはネイティブターゲットに対して Ubuntu clang ツールチェインを使用します。こちらが Emscripten 環境です。
[tmux] root@2dc5f92b98cf:/src/emsdk-portable# source ./emsdk_env.sh
&& which clang
/src/emsdk-portable/clang/e1.37.35_64bit/clang
そして、これが Clang/ネイティブ環境です。
root@2dc5f92b98cf:/src/emsdk-portable# which clang
/usr/bin/clang
バイナリの WebAssembly モジュールからテキスト形式への変換 (および逆の変換) には WebAssembly Binary Toolkit (WABT) を使用できます。
root@2dc5f92b98cf:/src# git clone --recursive https://github.com/WebAssembly/wabt && cd wabt
root@2dc5f92b98cf:/src/wabt# make && make install
WebAssembly の型チェックにおける脆弱性の悪用防止
WebAssembly を埋め込むもの(ブラウザ) は通常、WebAssembly プログラムによる実行を許可する前に、一般 (引数と戻り値の観点で) 関数の型 (引数と戻り値) が正しいかどうかを確認します (WebAssembly.validate も参照してください)。しかし、C や C++ での関数ポインタ呼び出しに類似した 間接呼び出し に対する型のチェックは実行時に行われます。間接呼び出しに対する型チェックが失敗すると、WebAssembly プログラムは停止してトラップが起動します。ブラウザでは、最終的にユーザーコードで処理できる (もしくはできない) JavaScript の例外となります。いずれの場合でも、WebAssembly の設計で提供される保証によって、埋め込み先(ブラウザ) のプロセスは定義外の動作 (メモリ破損など) を恐れることなく安全に実行を継続できます。
Trail of Bits のサンプルコードの cfi_vcall.cpp の修正版を使用して型チェックが正常に行われたことを確認します。修正版の名前は cfi_vcall_diff.cpp です。cfi_vcall_diff.cpp では、攻撃対象の関数 (プログラムが呼び出そうとする関数) は integer 型の引数を取りますが、攻撃側の関数 (攻撃者が何らかの手段で提供する関数) は float 型の引数を取るよう制限されています。この場合、攻撃者は攻撃対象に以下のコードを実行させようと試みています。
virtual void makeAdmin(float * i) {
std::cout << "CFI Prevents this control flow " << i << "\n";
std::cout << "Evil::makeAdmin\n";
}
本来実行されるはずのコードは次の通りです。
virtual void printMe(int i) {
std::cout << "Derived::printMe " << i << "\n";
}
次のセクションでは、このようなプログラムの悪用方法を紹介します。
実験1: ネイティブ実行における型の取り違えの悪用
WebAssembly の型チェックをしないと何が起こるのか、まずは Clang で次の脆弱性/悪用プログラムをコンパイルして確認してみます。
root@2dc5f92b98cf:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -o cfi_vcall_diff cfi_vcall_diff.cpp
これを実行します。
root@2dc5f92b98cf:/src/clang-cfi-showcase# ./cfi_vcall_diff
Derived::printMe 55.5
CFI Prevents this control flow 0
Evil::makeAdmin
上記の出力から、脆弱プログラムが悪用され、攻撃者の makeAdmin
ペイロードが実行されたことが確認できます。この原因は、ネイティブのマシンコードに関数のパラメーターの型に対する実行時チェックが含まれていないため、攻撃側の関数が阻止されずに実行されたからです (フィードされた integer が関数で float とみなされた結果です)。
実験2: WebAssembly における型の取り違えの悪用防止
WebAssembly の型チェックが実行される場合はどうなるか、Emscripten で脆弱性/悪用プログラムをコンパイルして確認してみます。
[tmux] root@2dc5f92b98cf:/src/clang-cfi-showcase# emcc cfi_vcall_diff.cpp -Werror -s WASM=1 -o cfi_vcall_diff.html
上記のコマンドは、WebAssembly モジュール (.wasm)、呼び出し用の JavaScript ラッパーなど (.js)、およびそれらを結び付ける HTML ファイル (.html) を生成します。結果の HTML ページを表示し、ブラウザで開発者コンソールを開いて結果を確認できます。Python SimpleHTTPServer をホストシステムで実行します。
mayor:clang-cfi-showcase foote$ python -m SimpleHTTPServer 8081
さらに生成されたページを確認してみます。
その結果、WebAssembly のコードがエラーをキャッチしてホストプログラムにトラップを返していることがわかります。インタプリタが call_indirect を実行すると、型チェックが失敗してトラップが起動します。何が起きているのかをさらによく理解するために、WebAssembly のコードをテキスト表示に変換してみましょう。
wasm2wat cfi_vcall_diff.wasm > cfi_vcall_diff.wat
ファイルを表示すると、次のような call_indirect の呼び出し (注釈付き) が確認できます。
[...]
f64.const 0x1.bcp+5 (;=55.5;) // Push arg (55.5) onto the stack
get_local 4 // Calculate function ptr (cont’d)
i32.const 15 // (an index into the func table)
i32.and // ..
i32.const 5376 // ..
i32.add // ..
call_indirect (type 0) // call func ptr: printMe/makeAdmin
[..]
call_indirect がチェックする型の定義は、float を受け取る関数を指しています。
(type (;0;) (func (param i32 f64)))
したがって、この場合は WebAssembly で 2 つの関数が異なるシグネチャを持っているため、この型チェックは失敗します。「攻撃対象」の関数は float (f64
) を受け取るのに対し、「攻撃側」関数は Integer (i32
) を受け取っています。
なお、この時点で WebAssembly プログラムは停止してトラップしますが、設計上、この障害は WebAssembly のゲストプログラムインスタンスに分離されます。これは、WebAssembly をホストしているブラウザのプロセスが、メモリ破壊などを心配することなく安全に継続できるということを意味しています。
WebAssembly の型チェックにおける脆弱性の悪用
WebAssembly は安定した型システムを始めとする洗練されて安全な設計を提供します。この設計の副次的影響の1つに、(本ブログ記述時点では) WebAssembly では少数の型 (i32
、i64
、f32
、および f64
) しか提供されていないことがあります。つまり、ソース言語 (C や C++ など) の型はすべてこれらの型にマッピングされ、これらがWebAssembly の間接呼び出しの型チェックで使用される型になるのです。同様にこの実行例で、攻撃者が WebAssembly での型シグネチャが一致する関数を提供できる場合、攻撃対象の WebAssembly のコントロールフローを乗っ取ることができる可能性があることを意味します (詳細については、WebAssembly のメモリ安全性に関する資料を参照してください)。
この動作を確認するため、Trail of Bits のサンプルコードから引用した cfi_vcall.cpp の修正版 cfi_vcall_same.cpp を使用します。cfi_vcall_same.cpp では、攻撃対象の関数 (プログラムが呼び出そうとする関数) は、整数型の引数を取りますが、攻撃側の関数 (攻撃者が何らかの手段で提供する関数) は void 型引数を取るよう制限されています。この2つの関数は、C++ では型が異なりますが、WebAssembly の同じ型にマッピングされます。つまり、「攻撃対象」関数と「攻撃側」関数のシグネチャが一致するため、攻撃者が攻撃対象の WebAssembly プログラムのコントロールフローを乗っ取ることができるのです。要約すると、cfi_vcall_same.cpp では攻撃者は攻撃対象に以下のコードを実行させようと試みています。
virtual void makeAdmin(void * i) {
std::cout << "CFI Prevents this control flow " << i << "\n";
std::cout << "Evil::makeAdmin\n";
}
本来実行されるはずのコードは次の通りです。
virtual void printMe(int i) {
std::cout << "Derived::printMe " << i << "\n";
}
次のセクションでは、このようなプログラムのもう1つの悪用例を紹介します。
実験3: ネイティブバイナリ実行における型の取り違えの悪用
Clang を使って脆弱性/悪用プログラムをコンパイルすることで、WebAssembly の型チェックをしないと何が起こるのかをもう一度確認してみます。
root@2dc5f92b98cf:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -o cfi_vcall_same cfi_vcall_same.cpp
これを実行します。
root@2dc5f92b98cf:/src/clang-cfi-showcase# ./cfi_vcall_same
Derived::printMe 55
CFI Prevents this control flow 66
Evil::makeAdmin
上記の出力から、脆弱プログラムが悪用され、攻撃者の makeAdmin
ペイロードが実行されたことが確認できます。これは、実行時のネイティブマシンコードに関数のパラメーターの型チェックが含まれていないため、ここでも「攻撃側」関数が阻止されずに実行されたことが原因です。
実験4: WebAssembly における型の取り違えの脆弱性悪用
それでは、WebAssembly の型チェックを行うと何が起こるのか、Emscripten で脆弱性/悪用プログラムをコンパイルして確認してみます。
[tmux] root@2dc5f92b98cf:/src/clang-cfi-showcase# emcc cfi_vcall_same.cpp -Werror -s WASM=1 -o cfi_vcall_same.html
先程の WebAssembly の実験同様、上記のコマンドは cfi_vcall_same.wasm (WebAssembly モジュール)、cfi_vcall_same.js (ブラウザと WebAssembly モジュール間のインターフェイスを定義した JavaScript ファイル)、および cfi_vcall_same.html (JavaScript を実行する HTML ページ) を生成します。
再度、ホスト上で Python SimpleHTTPServer を実行します。
mayor:clang-cfi-showcase foote$ python -m SimpleHTTPServer 8081
さらに生成されたページを確認してみます。
ここでは型の取り違えの脆弱性が存在し、「悪用」が実行された (makeAdmin
が実行された) ことが確認できます。この原因は、printMe
も makeAdmin
も今回は一致する型シグネチャを持つためです。
(type (;0;) (func (param i32 i32)))
これは C の void* 型と int 型が WebAssembly ではどちらも i32 型にマッピングされるためです (WebAssembly プログラムは 32 ビットアドレッシングを採用しています。詳細については、2017 PLDI の論文をご覧ください)。したがって、WebAssembly が呼び出そうとする関数の関数シグネチャ (WebAssembly の関数パラメーターと結果の型) をチェックしても、2 つの関数は同じシグネチャを持つため悪用プログラムの実行が成功してしまうのです。
Clang CFI を利用して WebAssembly の脆弱性を保護
これまでに確認した通り、WebAssembly のシンプルな型システムには大きなメリットがある一方で、型の取り違えの脆弱性が依然として発生しうるという欠点があります。幸い、ネイティブの実行可能ファイルと同様に Clang CFI チェックを使用して WebAssembly コードをコンパイルすることができます。WebAssembly のメモリの安全性に関する資料にも記載されている通り、これは今回確認したコード再利用攻撃に対する防御に役立つほか、一般的に粒度の細かい C/C++ の型を使用して他の関数シグネチャもチェックします。
このように、Clang CFI チェックを* WebAssembly にコンパイルして埋め込み先 (ブラウザなど) で実施できます*。これは非常に有効です。
実験5: ネイティブ実行可能ファイルでの Clang CFI の実行
-fsanitize=cfi-vcall で cfi_vcall_same.cpp をコンパイルすることで (cfi_vcall_same_cfi にバイナリを出力) 、ネイティブバイナリで名目的な Clang CFI の実行を確認できます。
root@2dc5f92b98cf:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -fvisibility=hidden -flto -fsanitize=cfi-vcall -fno-sanitize-trap=all -o cfi_vcall_same_cfi cfi_vcall_same.cpp
これを実行します。
root@2dc5f92b98cf:/src/clang-cfi-showcase# ./cfi_vcall_same_cfi
Derived::printMe 55
cfi_vcall_same.cpp:45:5: runtime error: control flow integrity check for type 'Derived' failed during virtual call (vtable address 0x0000004300a0)
0x0000004300a0: note: vtable is of type 'Evil'
00 00 00 00 20 84 42 00 00 00 00 00 30 84 42 00 00 00 00 00 60 84 42 00 00 00 00 00 00 00 00 00
上記の出力から、Clang CFI が意図した通りに動作した (悪用が阻止された) ことがわかります。
実験6: WebAssembly プログラムでの Clang CFI の実行
ここからが本題です。類似の -fsanitize=cfi-vcall フラグで Emscripten を実行し、先程悪用された WebAssembly プログラムで Clang CFI を実行してみましょう。
[tmux] root@2dc5f92b98cf:/src/clang-cfi-showcase# emcc cfi_vcall_same.cpp -fvisibility=hidden -flto -fsanitize=cfi -s WASM=1 -o cfi_vcall_same_cfi.html
結果の HTML ファイルをブラウザで確認します。
ブラウザで Clang CFI が実行され、悪用が阻止されていることがわかります。
最後に
このブログ記事では、型の取り違えの脆弱性を悪用してサンプルの WebAssembly プログラムの制御フローをハイジャックする方法を解説し、WebAssembly が提供するメモリの安全性を保証する機能の一部をデモでご紹介しました。また、Clang CFI を使用してこれらの種類の攻撃に対して WebAssembly プログラムを強化する方法についても取り上げました。
全体的にみて、WebAssembly は開発の安全性において優れたベースラインを提供するよく設計されたテクノロジーです。このチュートリアルでは、エコシステムにおけるセキュリティの側面の一部をご紹介しました。
このシリーズの第2部にもご期待ください。第2部では WebAssembly を埋め込むソフトウェア (すなわち WebAssembly のゲストプログラムを実行する環境を提供するブラウザ) に関する、興味深いセキュリティトピックを取り上げます。