Defending against JavaScript side-channels

Mahesh Paolini-Subramanya
3 min readMar 13, 2018

--

You’ve heard of “Micro-architectural attacks” yes? You know, stuff like Rowhammer, ASLR bypass, DRAM addressing attacks, and so forth? An excellent paper by Schwarz et al. — reviewed by Adrian Colyer here— describes how one can protect against these attacks coming in via JavaScript. Which basically translates to “preventing any random website from hacking you” 🙌.

The one I found particularly fascinating was how JavaScript can be used to identify memory addresses, and how JavaScript Zero (the solution described in the paper) defeats it. The thing about JavaScript is, it’s sandboxed nature ensures that virtual addresses are never actually disclosed to the user. That said,

  1. JavaScript has ArrayBuffers — what is effectively a block of virtual memory that is directly accessible to the user in a fast and efficient way
  2. Browsers allocate ArrayBuffers in a page-aligned manner (at the start of a new physical page, and with the least significant 12 bits set to ‘0’)
  3. Browsers use mmap to allocate larger chunks of memory
  4. mmap is optimized to allocate 2 MB transparent huge pages (THP).

This is where the side-channel attack kicks in. The physical pages in a THP are mapped on demand (when the first access occurs). So, as you iterate over the array indices in your ArrayBuffer, when you get to the first new physical page will incur a page fault, and accessing the underlying data will take much longer (it’s not just memory access, y’see?). And so, you — or more to the point, the attacker — now knows the index at which a new page starts, a page that has the 21 least significant bits set to ‘0’.

As these physical pages are mapped on demand, i.e., as soon as the first access to the page occurs, iterating over the array indices results in page faults at the beginning of a new page. The time to resolve a page fault is significantly higher than a normal memory access. Thus, an attacker knows the index at which a new 2 MB page starts. At this array index, the underlying physical page has the 21 least significant bits set to ‘0’.

Nifty, isn’t it? And one of those things that you have to be a really sneaky bugger to have thought of in the first place 😮.

And this is where we get to Javascript Zero, and preventing side-channel attacks via ArrayBuffers. The solution, it turns out, is equally simple (and not so sneaky). In particular

  1. Buffer ASLR: When you ask for an ArrayBuffer, behind the scenes, the request adds an extra 4KB. The array you get back is randomly located within that “size + 4KB” array that was actually allocated. This prevents attackers from assuming that the array starts at a new physical page.
  2. Preloading: When the array is allocated, the system proceeds to iterate through it, triggering all the page-faults. That way, the user can’t trigger page-faults by iterating through the array (it’s already loaded in memory!), and finding page boundaries
  3. Non-determinism: The array’s setter is modified so that each access also involves an access to a random location in the same array. That way, the attacker can’t know the actual index at which a page-fault was triggered (it prevents the user from just waiting for the Preloaded pages to be swapped out)

There’s more — much more — including Array Index Randomization, Timestamp manipulation, Multithreading updates, and more, but the above should serve to explain both how the attacks work, and how some of the attacks can be prevented.
Performance is remarkably good too, largely because JITs are constantly optimizing this stuff. The bottom line is that, by and large, people had no idea that this was running in their browsers.

Read the paper for the whole thing, or Adrian Colyer’s summary for, well, a summary.

--

--