Misconceptions in Client-Side Security: Reverse Engineering Obfuscation & Disguised Endpoints

July 17, 2017

The information provided in the following is for educational purposes only and should not be used to wrongfully infiltrate or manipulate an application without the legal consent of its owner.

Client-side security is never safe. No matter the practices followed, deterrents used, or tricks added, once content is presented to the user, on their machine, they have access to manipulate it in any form they desire. With that being said, understanding how certain techniques can be reverse-engineered will help a developer comprehend the concept of relying on access control and the server as a gateway to protected content.

A multitude of practices may be used to improve security, a few of which potentially beneficial, but never more than a trivial obstacle for a hacker. With many of the methods causing more issues in a developer’s workflow than they’re worth, it may be unproductive to use these strategies in production.

OWASP Top 10 & False Security

A6-Sensitive Data Exposure is listed as one of the Top 10 Most Critical Web Application Security Risks cautioned by the Open Web Application Security Project (OWASP). In regards to building secure applications, each of the following practices do not conform to this principle and should be understood as such.

What are methods of false security that may be introduced to make the developer feel as though they are implementing a secure solution?

Masking API Endpoints

At the foundation of the modern web is the asynchronous http request. In most cases, understanding how these are architected within an application can tell a infiltrator a lot about the state of the program to find ways in manipulating it.

If following a REST or possibly a GraphQL API implementation, an endpoint is the location in which a request is made for information. The URL appropriately defines the resource which is being retrieved or modified. With this in mind, does abandoning these standard conventions through disguising the target location sound appropriate?

Hiding XMLHttpRequest (XHR) calls in obscure JavaScript files with names irrelevant to the feature or the misnaming of the endpoint itself are two of the many futile attempts to deter snoopers from digging into responses, headers, and other metadata.

Network Request Sniffing

Chrome DevTools’s Network panel (or alternatively Wireshark) can quickly expose these techniques. Especially with the preview feature, XHR calls returning JSON are becoming increasingly easier to examine.

Making use of a misnomer for the location of sensitive data may seem ideal, but in actuality it can confuse developers working with the codebase in the future. Additionally, performing other techniques such as hiding query parameters in the Referer header URL is unnecessary. With the availability to quickly sniff the network calls, these approaches are ultimately frivolous.

Note: Methods using WebSockets have been thought to be preventative of these insecurities but “if you can sniff http: you can sniff ws:. JSONP has also been used through script injection, but this is a very unsecure practice and should not be utilized.

Figure 1. A REST call made to `/assets/css/svg/generate` and returning a property with a Base64 string.

As shown above, a seemingly inconspicuous REST call (/assets/css/svg/generate) returns an encoded Base64 string. Now although there is the possibility that an actual SVG is being optimized here, it is always of interest for a attacker to dissect the response data when it is encoded in Base64.

Base64 Encoding & Modification

Base64 is not encryption. By design it is an ASCII string representation of binary data and should be thought of as such.

Encoding data on the server in a Base64 sequence and then modifying the string before exposing it to the client-side has been used by developers to restrict the plain view visibility of raw payload data.

An Impractical Approach

An example of a modification technique could be as simple as rearranging each character of the string in reverse order and moving the last n characters to the front. By performing this step, or any similar process that the developer uses at their discretion, network sniffing for the encoded string would lead to the inability to decode the response payload.

JavaScript’s native atob() decoding method would throw an error, while other tools found online might be partially more successful. Base64 Decode and Encode may possibly decode the Base64 string, but certainly into a set of undecipherable characters.

Decoding the Request

Sifting through the source files of a web application becomes possibly the most difficult part in debunking client-side security solutions. But through basic find and search techniques, like querying for the Request URL header value or looking for where a response’s property values are used, an intruder can and will eventually find the source of where the request is called from.

function requestGeneratedSVG(){
	return xhr('/assets/css/svg/generate')
			.then(response => {
				let content = parseSVGResponse(response.data);

				// Decoded sensitive data would be handled here.

				return JSON.parse(atob(content));
			});
}

// Note: Unnecessary complexity added to logic to increase level of ambiguity.

function parseSVGResponse(data){
	// Puts the last 64 chars in the front.

	let dataAry = data.split('');
	for (let i = 0; i < 64; i++) {
		let lastChars = dataAry.pop().trim();

		dataAry.unshift(lastChars);
	}

	// Reverses the order of the string.

	let dataOut = [];
	dataAry.forEach(x => dataOut.unshift(x))

	return dataOut.join('');
}

Based on the need to decode the response payload value on the client-side, this approach gives any meddler a direct blueprint of the exact design of this “pseudo-encryption algorithm”. If it is not backed by mathmatical principle and can be cracked in a relatively short period of time, without the use of quantum computing, it is not secure and should never be considered a viable substitute for proper endpoint security.

After implementing all of these erroneous data manipulation techniques, the XHR call may then be obfuscated to further the level of ambiguity of the processing methods used by the application.

Vulnerabilities in Obfuscation

Obfuscation is an approach used to prevent an individual from easily gaining insight into the logic running behind an application. Through manipulation of the source code, formatting, variable-naming, and other declarations become virtually unreadable.

JavaScript Obfuscator & Build Tools

Various build tools are available to developers to perform obfuscation, with a few of the most prominent being the JavaScript Obfuscator, Google Closure Compiler, YUI Compressor, and UglifyJS. Arguably, these processors are just minifiers with added optimizations to mangle code.

After processing the example request and parsing methods through the JavaScript Obfuscator, one can see the source become almost impossible to digest.

var _0xe6a3=["\x64\x61\x74\x61","\x70\x61\x72\x73\x65","\x74\x68\x65\x6E","\x2F\x61\x73\x73\x65\x74\x73\x2F\x63\x73\x73\x2F\x73\x76\x67\x2F\x67\x65\x6E\x65\x72\x61\x74\x65","","\x73\x70\x6C\x69\x74","\x74\x72\x69\x6D","\x70\x6F\x70","\x75\x6E\x73\x68\x69\x66\x74","\x66\x6F\x72\x45\x61\x63\x68","\x6A\x6F\x69\x6E"];function requestGeneratedSVG(){return xhr(_0xe6a3[3])[_0xe6a3[2]](function(_0xe415x2){let _0xe415x3=parseSVGResponse(_0xe415x2[_0xe6a3[0]]);return JSON[_0xe6a3[1]](atob(_0xe415x3))})}function parseSVGResponse(_0xe415x5){var _0xe415x6=_0xe415x5[_0xe6a3[5]](_0xe6a3[4]);for(let _0xe415x7=0;_0xe415x7< 64;_0xe415x7++){var _0xe415x8=_0xe415x6[_0xe6a3[7]]()[_0xe6a3[6]]();_0xe415x6[_0xe6a3[8]](_0xe415x8)};var _0xe415x9=[];_0xe415x6[_0xe6a3[9]](function(_0xe415xa){_0xe415x9[_0xe6a3[8]](_0xe415xa)});return _0xe415x9[_0xe6a3[10]](_0xe6a3[4])}

Renaming variables as hexadecimal integer literals and replacing property values with array references to hex value representations of character codes for ASCII characters, this obsfucator claims to convert JavaScript into a “completely unreadable form, preventing it from analysing and theft.”

Pretty-Printing Obfuscation in Browser

Through the formatting functionality of Chrome DevTools, in-browser source code becomes easier to visualize but with obfuscation, dissecting these methods are far from a menial task.

Figure 2. A pretty-print formatted version of the `requestColors` methods above.

Being a just-in-time compiled and interpreted language, JavaScript virtual machines, like Google’s V8 engine, present very high-performing applications by optimizing and compiling the human-readable source code at runtime. Unless unrealistically compiled down to machine code, the source of any client-side JavaScript application can be reverse-engineered.

Reverse Engineering Obfuscation

With a simple script, similar to the one I’ve quickly developed below, an individual can parse obfuscated JavaScript into a more human-readable format for further inspection.

/**
 * Reverses basic obfuscation techniques used by the JavaScript Obfuscator.
 * 
 * Reference: https://javascriptobfuscator.com/
 * 
 * @param {string} data String representation of a JavaScript file
 * @returns {string}
 */
function reverseObfuscation(data) {
	return replaceArrayReferences(replaceHexArrayValues(replaceHexVariables(data)));
}


/**
 * Replaces all hex values with `_0x` prefix, 4 characters following, 
 * and an optional `x#` value to letter(s) representation.
 * 
 * e.g. `_0xcd03` -> `a` or `_0xea02x8` -> `ab`
 * 
 * @param {string} data JavaScript file text
 * @returns 
 */
function replaceHexVariables(data) {
	// Match all hex values

	let matches = data.match(/_0x.{4}(x[0-9a-z]+)?/g);
	
	// Filters out only unique hex values.

	// Reference: https://stackoverflow.com/a/9229821/5199198

	var detected = {};
	matches
		.filter(value => detected.hasOwnProperty(value) ? false : (detected[value] = true))
		.forEach((hexValue, i) => {
			// Replaces hex value with character

			data = data.replace(new RegExp(hexValue, "g"), numberToLetters(i));
		});

	return data;
}


/**
 * Converts hex values `\x??` found in arrays to their ASCII equivalent.
 * 
 * e.g. `\x63` -> `c`
 * 
 * @param {string} data JavaScript file text
 * @returns {string}
 */
function replaceHexArrayValues(data) {
	return data = data.replace(/\\x.{2}/g, value => String.fromCharCode(parseInt(value.substr(2), 16)));
}


/**
 * Replaces array reference values with the property name stored in an array.
 * 
 * e.g. `e[a[6]]()` -> `e['trim']()`
 * 
 * @param {string} data JavaScript file text
 */
function replaceArrayReferences(data) {
	let arrays = {};

	// Extracts basic array declarations created by the obfuscator into the `arrays` object.

	const arraySyntaxPattern = /var\s(.+)\s=\s(\[.+\]);/g;
	data.replace(arraySyntaxPattern, (match, arrayName, arrayString) => {
		// Be wary of the use of `eval` here

		// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

		arrays[arrayName] = eval(arrayString);
	});

	// Replaces array references with the corresponding value from the `arrays` object.

	const arrayReferenceSyntaxPattern = /([a-z]+)\[([0-9]+)\]/g;
	data = data.replace(arrayReferenceSyntaxPattern, (match, arrayName, indexValue) => {
		return Array.isArray(arrays[arrayName]) && arrays[arrayName][indexValue] !== undefined ? `'${arrays[arrayName][indexValue]}'` : match;
	});

	return data;
}


/**
 * Converts integers to multi-character strings.
 * 
 * e.g. `0` -> a or `26` -> `aa`
 * 
 * Reference: https://stackoverflow.com/a/32007970/5199198
 * 
 * @param {number} i Position of value
 * @returns {string}
 */
function numberToLetters(i) {
	return (i >= 26 ? numberToLetters((i / 26 >> 0) - 1) : '') +  'abcdefghijklmnopqrstuvwxyz'[i % 26 >> 0];
}

And now, the reverse-engineered methods are very similar to the originals.

var a = ["data", "parse", "then", "/assets/css/svg/generate", "", "split", "trim", "pop", "unshift", "forEach", "join"];

function requestGeneratedSVG() {
    return xhr('/assets/css/svg/generate')['then'](function(b) {
        let c = parseSVGResponse(b['data']);
        return JSON['parse'](atob(c))
    })
}

function parseSVGResponse(d) {
    var e = d['split']('');
    for (let f = 0; f < 64; f++) {
        var g = e['pop']()['trim']();
        e['unshift'](g)
    };
    var h = [];
    e['forEach'](function(i) {
        h['unshift'](i)
    });
    return h['join']('')
}

Deobfuscation, Web Scraping, & ASTs

Among the various deobfuscation tools available, a simple script that reads in a JavaScript file and parses it, like displayed above, is plenty for an infiltrator to analyze how these manipulation methods modify the XHR response. Alarmingly so, one may potentially even utilize the original source code to write a web scraping script to automate the processing of this sensitive data.

For more complex reverse obfuscation techniques, one could parse their script into an abstract syntax tree (AST) and more efficiently convert these using Babel’s Babylon or with one of the many other JavaScript AST parsers.

Thinking Pragmatically

As with any application, it is up to the developers to decide which practices they feel have technical worth and whether they will limit a hacker’s ability to manipulate the state of their application. But certain mechanisms like authorization frameworks, such as OAuth with JSON Web Tokens (JWT), are truly essential. Proper endpoint security solutions to ensure that sensitive resources are guarded safely and requests are coming from authenticated sources are imperative requisites for secure web applications.

Recognizing when a security implementation does more harm than added value to the application is the hardest part about deciding whether to use some of the tactics discussed. Take into consideration the above notions, perform research of your own, and make informed decisions.