Hello readers! As promised in previous blog post, today I’ll write (a bit more technically) about third party JS security, but from a different angle.

Privacy Badger

Privacy Badger is a privacy focused browser extension by EFF, that detects and blocks third party trackers. Unlike other extensions, it does it by analyzing the tracking behaviors, rather than relaying on domains blacklist.

Canvas fingerprinting

On of these tracking behaviors is canvas fingerprinting, which I briefly mentioned in previous blog posts. Generally speaking, canvas fingerprinting is a method to generate stateless, consistent, high entropy identifier from the HTML5 canvas element, by drawing several graphics primitives into it and then serialize its pixels. Different browsers and devices produce slightly different pixels due to differences in their graphics rendering stack. You can read the paper “Pixel Perfect: Fingerprinting Canvas in HTML5” for more info.

Privacy Badger Canvas fingerprinting detection

From Privacy Badger website:

Does Privacy Badger prevent fingerprinting? Browser fingerprinting is an extremely subtle and problematic method of tracking, which we documented with the Panopticlick project. Privacy Badger 1.0 can detect canvas based fingerprinting, and will block third party domains that use it. Detection of other forms of fingerprinting and protections against first-party fingerprinting are ongoing projects. Of course, once a domain is blocked by Privacy Badger, it will no longer be able to fingerprint you.

How Privacy Badger detect canvas fingerprinting

Privacy badger injects fingerprinting.js, along with several other context scripts, as specified in its manifest.json, to all the frames (“all_frames“: true) of all the pages (“matches”: [ “<all_urls>” ]) visited by the user, before any other script in the page has executed (“run_at“: “document_start“).

Content script have access to their frame DOM, but a separate JavaScript context. Because the goal of the script requires to monitors things that happen in the page JS context (canvas manipulation and serialization), this content script injects another, self removing script into the frame DOM, which executes in its JS context.

This script hooks into several canvas related APIs, including fillText (manipulation) and toDataURL (serialization). I wrote about JS hooking before, in the context of spoofing viewabiliy measurements. Whenever once of these APIs gets called, Privacy Badger hook is figuring out the caller script URL form within the call stack.

Threat Model

When designing and implementing fingerprinting countermeasures, there are two significant concerns:

Observability: which means trackers can fingerprint the presence of the fingerprinting countermeasure itself and using it as another data point in the fingerprint.

Bypassability: which means tracker can evade the fingerprinting countermeasure or rendering it useless, thus getting access to the desired fingerprinted feature.

Vulnerabilities in Privacy Badger canvas fingerprinting detection

Observability of the canvas API hooking:

as I wrote previously in depth at “JavaScript tampering – detection and stealth” (my most visited blog post so far!), there are several methods to detect that a native function was tampered with. Privacy Badger recognized this threat and tries to hide the tampering by setting the length, name, and toString properties of the hooked functions to match those of the original, but without referring to the native Function.protype.toString, a tracker can write:

Function.prototype.toString.call(HTMLCanvasElement.prototype.toDataURL);

And get:

"function wrapped() { var args = arguments; ...

Of course, it also won’t pass the prototype and hasOwnProperty test (detailed explanation here).

Bypassability of the APIs hooking

Privacy Badger recognized this threat site code tampering with its own code, and tries to prevent this by copying the objects it uses into its own function scope. However, it still relies on prototype inherited methods inside the hook code itself, and these methods can be abused to steal the reference to the original API. Let’s look closely on the hook code itself, which gets called whenever a consumer calls one of the hooked canvas APIs:

function wrapped() { var args = arguments; if (is_canvas_write) { // to avoid false positives, // bail if the text being written is too short if (!args[0] || args[0].length < 5) { return orig.apply(this, args); } } var script_url = ( V8_STACK_TRACE_API ? getOriginatingScriptUrl() : getOriginatingScriptUrlFirefox() ), msg = { obj: item.objName, prop: item.propName, scriptUrl: script_url }; if (item.hasOwnProperty('extra')) { msg.extra = item.extra.apply(this, args); } send(msg); if (is_canvas_write) { // optimization: one canvas write is enough, // restore original write method // to this CanvasRenderingContext2D object instance this[item.propName] = orig; } return orig.apply(this, args); }

As we can see, there’s an interesting exception: if is_canvas_write is true and the length of the first arg is shorter then 5, the original function gets called, using the prototype inherited apply method, and returns before send(msg) is called, so Privacy Badger won’t be considering it as a fingerprinting attempt, to avoid false positives.

We can look few lines up and see that is_canvas_write is computed as:

var is_canvas_write = ( item.propName == 'fillText' || item.propName == 'strokeText' );

So, our attack will look like this:

Hook the apply method Call the hooked fillText or strokeText Steal the reference to the original fillText or strokeText Write to the canvas text with length > 5 using the original function



Let’s implement a PoC:

let _apply = Function.prototype.apply; let original; Function.prototype.apply = function () { // `this` is the function if (this.name === 'fillText' || this.name === 'strokeText') { original = this; } // restore the original apply Function.prototype.apply = _apply; };

Then, we call the function:

var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); ctx.fillText('a');

And now we have the original fillText:

original ƒ fillText() { [native code] }

Viola!

The same technique can be used to extract the original serialization method, toDataURL. Notice the call to getOriginatingScriptUrl which is also using prototype inherited methods that can be tampered with.

Another bypass method is to obtain a references to the original APIs by using the iframe sandbox attribute. This attribute allows us to specify permissions for the content inside the iframe, and if we specify the allow-same-origin permission and don’t specify the allow-scripts permission, the script injected by the context script won’t execute, according the the sandbox policy[1], but the embedding page will be able to access the iframe’s contentWindow and obtain an unhooked canvas from it.

That’s it for today! Although this topic could be expanded even more, I’ll save something for next time 🙂

Hope you enjoyed, and feel free to contact me to discuss any of it!

[1] This is currently true in Firefox, but not in Chrome. In the past I observed the same behavior in Chrome, but from my test it seems like now DOM script that was added from content script will execute inside sandboxed iframes. I’m not sure if that’s intentional.