Description

Unravel the layers of malvertising to uncover the Flag.
https://malvertising.web.ctfcompetition.com

Analysis

We’re given the webpage of the link above. When we read the source, an iframe to ads/ad.html appears. Clicking it, we notice the file src/metrics.js, which is, not only minimized, but completely obfuscated.

Procedure

Stage 1

After a few hours of cleaning the code, we reach the following conclusions:

  • There’s a method, this['steg'], which does quite a few mathematics, and, in a nutshell, decrypts some input. We really don’t care about how it does it, it gets called only once.
  • Then there’s the method called b, which decodes the contents of an array of base64 strings called a. It implements a cache so that if the same ID is tried to be decoded twice, it doesn’t have to be computed again. This allows us to, using the browser’s debugger, read the stored contents and keep track of what’s going on.
  • There are anti-deobfuscator protections, so that the prettified code cannot be executed. These mainly consist in a method which returns a string. Another method gets the code of the last one (calling toString), and checks whether that string follows a given RegEx. In order to deobfuscate this, we must copy the exact code from the unprettified file, and run the regex to get the boolean output in the console of the browser’s developer tools.
  • The actual code does the following: messes with a, sorting it so that it’s difficult to deduce in static analysis, gets the image of the ad, calls this['steg'], and saves de decrypted string, which is JS code. Then, it checks whether the UserAgent of the browser contains android. If it does, it executes the javascript code; otherwise, it doesn’t do anything.

We get the decrypted JS code via the browser’s debugger, and we see that it’s just loading another script: src/uHsdvEHFDwljZFhPyKxp.js.

Stage 2

We realise there’s a huge dictionary which contains only functions, and a base64 string at the end of the file. After some cleaning, we know that there are no anti-deobfuscators nor anti-debuggers, and that most of the functions are used to decrypt that base64 string. The key is computed in the same JS, using the following formula:

key = navigator.platform.toUpperCase().substr(0, 5) +
Number(/android/i.test(navigator.userAgent)) +
Number(/AdsBot/i.test(navigator.userAgent)) +
Number(/Google/i.test(navigator.userAgent)) +
Number(/geoedge/i.test(navigator.userAgent)) +
Number(/tmt/i.test(navigator.userAgent)) +
navigator.language.toUpperCase().substr(0, 2) +
Number(/tpc.googlesyndication.com/i.test(document.referrer) || /doubleclick.net/i.test(document.referrer)) +
Number(/geoedge/i.test(document.referrer)) +
Number(/tmt/i.test(document.referrer)) +
performance.navigation.type +
performance.navigation.redirectCount +
Number(navigator.cookieEnabled) +
Number(navigator.onLine) +
navigator.appCodeName.toUpperCase().substr(0, 7) +
Number(navigator.maxTouchPoints > 0) +
Number((undefined == window.chrome) ? true : (undefined == window.chrome.app)) +
navigator.plugins.length

For instance, if I run that code I get the following key: LINUX00000ES0000011MOZILLA010. If we copy the whole JS file and run it on local, we can call the decrypt function with the key we want and get the decrypted string. After playing around with that for a bit, we notice that changing the last three numbers does not change the result, so the decrypt function doesn’t care about those. The same thing applies to the three numbers right before the MOZILLA in my example key. So we have the platform in uppercase, followed by a binary string of length 5, the language, another binary string (this of length 4), three numbers that are not checked, the appCodeName, and other three numbers that are not checked.

We have to bruteforce this, but there are way too many possibilities, so we must use some logic. First, the platform in uppercase: this code is meant to be executed in an Android, and Android uses Linux as its kernel. With the code above we know that the platform name must be 5 characters long. LINUX is 5 characters long. Therefore, the platform in uppercase is LINUX.

We also know that both Chrome and Firefox have the appCodeName MOZILLA, and that it’s 7 chars long, so that field is MOZILLA for sure.

Now we have to crack 5 bits, two characters for the browser’s language, and another 4 bits. This is not too hard, specially since the wrong key decrypts the message as garbage (such as ê,ôÄÐ@@äd§]Ú...).

The code actually calls eval passing as an argument the decrypted string, so we know it’s JS code. In order to find the correct key, we can discard those that do not produce JS code and manually have a look at the rest. For this part, I’ve discarded those results that do not contain ( or ), and those that contain tilded characters (á, è, Ú, …). This is the code I’ve used to crack it:

cte = "A2xcVTrDuF+EqdD8VibVZIWY2k334hwWPsIzgPgmHSapj+zeDlPqH/RHlpVCitdlxQQfzOjO01xCW/6TNqkciPRbOZsizdYNf5eEOgghG0YhmIplCBLhGdxmnvsIT/69I08I/ZvIxkWyufhLayTDzFeGZlPQfjqtY8Wr59Lkw/JggztpJYPWng=="
NO = "ÁÉÍÓÚÀÈÌÒÙáéíóúàèìòù"
for(var i="A".charCodeAt(0); i <= "Z".charCodeAt(0); i++) {
	for(var j="A".charCodeAt(0); j <= "Z".charCodeAt(0); j++) {
		language = String.fromCharCode(i)+String.fromCharCode(j)

		for(var bin=0; bin<512; bin++) {
			a = bin.toString(2)
			while(a.length !== 9) a = '0'+a

			b_ = 'LINUX'+a.substr(0, 5)+language+a.substr(5, 9)+'000MOZILLA000'
			b = T.d0(cte, b_)	// Decrypt with the key.

			if(b.includes('(') && b.includes(')')) {
				ok = true
				for(var k=0; k<NO.length && ok; k++) {
					if(b.includes(NO[k])) ok = false
				}
				if(ok) {
					console.log(b_)
					console.log(b)
				}
			}
		}
	}
}

In a few seconds, we get the right key, LINUX10000FR1000000MOZILLA000, as well as the decrypted code, which just includes the file src/npoTHyBXnpZWgLorNrYc.js.

Stage 3

The code in npoTHyBXnpZWgLorNrYc.js has everything you’ve ever dreamed of: a big array of hexadecimal encoded base64 strings, anti-prettifier methods (same as stage 1), messy anti-debuggers, a similar b function, RTC stuff…

After some time, we get rid of the anti-prettifiers and anti-debuggers, which basically call the debugger method in diferent forms, some of them through the b function. They’re nasty but they do not actually require much focus.

Then, we modify our local version of the code so that it gets all the b decrypted values, and one of them, b('0x18', '\x4c\x5d\x34\x37'), gives us the following value in the debugger: ./src/WFmJWvYBQmZnedwpdQBU.js.

We load that JS file in the browser, and admire the solved challenge:

alert("CTF{I-LOVE-MALVERTISING-wkJsuw}")

One thought to “Write-Up Google CTF – “Malvertising””

  • Hi!

    By the way, the reason the last part of the key string was ignored is because the encryption algorithm used was TEA, (tiny encryption algorithm), tha only uses 16 bytes keys.

    Reply

Leave a comment

Your email address will not be published. Required fields are marked *