Compreendendo a Ofuscação de Código JavaScript
A ofuscação de código é uma técnica amplamente utilizada para proteger a propriedade intelectual de scripts executados no lado do cliente. Dois métodos comuns são a ofuscação estrutuarl e a ofuscação de identificadores.
A ofuscação estrutural consiste em inserir blocos de código morto, funções auxiliares desnecessárias e fluxos de controle alternativos para elevar a complexidade de leitura. Já a ofuscação de identificadores renomeia variáveis, funções e propriedades para sequências sem significado semântico, dificultando a análise estática.
Características Típicas do Obfuscator
- Transformação de acesso por ponto (
obj.prop) em acesso por colchetes (obj["prop"]) - Criptografia centralizada de strings literais, com funções de descriptografia embutidas
- Embaralhamento de arrays de strings com algoritmos de restauração acoplados
- Inserção de fluxos de controle baseados em máquinas de estados (switch-case com índices embaralhados)
Caso Prático: Descriptografia de API de Dados
Foi necessário extrair informações de um endpoint que retorna dados cifrados. Ao inspecionar as requisições XHR/Fetch, constatou-se que o corpo da resposta não contém JSON legível, mas sim uma string hexadecimal cifrada.
Após depuração no navegador, identificou-se que a descriptografia ocorre via uma chamada semelhante a:
JSON.parse(JSON.parse(webInstace.shell(respostaCifrada))
Ao colocar um breakpoint dentro da função shell, foi possível extrair o código relevante:
var _0x4da59e = {
'bUIIa': function _0x2a2af9(_0x779387, _0x4a4fec) {
return _0x779387 + _0x4a4fec;
}
};
var _0x9843d3 = function(_0x29d556, _0xcc6df, _0x3d7020) {
if (0x0 == _0xcc6df)
return _0x29d556[_0x2246('0x254', '4VZ$')](_0x3d7020);
var _0x48914b;
_0x48914b = '' + _0x29d556[_0x2246('0x255', 'GL3Q')](0x0, _0xcc6df);
return _0x48914b += _0x29d556['substr'](_0x4da59e[_0x2246('0x256', 'DK[&')](_0xcc6df, _0x3d7020));
};
this[_0x2246('0x257', 'nArV')] = function(_0xa0c834) {
var _0x51eedc = {
'pKENi': function _0x2f627(_0x5b6f5a, _0x440924) {
return _0x5b6f5a === _0x440924;
},
'wnfPa': 'ZGz',
'VMmle': '7|1|8|9|5|2|3|6|0|4',
'GKWFf': function _0x1a4e13(_0x40cfde, _0x16f3c2) {
return _0x40cfde == _0x16f3c2;
},
'MUPgQ': function _0x342f0d(_0x19038b, _0x4004d6) {
return _0x19038b >= _0x4004d6;
},
'hLXma': function _0x55adaf(_0x45a871, _0x161bdf) {
return _0x45a871 + _0x161bdf;
},
'JdOlO': function _0x13e00a(_0x5899a9, _0x4bb34d) {
return _0x5899a9 + _0x4bb34d;
},
'qrTpg': function _0x1198fb(_0x55b317, _0x22e1db, _0x1b091a) {
return _0x55b317(_0x22e1db, _0x1b091a);
},
'pdmMk': function _0xe2b022(_0x4af286, _0x4c2fd4) {
return _0x4af286 - _0x4c2fd4;
},
'xVKWW': function _0x1094a3(_0x5f3627, _0x2a0ac5, _0x3ad2e5) {
return _0x5f3627(_0x2a0ac5, _0x3ad2e5);
}
};
if (_0x51eedc[_0x2246('0x258', '@1Ws')](_0x2246('0x259', 'E&PI'), _0x51eedc['wnfPa'])) {
this['_append'](a);
return this[_0x2246('0x25a', 'GL3Q')]();
} else {
var _0x492a62 = _0x51eedc[_0x2246('0x25b', '&59Q')][_0x2246('0x25c', ')q#9')]('|')
, _0x356b01 = 0x0;
while (!![]) {
switch (_0x492a62[_0x356b01++]) {
case '0':
_0x554c90 = _grsa_JS[_0x2246('0x25d', 'E&PI')]['decrypt']({
'ciphertext': _grsa_JS['enc'][_0x2246('0x25e', 'sy^o')]['parse'](_0xa0c834)
}, _0x2cf8ae, {
'iv': _0x554c90,
'mode': _grsa_JS[_0x2246('0x16c', 'O^50')][_0x2246('0x25f', 'Who^')],
'padding': _grsa_JS[_0x2246('0x260', '7IfV')][_0x2246('0x261', 'E&PI')]
})[_0x2246('0x1c', 'yY#5')](_grsa_JS['enc'][_0x2246('0x262', ']2BX')]);
continue;
case '1':
if (_0x51eedc[_0x2246('0x263', 'Jsmq')](null, _0xa0c834) || _0x51eedc[_0x2246('0x264', '!2eC')](0x10, _0xa0c834['length']))
return _0xa0c834;
continue;
case '2':
_0xa0c834 = _0x9843d3(_0xa0c834, _0x2cf8ae, 0x8);
continue;
case '3':
_0x2cf8ae = _grsa_JS[_0x2246('0x265', 'RQ2o')][_0x2246('0x266', '3j7z')][_0x2246('0x267', 'RQ2o')](_0x554c90);
continue;
case '4':
return _0x554c90[_0x2246('0x268', 'cs*4')](0x0, _0x51eedc[_0x2246('0x269', 'MVsm')](_0x554c90[_0x2246('0x26a', '0J6f')]('}'), 0x1));
case '5':
_0x554c90 = _0xa0c834[_0x2246('0x26b', 'UwHa')](_0x2cf8ae, 0x8);
continue;
case '6':
_0x554c90 = _grsa_JS[_0x2246('0x26c', '4VZ$')]['Utf8']['parse'](_0x554c90);
continue;
case '7':
if (!navigator || !navigator[_0x2246('0x26d', '0I#o')])
return '';
continue;
case '8':
var _0x554c90 = _0x51eedc[_0x2246('0x26e', 'Yb4P')](_0x51eedc[_0x2246('0x26f', 'BQ5p')](parseInt, _0xa0c834[_0x51eedc[_0x2246('0x270', 'Z2VK')](_0xa0c834['length'], 0x1)], 0x10), 0x9)
, _0x2cf8ae = _0x51eedc[_0x2246('0x271', 'yY#5')](parseInt, _0xa0c834[_0x554c90], 0x10);
continue;
case '9':
_0xa0c834 = _0x9843d3(_0xa0c834, _0x554c90, 0x1);
continue;
}
break;
}
}
}
Processo de Desofuscação
Deconstructing the obfuscated code step by step:
- Desmontagem de funções-proxy: O objeto
_0x51eedcatua como um dicionário de wrappers que apenas encapsulam operações aritméticas e comparações simples. Cada propriedade pode ser substituída pela operação correspondente. - Resolução de strings: Todas as chamadas
_0x2246()devem ser substituídas pelos valores de string retornados, revelando os nomes reais de métodos e propriedades. - Linearização do fluxo de controle: A string
'7|1|8|9|5|2|3|6|0|4'define a ordem de execução dos cases. Reordenando conforme essa sequência, obtém-se o fluxo linear real.
Após todas as substituições, o algoritmo desofuscado fica assim:
var CryptoJS = require('crypto-js');
function removerTrecho(str, pos, tam) {
if (pos === 0) return str.substring(tam);
var prefixo = str.substring(0, pos);
return prefixo + str.substring(pos + tam);
}
function descriptografar(cifrado) {
var posUlt = parseInt(cifrado[cifrado.length - 1], 16) + 9;
var posChave = parseInt(cifrado[posUlt], 16);
cifrado = removerTrecho(cifrado, posUlt, 1);
var chave = cifrado.substring(posChave, posChave + 8);
cifrado = removerTrecho(cifrado, posChave, 8);
var chaveBytes = CryptoJS.enc.Utf8.parse(chave);
var ivBytes = CryptoJS.enc.Utf8.parse(chave);
var resultado = CryptoJS.DES.decrypt(
{ ciphertext: CryptoJS.enc.Hex.parse(cifrado) },
chaveBytes,
{ iv: ivBytes, mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
).toString(CryptoJS.enc.Utf8);
return resultado.substring(0, resultado.lastIndexOf('}') + 1);
}
function processarResposta(dados) {
return JSON.parse(descriptografar(dados));
}
Implementação em Python
Utilizando PyExecJS para executar o código JavaScript extraído:
import subprocess
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding='utf-8')
import execjs
import requests
sessao = requests.Session()
sessao.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
with open("decrypt_module.js", "r", encoding="utf-8") as fh:
motor_js = execjs.compile(fh.read())
def consultar_filmes(ano):
endpoint = "https://www.exemplo.com.br/API/GetData.ashx"
payload = {"year": ano, "MethodName": "BoxOffice_GetYearInfoData"}
resp = sessao.post(endpoint, data=payload)
return motor_js.call("processarResposta", resp.text)
if __name__ == '__main__':
for ano in range(2008, 2024):
resultado = consultar_filmes(ano)
print(ano, "-", resultado)
Alternativamente, a mesma lógica pode ser implementada inteiramente em Python nativo:
from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad
import binascii
import json
import requests
def recortar(texto, indice, extensao):
"""Remove uma porção da string na posição especificada."""
if indice == 0:
return texto[extensao:]
return texto[:indice] + texto[indice + extensao:]
def decifrar(carga):
if carga is None or len(carga) <= 16:
return carga
pos_final = int(carga[-1], 16) + 9
pos_chave = int(carga[pos_final], 16)
carga = recortar(carga, pos_final, 1)
chave = carga[pos_chave:pos_chave + 8]
carga = recortar(carga, pos_chave, 8)
cifra = DES.new(key=chave.encode('utf-8'), mode=DES.MODE_ECB)
bytes_decifrados = cifra.decrypt(binascii.unhexlify(carga))
bytes_decifrados = unpad(bytes_decifrados, 8)
texto = bytes_decifrados.decode('utf-8')
return texto[:texto.rindex('}') + 1]
def converter_json(dados):
return json.loads(decifrar(dados))
def obter_dados(ano, sessao):
for tentativa in range(10):
try:
endpoint = "https://www.exemplo.com.br/API/GetData.ashx"
payload = {"year": ano, "MethodName": "BoxOffice_GetYearInfoData"}
resp = sessao.post(endpoint, data=payload)
return converter_json(resp.text)
except Exception:
print(f"Tentativa {tentativa + 1} falhou para ano {ano}")
if __name__ == '__main__':
sessao = requests.Session()
sessao.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
for ano in range(2008, 2024):
dados = obter_dados(ano, sessao)
print(ano, "-", dados)
Atenção à Diferença Entre Hex em Python e JavaScript
Strings hexadecimais podem ter comportamentos diferentes entre as duas linguagens. Por exemplo, a string 0123df pode ser interpretada como 123df em Python, removendo o zero à esquerda. Por isso, é fundamental envolver o processo de descriptografia em blocos try/except com retentativas para garantir a robustez da extração.