Engenharia Reversa de Ofuscação JavaScript: Descriptografia de Dados com DES

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 _0x51eedc atua 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.

Tags: javascript obfuscation CryptoJS DES reverse-engineering

Publicado em 6-28 16:37