-
-
[原创] CVE-2023-4427:Out-of-bounds access in ReduceJSLoadPropertyWithEnumeratedKey
-
发表于: 2024-4-21 20:57 8177
-
[原创] CVE-2023-4427:Out-of-bounds access in ReduceJSLoadPropertyWithEnumeratedKey


@
前言
这篇文章很久之前发在了 CSDN
上,但当笔者想再次查看时,发现其广告实在是太多了,每次看的时候都很不方便,广告老是挡住,所以将文章转到看雪上面(我爱说实话,某平台全是广告...)
=============================
看到一半,发现忘记写 patch
分析了,后面补上......
=============================
之前分析调试漏洞时,几乎都是对着别人的 poc/exp
调试,感觉对自己的提升不是很大,所以后面分析漏洞时尽可能全面分析,从漏洞产生原理、如何稳定触发进行探索。并尝试自己写 poc/exp
环境搭建
1
2
3
4
5
|
git checkout 12.2.149 gclient sync -D
git apply diff .patch
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
|
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;
|
for-in && enum cache
最初接触 enum cache
是在 V8
的官方博客 Fast for-in in V8 中,其介绍了 V8
是如何实现快速的 for-in
语句的,详细的内容可以参考上述官方博客。
总的来说 for-in
语句用于遍历对象的可枚举属性(包括原型链),在 V8
中其设计大概如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function * EnumerateObjectProperties(obj) {
const visited = new Set();
for (const key of Reflect.ownKeys(obj)) {
if ( typeof key === 'symbol' ) continue ;
const desc = Reflect.getOwnPropertyDescriptor(obj, key);
if (desc && !visited.has(key)) {
visited.add(key);
if (desc.enumerable) yield key;
}
}
const proto = Reflect.getPrototypeOf(obj);
if (proto === null ) return ;
for (const protoKey of EnumerateObjectProperties(proto)) {
if (!visited.has(protoKey)) yield protoKey;
}
} |
可以看到,其首要的工作就是迭代遍历对象及原型链上的可枚举属性从而收集所有的可枚举 keys
。那么 V8
为了优化这一过程,配合 V8
的隐藏类机制提出了 enum cache
。
我们知道 V8
通过隐藏类或所谓的 Map
来跟踪对象的结构。具有相同 Map
的对象具有相同的结构。此外,每个 Map
都有一个共享数据结构——描述符数组,其中包含有关每个属性的详细信息,例如属性存储在对象上的位置,属性名称以及是否可枚举等属性信息。为了避免反复的访问描述符数组和检测相关属性,V8
将可枚举对象内属性和快属性的 key
和位置 index
保存在了 enum cache
:
注:
enum cache
保存在描述符数组中,而字典模式是不具有描述符数组的,而对于具有描述符数组的element
其也默认就是可枚举的,而对于elements
的键查找是非常简单的。所以这里enum cache
主要就是针对快属性和对象内属性的
所以如果对象只要快属性或对象内属性,那么在执行for-in
时,只需要访问一次描述符数组,从描述符数组中拿到enum cache
即可找到所有的可枚举属性,然后遍历原型链,取原型链的enum cache
(如果有的话)。当然如果对象中还有elements
呢?这时也会取enum cache
,但是会进行一些其它的操作,大致流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// For-In Prepare: FixedArray* keys = nullptr; Map* original_map = object->map(); if (original_map->HasEnumCache()) {
if (object->HasNoElements()) {
keys = original_map->GetCachedEnumKeys();
} else {
keys = object->GetCachedEnumKeysWithElements();
}
} else {
keys = object->GetEnumKeys();
} // For-In Body: for ( size_t i = 0; i < keys->length(); i++) {
// For-In Next:
String* key = keys[i];
if (!object->HasProperty(key) continue ;
EVALUATE_FOR_IN_BODY();
} |
漏洞分析
对于 for-in
语句,V8
会将其转换成一个循环,其主要使用 3 个关键的操作:ForInEnumerate
、ForInPrepare
、ForInNext
,其中 ForInEnumerate/ForInPrepare
主要就是收集对象所有的可枚举属性,然后 ForInNext
用来遍历这些收集的可枚举属性,对于对象属性的访问会调用 JSLoadProperty
:
而如果对象存在 enum_cache
,则在 InliningPhase
阶段会对 JSLoadProperty
进行优化:
在 InliningPhase
存在一个 native_context_specialization
裁剪器:
1
2
3
4
5
6
7
8
9
10
|
struct InliningPhase {
...... AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
AddReducer(data, &graph_reducer, &native_context_specialization);
AddReducer(data, &graph_reducer, &context_specialization);
AddReducer(data, &graph_reducer, &intrinsic_lowering);
AddReducer(data, &graph_reducer, &call_reducer);
...... |
该裁剪器会对一些 JS
原生操作进行优化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Reduction JSNativeContextSpecialization::Reduce(Node* node) { switch (node->opcode()) {
case IrOpcode::kJSAdd:
return ReduceJSAdd(node);
...... case IrOpcode::kJSLoadProperty:
return ReduceJSLoadProperty(node);
case IrOpcode::kJSSetKeyedProperty:
return ReduceJSSetKeyedProperty(node);
...... default :
break ;
}
return NoChange();
} |
可以看到这里会调用 ReduceJSLoadProperty
对 JSLoadProperty
节点进行优化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Reduction JSNativeContextSpecialization::ReduceJSLoadProperty(Node* node) { JSLoadPropertyNode n(node);
PropertyAccess const & p = n.Parameters();
Node* name = n.key(); // obj[key]
// 从之前的 IR 图中可以看出,key 是通过 ForInNext 进行遍历的,所以这里就是 JSForInNext 节点
if (name->opcode() == IrOpcode::kJSForInNext) {
// 调用 ReduceJSLoadPropertyWithEnumeratedKey 进行优化
Reduction reduction = ReduceJSLoadPropertyWithEnumeratedKey(node);
if (reduction.Changed()) return reduction;
}
if (!p.feedback().IsValid()) return NoChange();
Node* value = jsgraph()->Dead();
return ReducePropertyAccess(node, name, base::nullopt, value,
FeedbackSource(p.feedback()), AccessMode::kLoad);
} |
对于 for-in
中的属性加载会调用 ReduceJSLoadPropertyWithEnumeratedKey
进行优化:
这里建议读者自己好好看下这个函数中本身的注释,其写的很清楚
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
|
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); // obj
JSForInNextNode name(NodeProperties::GetValueInput(node, 1)); // JsForInNext
Node* effect = NodeProperties::GetEffectInput(node);
Node* control = NodeProperties::GetControlInput(node);
// 存在 EnumCache
if (name.Parameters().mode() != ForInMode::kUseEnumCacheKeysAndIndices) {
return NoChange();
}
Node* object = name.receiver(); // 理论上是 JSToObject 节点
Node* cache_type = name.cache_type();
Node* index = name.index();
if (object->opcode() == IrOpcode::kJSToObject) {
object = NodeProperties::GetValueInput(object, 0); // object = receiver
}
if (object != receiver) return NoChange();
// No need to repeat the map check if we can prove that there's no
// observable side effect between {effect} and {name].
// 对 map 进行检查
if (!NodeProperties::NoObservableSideEffectBetween(effect, name)) {
// Check that the {receiver} map is still valid.
Node* receiver_map = effect =
graph()->NewNode(simplified()->LoadField(AccessBuilder::ForMap()),
receiver, effect, control);
Node* check = graph()->NewNode(simplified()->ReferenceEqual(), receiver_map,
cache_type);
effect =
graph()->NewNode(simplified()->CheckIf(DeoptimizeReason::kWrongMap),
check, effect, control);
}
// Load the enum cache indices from the {cache_type}.
// 后面就不用多说了,descriptor_array => enum_cache => enum_indices
Node* descriptor_array = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForMapDescriptors()), cache_type,
effect, control);
Node* enum_cache = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForDescriptorArrayEnumCache()),
descriptor_array, effect, control);
Node* enum_indices = effect = graph()->NewNode(
simplified()->LoadField(AccessBuilder::ForEnumCacheIndices()), enum_cache,
effect, control);
// Ensure that the {enum_indices} are valid.
Node* check = graph()->NewNode(
simplified()->BooleanNot(),
graph()->NewNode(simplified()->ReferenceEqual(), enum_indices,
jsgraph()->EmptyFixedArrayConstant()));
effect = graph()->NewNode(
simplified()->CheckIf(DeoptimizeReason::kWrongEnumIndices), check, effect,
control);
// Determine the key from the {enum_indices}.
Node* key = effect = graph()->NewNode(
simplified()->LoadElement(
AccessBuilder::ForFixedArrayElement(PACKED_SMI_ELEMENTS)),
enum_indices, index, effect, control);
// Load the actual field value.
Node* value = effect = graph()->NewNode(simplified()->LoadFieldByIndex(),
receiver, key, effect, control);
ReplaceWithValue(node, value, effect, control);
return Replace(value);
} |
总的来说对于将 for-in
中的快属性访问,会将 JSLoadProperty
节点优化成 obj map check
+ LoadFieldByIndex
节点
接下来我们去看下经过 trubofan
优化后的代码的具体执行逻辑:
获取 map
:
执行完 Builtins_ForInEnumerate
后,返回值 rax
就是 map
的值:
获取描述符数组:
获取 EnumCache
:
获取 EnumCache.keys
:
获取 map.enum_length
:
将 enum_length
、enum_cache
、map
保存在栈上:
每次执行完 callback
后,都会检测 obj2
的 map
是否被改变,如果被改变,则直接去优化;否则通过保存在栈上的 map
获取描述符数组,从而获取 enum_cache
,进而获取 enum_cache.indices
,这里会检测 enum_cache.indices
是否为空,如果为空则直接去优化
但是这里的 enum_length
并没有更新,使用的还是之前保存在栈上的值:
这里
debug
版本有检测,所以没办法展示,而release
版本又用不了job
命令,有点难调试,所以这里得两个对着调(说实话,挺麻烦的,这里其实可以直接在release
中设置一下的)
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!