CTFs, Programação

Como finalizei o desafio “Malvertising” do Google CTF 2019

Tempo de leitura: 11 minutos

Na área de engenharia reversa do Google CTF de 2019 tinha um desafio chamado “Malvertising”. Neste desafio, o competidor deveria fazer o reverse de códigos de anúncios para conseguir revelar a flag. Então vamos lá.

Primeira etapa

Quando entrei no link ( https://malvertising.web.ctfcompetition.com/ ) vi que a página é uma cópia do Youtube. Ao analisar o código fonte da página, percebi que tudo é uma imagem de background de um print do Youtube e existe um “iframe” com source em “ads/ad.html”.

Abrindo o site https://malvertising.web.ctfcompetition.com/ads/ad.html, analisei o código e percebi a inserção de um script com source em “./src/metrics.js”. Abrindo https://malvertising.web.ctfcompetition.com/ads/src/metrics.js, me é mostrado um código minificado e obfuscado. A primeira coisa que fiz foi usar um “unminificador” (unminify.com) para obter um código legível, selecionei o código, joguei no Notepad++ e comprimi as declarações de todas as funções para ficar mais fácil de ver o fluxo do código principal. Isso aqui foi o que me apareceu:

Código de metrics.js

Vi que no fluxo principal existe um array de strings em base64 que quando decodificadas aparecem dados binários sem sentido.

Analisando a função “l”, vejo que constantemente é chamada a função “b”. Então fiz um teste: selecionei a chamada para a função “b” da linha 370 do código javascript e rodei no console do meu navegador. Isso foi o que me apareceu:

Código de descriptografia do metrics.js

Ummm… Interessante… Copiei algumas outras chamadas para a função “b” nas linhas 371, 372 e 373 e executei no console. Esses foram os resultados:

Código de descriptografia do metrics.js

Então vi que a função “b” é, na verdade, uma função que descriptografa uma string qualquer! Legal!

Decidi voltar minha atenção para as 10 últimas linhas (390 a 399). Descriptografei todas as strings com chamadas para a função “b” e coloquei em comentários o que era cada coisa.

Strings descriptografas

Na linha 396, convertendo os números hexadecimais para char, descubro que significa “android”.

Reescrevendo essas linhas obfuscadas, fica assim:

var s = "constructor";
var t = document['getElementById']('adimg');
t['onload'] = function() {
    try {
        var u = steg['decode'](t);
    } catch (v) {}
    if (Number(/android/i ['test'](navigator['userAgent']))) {
        s[s][s](u)();
    }
};

Basicamente temos o seguinte: Se o “userAgent” do navegador que estiver vendo essa página contiver a palavra “android”, então executa o código da linha 410. O que eu fiz então foi, no Developer Tools, colocar um breakpoint na linha 469 e recarreguei a página. Este foi o resultado:

Breakpoint em metrics.js

Dentro do “try”, a variável “u” é definida com o valor: “var dJs = document.createElement(‘script’); dJs.setAttribute(‘src’,’./src/uHsdvEHFDwljZFhPyKxp.js’); document.head.appendChild(dJs);”. Então se o navegador estiver rodando em um Android, é adicionado este código na página! Ahááááá! Legal! Estou indo bem!

Segunda etapa

Acessei o código de https://malvertising.web.ctfcompetition.com/ads/src/uHsdvEHFDwljZFhPyKxp.js e novamente um código minificado. Usei o site unminificador novamente, copiei e colei no Notepad++, comprimi as declarações de funções e fiquei com isso no fluxo principal:

a = "A2xcVTrDuF+EqdD8VibVZIWY2k334hwWPsIzgPgmHSapj+zeDlPqH/RHlpVCitdlxQQfzOjO01xCW/6TNqkciPRbOZsizdYNf5eEOgghG0YhmIplCBLhGdxmnvsIT/69I08I/ZvIxkWyufhLayTDzFeGZlPQfjqtY8Wr59Lkw/JggztpJYPWng=="
eval(T.d0(a, dJw()));

Bem… Ok… Então a string “a” é enviada como parâmetro para a função d0 de “T” e o resultado de dJw() também é enviado como pamâmetro.

Eu peguei o código do “try” da função dJw e joguei no meu console. O resultado foi o seguinte:

Rodando o código no console

“WIN3200000PT0000011MOZILLA003”. Vamos quebrar essa string:

WIN32 – plataforma que o navegador está rodando
0 – Se tem “android” no userAgent do navegador. Neste caso, 0 significa que não tem
0 – Se tem “AdsBot” no userAgent do navegador. Neste caso, 0 significa que não tem
0 – Se tem “Google” no userAgent do navegador. Neste caso, 0 significa que não tem
0 – Se tem “geoedge” no userAgent do navegador. Neste caso, 0 significa que não tem
0 – Se tem “tmt” no userAgent do navegador. Neste caso, 0 significa que não tem
PT – As duas primeiras letras em maiúsculo da língua do navegador
0 – Verifica se a URL que continha o link para essa página tinha a string tpc.googlesyndication.com ou doubleclick.net. 0 significa que não tinha
0 – Verifica se a URL que continha o link para essa página tinha a string “geoedge”. 0 significa que não tinha
0 – Verifica se a URL que continha o link para essa página tinha a string “tmt”. 0 significa que não tinha
0 – performance.navigation.type = 0 significa que “a página foi acessada clicando em algum link, pelos favoritos, por submissão de forma, por um script ou digitando a URL diretamente na barra de endereços” (documentação da Mozilla em https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation/type )
0 – performance.navigation.redirectCount diz “o número de REDIRECTs feitos antes de chegar na página” (documentação da Mozilla em https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation/redirectCount )
1 – navigator.cookieEnabled diz se os cookies estão liberados ou não. Neste caso, está
1 – navigator.onLine diz se o navegador está online ou não. Neste caso, está.
MOZILLA – navigator.appCodeName retorna o codinome do navegador. “Todos os navegadores modernos retornam “Mozilla” ( https://www.w3schools.com/jsref/prop_nav_appcodename.asp ).
0 – navigator.maxTouchPoints diz o “número máximo de pontos de contato de toque suportados pelo atual dispositivo”
0 – (undefined == window.chrome) ? true : (undefined == window.chrome.app). window.chrome é um objeto exclusivo do Google Chrome e não é documentado
3 – navigator.plugins.length diz quantos plugins existem no navegador

Decidi criar uma cópia local de todo este código javascript  unminificado na minha máquina e um arquivo html que chamaria este código. Abri este arquivo local e executei a função T.d0 com os argumentos “a” e “WIN3200000PT0000011MOZILLA003” no console. O resultado foi este:

Executando código de descriptografia

Ok. Substitui o segundo argumento para WIN3200000PT0000011MOZILLA000. Executei a função e o resultado foi o mesmo… Mudei o segundo argumento para “WIN3200000PT00000110000000000” (tirei o MOZILLA e preenchi com 0), executei e deu o mesmo resultado…

Olhando a função d0, vi isso: “b.u0().slice(0, 16)”. Será que a função de “descriptografia” só leva em consideração os 16 primeiros caracteres? Bem, vamos testar.

Executei a função d0 com a string “WIN3200000PT0000” (16 caracteres) e deu o mesmo resultado das outras, mas quando mudei para WIN3200000PT0001, aí o resultado mudou! Veja:

Executando código de descriptografia

Ok! Então eu terei que descobrir qual o valor correto para descriptografar a string “a” e, finalmente, conseguir o valor correto. Fazer força bruta em 16 bytes será difícil… Então vamos reduzir as possibilidades.

Primeiro devemos lembrar que o site espera que eu acesse a página por um Android!!! Quais são os valores retornados pelo Android?

Para descobrir, eu criei um arquivo html com o seguinte código:

<script>
document.write(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 &gt; 0) + Number((undefined == window.chrome) ? true : (undefined == window.chrome.app)) + navigator.plugins.length);
</script>

Salvei este arquivo no meu desktop como teste.html e abri um servidor web simples com php usando o comando: php -S MeuIPnaRede:8080

No meu Android, abri o Chrome e acessei http://MeuIPnaRede:8080/teste.html

O resultado que apareceu em meu navegador foi: LINUX10000PT0000011MOZILLA110. Apenas os 16 primeiros caracteres são levados em consideração. Portanto “LINUX10000PT0000”. Testei usar este valor na função de descriptografia e não deu certo…

Tá. Ok. Então vamos ver o que eu posso modificar dessa lista.

O primeiro campo é a plataforma que o navegador está rodando. O Android é um Linux. Por isso esse valor será fixo.

O segundo valor diz se tem “android” no userAgent. Esse terá que ser 1 sempre.

Os próximos 4 bytes verificam se contem strings diversas no userAgent. Em teoria deve ser 0… Mas não sei se é algum truque do Google… Então vou chutar que pode mudar.

Porém estes são valores booleanos. Ou seja, só podem ser 0 ou 1. Isso reduz para apenas 2 possibilidades nestes 4 bytes. O que dá 2^4 = 16! Fácil.

Depois vem a língua do navegador. Eu poderia percorrer todas as possibilidades de AA até ZZ. Isso me daria 26^2 = 676 possibilidades. Mas quero reduzir isso. Procurei pelos valores possíveis para línguas de navegadores e achei no StackOverflow um array com as possibilidades:

[“af”, “sq”, “ar-SA”, “ar-IQ”, “ar-EG”, “ar-LY”, “ar-DZ”, “ar-MA”, “ar-TN”, “ar-OM”, “ar-YE”, “ar-SY”, “ar-JO”, “ar-LB”, “ar-KW”, “ar-AE”, “ar-BH”, “ar-QA”, “eu”, “bg”, “be”, “ca”, “zh-TW”, “zh-CN”, “zh-HK”, “zh-SG”, “hr”, “cs”, “da”, “nl”, “nl-BE”, “en”, “en-US”, “en-EG”, “en-AU”, “en-GB”, “en-CA”, “en-NZ”, “en-IE”, “en-ZA”, “en-JM”, “en-BZ”, “en-TT”, “et”, “fo”, “fa”, “fi”, “fr”, “fr-BE”, “fr-CA”, “fr-CH”, “fr-LU”, “gd”, “gd-IE”, “de”, “de-CH”, “de-AT”, “de-LU”, “de-LI”, “el”, “he”, “hi”, “hu”, “is”, “id”, “it”, “it-CH”, “ja”, “ko”, “lv”, “lt”, “mk”, “mt”, “no”, “pl”, “pt-BR”, “pt”, “rm”, “ro”, “ro-MO”, “ru”, “ru-MI”, “sz”, “sr”, “sk”, “sl”, “sb”, “es”, “es-AR”, “es-GT”, “es-CR”, “es-PA”, “es-DO”, “es-MX”, “es-VE”, “es-CO”, “es-PE”, “es-EC”, “es-CL”, “es-UY”, “es-PY”, “es-BO”, “es-SV”, “es-HN”, “es-NI”, “es-PR”, “sx”, “sv”, “sv-FI”, “th”, “ts”, “tn”, “tr”, “uk”, “ur”, “ve”, “vi”, “xh”,  “ji”, “zu”] ( https://stackoverflow.com/questions/5580876/navigator-language-list-of-all-languages ).

Com certeza é bem menos que as 676 possibilidades =D

Os próximos 4 bytes também são valores booleanos e eu também resolvi fazer força bruta neles porque considerei que o Google quisesse tornar mais difícil.

Então criei um script em javascript que iria modificar esses valores e tentar descriptografar com cada chave gerada na força bruta. Eu imaginei que teria alguma inclusão de mais um javascript por isso defini que quando o resultado da descriptografia tivesse uma substring “.js”, me mostrasse a chave certa e o texto descriptografado.

Toda a função de descriptografia e a variável “a” com a string em base64 eu copiei e colei do javascript “uHsdvEHFDwljZFhPyKxp.js”.

Meu código final ficou assim:

<script>
var T = {};
T.e0 = function(a, b) {
    var c, d, e;
    return a = String(a), b = String(b), 0 == a.length ? '' : (c = T.f0(a.u0()), d = T.f0(b.u0().slice(0, 16)), c.length, c = T.e1(c, d), e = T.longsToStr(c), e.b0())
}, T.d0 = function(a, b) {
    var c, d;
    return a = String(a), b = String(b), 0 == a.length ? '' : (c = T.f0(a.b1()), d = T.f0(b.u0().slice(0, 16)), c.length, c = T.d1(c, d), a = T.longsToStr(c), a = a.replace(/\0+$/, ''), a.u1())
}, T.e1 = function(a, b) {
    var c, d, e, f, g, h, i, j, k;
    for (a.length < 2 && (a[1] = 0), c = a.length, d = a[c - 1], e = a[0], f = 2654435769, i = Math.floor(6 + 52 / c), j = 0; i-- > 0;)
        for (j += f, h = 3 & j >>> 2, k = 0; c > k; k++) e = a[(k + 1) % c], g = (d >>> 5 ^ e << 2) + (e >>> 3 ^ d << 4) ^ (j ^ e) + (b[3 & k ^ h] ^ d), d = a[k] += g;
    return a
}, T.d1 = function(a, b) {
    for (var c, d, e, f = a.length, g = a[f - 1], h = a[0], i = 2654435769, j = Math.floor(6 + 52 / f), k = j * i; 0 != k;) {
        for (d = 3 & k >>> 2, e = f - 1; e >= 0; e--) g = a[e > 0 ? e - 1 : f - 1], c = (g >>> 5 ^ h << 2) + (h >>> 3 ^ g << 4) ^ (k ^ h) + (b[3 & e ^ d] ^ g), h = a[e] -= c;
        k -= i
    }
    return a
}, T.f0 = function(a) {
    var b, c = new Array(Math.ceil(a.length / 4));
    for (b = 0; b < c.length; b++) c[b] = a.charCodeAt(4 * b) + (a.charCodeAt(4 * b + 1) << 8) + (a.charCodeAt(4 * b + 2) << 16) + (a.charCodeAt(4 * b + 3) << 24);
    return c
}, T.longsToStr = function(a) {
    var b, c = new Array(a.length);
    for (b = 0; b < a.length; b++) c[b] = String.fromCharCode(255 & a[b], 255 & a[b] >>> 8, 255 & a[b] >>> 16, 255 & a[b] >>> 24);
    return c.join('')
}, 'undefined' == typeof String.prototype.u0 && (String.prototype.u0 = function() {
    return unescape(encodeURIComponent(this))
}), 'undefined' == typeof String.prototype.u1 && (String.prototype.u1 = function() {
    try {
        return decodeURIComponent(escape(this))
    } catch (a) {
        return this
    }
}), 'undefined' == typeof String.prototype.b0 && (String.prototype.b0 = function() {
    if ('undefined' != typeof btoa) return btoa(this);
    if ('undefined' != typeof Buffer) return new Buffer(this, 'utf8').toString('base64');
    throw new Error('err')
}), 'undefined' == typeof String.prototype.b1 && (String.prototype.b1 = function() {
    if ('undefined' != typeof atob) return atob(this);
    if ('undefined' != typeof Buffer) return new Buffer(this, 'base64').toString('utf8');
    throw new Error('err')
}), 'undefined' != typeof module && module.exports && (module.exports = T), 'function' == typeof define && define.amd && define([''], function() {
    return T
});
a = "A2xcVTrDuF+EqdD8VibVZIWY2k334hwWPsIzgPgmHSapj+zeDlPqH/RHlpVCitdlxQQfzOjO01xCW/6TNqkciPRbOZsizdYNf5eEOgghG0YhmIplCBLhGdxmnvsIT/69I08I/ZvIxkWyufhLayTDzFeGZlPQfjqtY8Wr59Lkw/JggztpJYPWng=="
var str = "LINUX"
var final;
var resp;
var i1 = 1;
var i2;
var i3;
var i4;
var i5;
var i6;
var i7;
var i8;
var i9;
var ilg;
var lg = ["af", "sq", "ar-SA", "ar-IQ", "ar-EG", "ar-LY", "ar-DZ", "ar-MA", "ar-TN", "ar-OM",
 "ar-YE", "ar-SY", "ar-JO", "ar-LB", "ar-KW", "ar-AE", "ar-BH", "ar-QA", "eu", "bg",
 "be", "ca", "zh-TW", "zh-CN", "zh-HK", "zh-SG", "hr", "cs", "da", "nl", "nl-BE", "en",
 "en-US", "en-EG", "en-AU", "en-GB", "en-CA", "en-NZ", "en-IE", "en-ZA", "en-JM",
 "en-BZ", "en-TT", "et", "fo", "fa", "fi", "fr", "fr-BE", "fr-CA", "fr-CH", "fr-LU",
 "gd", "gd-IE", "de", "de-CH", "de-AT", "de-LU", "de-LI", "el", "he", "hi", "hu", 
 "is", "id", "it", "it-CH", "ja", "ko", "lv", "lt", "mk", "mt", "no", "pl",
 "pt-BR", "pt", "rm", "ro", "ro-MO", "ru", "ru-MI", "sz", "sr", "sk", "sl", "sb",
 "es", "es-AR", "es-GT", "es-CR", "es-PA", "es-DO", "es-MX", "es-VE", "es-CO", 
 "es-PE", "es-EC", "es-CL", "es-UY", "es-PY", "es-BO", "es-SV", "es-HN", "es-NI", 
 "es-PR", "sx", "sv", "sv-FI", "th", "ts", "tn", "tr", "uk", "ur", "ve", "vi", "xh",
 "ji", "zu"];
for (i2 = 0; i2<=1; i2++){
    for (i3 = 0; i3<=1; i3++){
        for (i4 = 0; i4<=1; i4++){
            for (i5 = 0; i5<=1; i5++){
                for (ilg = 0; ilg<lg.length; ilg++){
                    for (i6 = 0; i6<=1; i6++){
                        for (i7 = 0; i7<=1; i7++){
                            for (i8 = 0; i8<=1; i8++){
                                for (i9 = 0; i9<=1; i9++){
                                    //console.log(str + i1 + i2 + i3 + i4 + i5 + lg[ilg].toUpperCase().substr(0, 2) + i6 + i7 + i8 + i9);
                                    final = str + i1 + i2 + i3 + i4 + i5 + lg[ilg].toUpperCase().substr(0, 2) + i6 + i7 + i8 + i9;
                                    resp = T.d0(a, final);
                                    if (resp.includes(".js")){
                                        console.log(str + i1 + i2 + i3 + i4 + i5 + lg[ilg].toUpperCase().substr(0, 2) + i6 + i7 + i8 + i9);
                                        console.log(resp);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
</script>

Salvei isso tudo na minha Área de Trabalho com o nome go.html. Ao abrir o arquivo no Chrome e visualizar os logs do “console”, aparece isso:

String descriptografada

Uhuuuuu! Deu certo! Como imaginei, a chave correta (LINUX10000FR1000) descriptografaria a variável “a”. Essa string seria executada como código javascript pela função “eval” e seria adicionado mais um código javascript: ./src/npoTHyBXnpZWgLorNrYc.js

Terceira etapa

Mais um código minificado, obfuscado e com diversas strings estranhas… Ok. “Desminifiquei” o código inteiro, colei no Notepad++ novamente e comprimi todas as declarações de funções para ver o fluxo original.

Ok. Salvei tudo na minha máquina como “3.js”, criei um arquivo html que chame o “3.js” e salvei como “3.html” e abri o arquivo “3.html” no meu navegador.

Não apareceu nada…

Abri as “Ferrametas de Desenvolvedor” do navegador, recarreguei a página e a execução foi finalizada escrito “debugger”.

Anti-debugging em Javascript

O código está protegido contra debugging. Ou seja, o código impede que eu analise o código. Cliquei no link ali abaixo “3.js:1” para ver onde no código está este anti-debugger. Cheguei em um código hexadecimal. Procurei aquele código no meu código javascript e a string foi encontrada na linha 434.

Linha onde está o código anti-debugger

No console do navegador coloquei o valor hexadecimal entre aspas simples no console e apareceu “constructor”. Digitei a próxima string em hexa entre aspas simples no console e apareceu “debu”. Digitei no console _0x5877(‘0x6d’, ‘\x5a\x45\x5b\x79’) e apareceu “gger”.

Ummmmm! A função _0x5877 é a função que descriptografa as strings! Legal! =D

A partir daqui tirei todo este código de anti-debugging. Este código estava dentro de um “else”. Tirei todo o else. Portanto apaguei das linhas 420 até 447. Rodei meu código novamente e quando eu estava com o console aberto meu navegador travava porque alguma parte do código consuma bastante do meu processador. Com certeza era mais algum código anti-debugging. Mas não importa porque agora sei a função que descriptografa as strings!

Então fechei meu navegador, abrir meu arquivo de novo sem o console aberto. Três ou quatro segundos depois terminava o processamento. Então abri o console (após terminar de carregar a página, pode abrir sem problemas) e digitei _0x5877(‘0x0’, ‘\x66\x59\x56\x6f’). Apareceu que “_0x5877 is not a function”. Talvez eu tenha feito alguma besteira…

Neste ponto eu peguei o código original obfuscado e minificado, copiei e colei no meu arquivo local “3.js”. Recarreguei a página 3.html local com o console fechado, esperei carregar a página, abri o console e fui digitando todas as chamadas para _0x5877. Agora as strings iam aparecendo em texto claro! Deu certo!

Quando eu cheguei na função _0x5877(‘0x18’, ‘\x4c\x5d\x34\x37’), apareceu mais um javascript: “./src/WFmJWvYBQmZnedwpdQBU.js”

Strings descriptografadas

Final

Então eu copiei e colei este novo arquivo na URL do desafio ( https://malvertising.web.ctfcompetition.com/ads/src/WFmJWvYBQmZnedwpdQBU.js ) e finalmente apareceu a flag

Flag final

O que achou? Difícil? Me diz aí!

2 Comments

Leave a Comment

O seu endereço de e-mail não será publicado.