
パッチ適用済みのJavaScriptCoreの脆弱性を悪用する
これは、バンクーバーで開催された pwn2own コンテストで Fluoroacetate によって発見された WebKit の脆弱性に対するエクスプロイトです。私はこのバグを発見したわけではありませんが、エクスプロイト開発スキルを練習するためにこのエクスプロイトを作成しました。このエクスプロイトの元の解説記事は Zero Day Initiative による こちら です。この解説記事は非常に優れており、脆弱性の理解に役立ちましたが、脆弱性を検証する人の視点から書かれています。このエクスプロイトをゼロから作成しようとした際に、いくつかの重要な詳細が欠けていることに気づき、ZDI の解説記事では見落とされたギャップを埋め、複雑なエクスプロイトをゼロから設計する実践的なスキルを身につけたいと考えています。
以下の手順は、WebKit の JavaScript エンジンである JavaScriptCore (JSC) 内で任意のコード実行を達成するための概要です。
悪用される脆弱性は、WebKit の DFG ジャストインタイム (JIT) コンパイラによって生成されるコードで発生する整数オーバーフローです。これは特に compileNewArrayWithSpread 関数で発生します。この関数は、JavaScript スプレッド構文 を使用して新しい配列を作成するコードが DFG によって JIT コンパイルされるときに呼び出されます。

JIT コード内では、まず配列のサイズを計算します。これは、配列コンストラクタに渡された各引数の長さを加算することで行います。各加算のサイズを計算する際に、サイズのオーバーフローをチェックします。その後、この関数で計算された長さを引数として compileAllocateNewArray 関数を呼び出します。

compileAllocateNewArray は、先に計算された長さを emitAllocateButterfly に渡します。

emitAllocateButterfly は、サイズを 3 ビット左シフトします。これは 8 倍することに相当します。しかし、オーバーフローのチェックはなく、そのため 0x20000001 のような数値が 0x8 にオーバーフローする可能性があります。
次の C プログラムはこの脆弱性を示しています:


この脆弱性を利用して、JavaScript エンジンにサイズ 0x20000001 の配列を割り当てたと思い込ませながら、実際には 1 つの JSValue (8 バイト) 分のスペースしか割り当てていないように仕向けることができます。これにより、バウンダリ外 (OOB) の読み取りおよび書き込み (R/W) プリミティブが発生し、これを利用して任意の R/W を達成し、最終的にはリモートコード実行 (RCE) が可能になります。
OOB 読み取りが可能であることを確認するために、JSC の アドレスサニタイザー (ASAN) ビルドでこの脆弱性をトリガーしてみます。
これを行うには、WebKit ディレクトリから次のコマンドを実行します:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
これにより、JSCのデバッグビルドをASAN有効でビルドし、脆弱性を正常にトリガーできたかどうかを確認できるようになります。
以下がexploit.jsの最初のイテレーションです。```javascript
function jitMe(array){
return [...array]
}
let dummy = [1.1]
for(let i = 0; i < 200; i++){
jitMe(dummy);
}
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
これを実行すると、次のエラーが発生します:
プログラムはシグナルSIGKILLで終了しました、Killed。 プログラムはもう存在しません。
私の推測では、そのような大きな配列を割り当てようとした際にメモリを消費しすぎたのではないかと考えました。これを確認するために、JITコードにブレークポイントを追加しました。compileNewArrayWithSpread内にm_jit.breakpoint()の呼び出しを追加することで、JITコードにint3命令を追加しました。
ブレークポイントを追加したところ、それがヒットしなかったため、長さ0x20001でテストすることにしました。その後、コードがコンパイルさえされていないことに気づき、DFGコンパイラを起動するためにイテレーションを増やしました。```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }
let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){ a[i] = 1.1 }
jitMe(a)
プログラムをそのままテストするとSIGKILLが発生しますが、より小さな長さでテストするとブレークポイントにヒットします。この時点では、JSCがその巨大な配列を処理しようとしてメモリ不足になっているように思われます。
この問題に対処するため、より小さな `a` 配列を割り当て、スプレッド構文を使用してそれを複数回利用し、破損した配列を作成することにしました。結果として、次のような `exploit.js` ができました。```
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1]
for(let i = 0; i < 100; i++){
print(i)
jitMe(dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
このコードを使って、SIGKILLなしでブレークポイントにヒットできました!よくあることですが、一つの問題を修正すると別の問題が現れ、代わりにSIGABORTが発生しました... gdbのbtコマンドを使うと、operationNewArrayWithSizeが呼び出され、それがcreateを呼び出したことがわかります。
JITコンパイルされたコードがoperationNewArrayWithSizeを呼び出すのは奇妙に思えます。何らかの理由で、JITコードがJavaScriptエンジンへのスローパスを取らなければならなかったに違いありません。

compileAllocateNewArrayWithSize内で、確かにoperationNewArrayWithSizeへのベイルアウトがあることがわかります。そこで、なぜスローパスにベイルアウトするのかを正確に突き止める必要があります。
compileNewArrayWithSpreadの中で、shouldConvertLargeSizeToArrayStorageがfalseに設定されており、スローパスはコンパイルされたコードに含まれないことがわかります。
したがって、スローパスはemitAllocateJSObject内のどこかでヒットしていると考えるのが妥当です。

emitAllocateJSObjectはemitAllocateJSCellを呼び出し、さらにemitAllocateを呼び出します。


WebKitアロケータの動作を知らなければ、これはかなり混乱するように見えます。そこで、gdbでいくつかブレークポイントを追加し、ステップ実行することにしました。
emitAllocateButterflyによって呼び出されたemitAllocateVariableSizedに設定したブレークポイントにヒットした後、次のようなアセンブリコードが見えました。
これは、JITコンパイラがここで出力したコードに対応しています。
アロケーションサイズに0xfが加算され、4ビット右シフトされていることがわかります。その後、0x1f6と比較され、スローパス分岐に対応します。その後、サブスペースアロケータをrsiに移動し、計算結果に基づいてこのポインタにインデックスを付けます。そして、emitAllocateWithNonNullAllocatorに設定したブレークポイントに進み、次のアセンブリコードを見つけます。

これは、JITコンパイラがここで出力したコードに対応します。
アセンブリをいくつかステップ実行したので、何が起こっているのか少しコンテキストが掴めました。さらに2命令ステップ実行すると、ジャンプを行うことがわかります。

C++コードを見ると、これはこのアロケータのフリーリストに残りスペースがゼロであることを意味し、ポップパスを取ることが推測できます。

ジャンプを実行し、次の2命令を実行すると、ジャンプが直接スローパスを取ることに相当していることがわかります。スローパスを取るのは、アロケータの秘密がアロケータのスクランブルされた先頭とXORされ、結果がゼロになるからです。WebKitアロケータについてこれ以上知識がなければ、何が起こっているのかを正確に把握するのは困難です。
WebKitアロケータについてもっと時間をかけて学びたいところですが、より簡単な方法として、いくつかのアイデアを試して、異なる結果が得られるかどうかを見て、そこからデバッグすることにしました。