JavaScript tampering – detection and stealth

In my blog post about spoofing viewability measurements, I mentioned that:

The main drawback of this method is that it can be detected by using reflection, that is, it’s possible to detect that the APIs were tampered with, although I believe it’s possible to over come this drawback. Maybe I’ll write how in the future 🙂

In a reddit reply, Eliya Stein elaborated a little bit more:

These are fun examples, but this is such a game of cat and mouse. One byproduct of Javascript being so mutable is that a third party can check if you’ve tampered with native methods. For example:

document.hasFocus.toString() will return "function hasFocus() { [native code] }"

However, after it has been tampered with, it will return: "function () { return true; }"

I would hope that viewability vendors check for this kind of tampering and take it into consideration for their scoring.

So today I’ll deliver my promise and dive into JavaScript tampering, both from the perspective of the detection and stealth (avoiding detection). But first, what is tampering and why even doing it?

In JavaScript, methods can be easily re-assigned by a script and the change will affect all the other scripts running in the same environment. Technically speaking, any object property with writable property descriptor can be re-assigned, as simple as:

var o = { 
	a: function () {
		return 123;
	} 
};
o.a(); // 123
o.a = function () {
	return 456;	
};
o.a(); // 456

Historically, tampering, aka monkey-patching, overriding, instrumenting, augmenting, hooking and decorating, has been considered a bad practice, especially when applied to builtin and host objects. The once popular Prototype.js was notoriously designed around this approach, and it caused many, many nasty unexpected bugs due to the non-standard complying nature of the browser’s host objects.

When tampering with builtin or script defined objects, the main reason it considered a bad practice is the risk of non-transparent changes, i.e., changing the function signature or return value, which can break the assumptions of other code accessing these functions. For example, calling native Date.parse(”) returns NaN, and calling the tampered by the DateJS library Date.parse(”) returns null.

However, there are some use-cases where tampering is inevitable, and when applied with caution, it can be quite useful. For example, performance monitoring (for how long did function executed?) and profiling (aka dynamic analysis, which functions are executed and with what arguments?).

In the context of adtech, tampering is used both by bad guys:

  • Viewability spoofing, as mentioned above
  • Bots tampering with functions to conceal its true identity and appear legitimate to verification vendors

And good guys:

  • Client side malvertising protection
  • Bot detection vendors

Detecting tampering is done via “reflection”, which in JavaScript basically means you can see a given function’s code by converting it into a string, using the builtin Function.prototype.toString, or the SpiderMonkey (Firefox’s JS engine) specific Function.prototype.toSource.

For example:

console.log.toString();
"function log() { [native code] }"

However, if we tamper with the log function, we can spot the change:

console.log = function () {
	return 123;
};
console.log.toString();
"function () {
	return 123;
}"

So lets assume we want to spoof viewability measurements and there’s new viewability measurement technique that uses console.log. We want to change the native console.log function behavior with one script, and we want to hide it from any other script running in the same context so viewability vendors won’t know we are cheating. What can we do? a naive solution would be to tamper with the toString method itself:

console.log = function () {
	return 123;
};

console.log.toString = function () {
	return 'function log() { [native code] }';
};

console.log.toString();
"function log() { [native code] }"

Ah ha! However, what about the toString of the toString? :

console.log.toString.toString()
"function () {
	return 'function log() { [native code] }';
}"

It happens because JavaScript is designed around prototypal inheritance, so toString, being a function, inherits its own toString method from the builtin Function.prototype.toString. It’s also possible to call the native prototype method itself:

Function.prototype.toString.call(console.log)
"function () {
	return 'hello';
}"
Function.prototype.toString.call(console.log.toString)
"function () {
	return 'function log() { [native code] }';
}"

And detect the tampering.  Prototypal inheritance also means that overriding the prototype toString with own property returns:

console.log.toString === Function.prototype.toString;
false
console.log.hasOwnProperty('toString');
true

Where native toString returns:

console.log.toString === Function.prototype.toString;
true
console.log.hasOwnProperty('toString');
false

Bottom line, another strategy is needed. So, how about tampering with the prototype toString itself?

// save reference to the original toString
_ts = Function.prototype.toString;

// override the prototype toString
Function.prototype.toString = function () {
	// 'this' is the function which toString is called upon
	// is it toString? i.e. toString.toString() case
	if (this === Function.prototype.toString) { 
		// as if nothing changed
		return 'function toString() { [native code] }';
	}
	// if toString is called upon console.log
	if (this === console.log) {
		// as if nothing changed
		return 'function log() { [native code] }';
	}
	// for all other functions, call the original toString
	return _ts.call(this);
}

Let’s see if it passes all the tests:

console.log.toString();
"function log() { [native code] }"

console.log.toString.toString();
"function toString() { [native code] }"

Function.prototype.toString === console.log.toString;
true

Function.prototype.toString.call(console.log);
"function log() { [native code] }"

Looks good! But still not perfect:

Function.prototype.toString.name
""

Where’s the native toString returns:

Function.prototype.toString.name
"toString"

However, this is easy to solve: switching from anonymous to named function when re-assigning toString:

// override the prototype toString with named function
Function.prototype.toString = function toString () {...

Another behavioral difference between host functions and script defined ones, is upon calling them with the new operator. Calling the native toString returns:

new Function.prototype.toString();
Uncaught TypeError: Function.prototype.toString is not a constructor

Where’s calling our tampered toString returns:

new Function.prototype.toString();
Uncaught TypeError: Function.prototype.toString requires that 'this' be a Function

So, we can check if our tampered function is called with the new operator by checking whether the “this” argument is the newly created object by the new operator, i.e., it has toString as its constructor:

// if toString is called with the new operator
if (this.constructor && this.constructor === toString) {
	throw new TypeError('Function.prototype.toString is not a constructor');
}

To recap, our final tampered toString now looks like this:

Function.prototype.toString = function toString () {
	// 'this' is the function which toString is called upon
	// is it toString? i.e. toString.toString() case
	if (this === toString) { 
		// as if nothing changed
		return 'function toString() { [native code] }';
	}
	// if toString is called upon console.log
	if (this === console.log) {
		// as if nothing changed
		return 'function log() { [native code] }';
	}
	// if toString is called with the new operator
	if (this.constructor && this.constructor === toString) {
		throw new TypeError('Function.prototype.toString is not a constructor');
	}
	// for all other functions, call the original toString
	return _ts.call(this);
}

Is it perfect? not yet. The one problem I couldn’t find a solution for is the “prototype” property. i.e., the native toString doesn’t have one:

'prototype' in Function.prototype.toString;
false

Where’s the tampered:

'prototype' in Function.prototype.toString;
true

You might think “oh, just use the delete operator“, but it won’t work since it’s a non-configurable property:

delete Function.prototype.toString.prototype;
false

'prototype' in Function.prototype.toString;
true

Let me know if you can find a solution to this one!

Actually, there’s one solution I can come up with: using Proxy. Proxy object allow strong meta-programming capabilities by intercepting and modifying the behavior of multiple fundamental operations of the language. One of them is “apply”, which let you define custom behavior to function invocations:

var toStringProxy = new Proxy(Function.prototype.toString, {
	apply: function (target, thisArg, args) {
		if (thisArg === Function.prototype.toString) {
			return 'function toString() { [native code] }'
		}
		if (thisArg === console.log) {
			return 'function log() { [native code] }';
		}
		return target.call(thisArg);
	}
});

Function.prototype.toString = toStringProxy;

This code passes all the tests we tried so far, including the prototype test. Proxy is well suited for the task of being undetectable because it has the property of “transparent virtualization”, which means there’s no way to tell if a given object is a proxy or a regular object [1].

Anyway, I’d still love to hear if you can find a solution that doesn’t use Proxy 🙂

So far so good, but another script in browser environment can still detect the tampering by obtaining reference to a native toString function from another browsing context, i.e., iframe:

var i = document.createElement('iframe');
document.body.appendChild(i);
i.contentWindow.Function.prototype.toString.call(console.log);
function () { return 123; }

Solving this will require us setting up a getter function on contentWindow and contentDocument, in order to tamper with the iframe’s toString, but then another detection option is to execute the toString inside the new iframe, so our getter won’t be called. This requires us to execute the toString tampering in any newly created iframe, which is cumbersome and non-trivial to implement. I’ll leave it as an exercise to the reader 🙂

If you want to see more how all of this is used in practice for bot detection, I recommend these great slides by Ryan Castellucci.

Stay undetectable and tuned for my next post!

1. Although mentioned in few (credible) sources, I couldn’t find this phrase in the ECMAScript standard. Please let me know if you can point out to the relevant section!

 

 

2 thoughts on “JavaScript tampering – detection and stealth

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s