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 calleda
. 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, callsthis['steg']
, and saves de decrypted string, which is JS code. Then, it checks whether the UserAgent of the browser containsandroid
. 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
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 npoTHyBXnpZWgLorNrYc.js
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””
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.