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.
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;
}
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';
}
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