Ayer descargué una plantilla de Blogger llamada "Sora". Un diseño limpio, moderno, parecía perfecta. Empecé a editarla y me encontré con algo curioso: el blog redirigía a
www.soratemplates.com al quitar el copyright del footer.
Lo que siguió fue un viaje fascinante que me enseñó mucho sobre JavaScript y las distintas técnicas de ofuscación que usan quienes no quieren que leas su código, código que al final acabé pudiendo leer 😄 y modficar 😁
Esta es la historia completa.
El código
Después de instalar la plantilla, modificarla y subirla a Blogger, cualquier visitante que llegaba al blog era redirigido a
www.soratemplates.com. El culpable no fue difícil de encontrar: al final de la plantilla, a plena vista, había el siguiente código:
$('a#mycontent').each(function() {
var $t = $(this);
$t.attr('href', 'https://www.soratemplates.com/')
.attr('rel', 'dofollow')
.attr('title', 'Theme')
.attr('style', 'display: inline-block!important; visibility: visible!important; z-index:99!important; opacity: 1!important;')
.text('SoraTemplates');
});
setInterval(function() {
if (!$('a#mycontent').length) window.location.href = 'https://www.soratemplates.com/';
if (!$('a#mycontent:visible').length) window.location.href = 'https://www.soratemplates.com/';
}, 1000);
Este código, que se ejecutaba cada segundo, hacía lo siguiente:
Verificar la presencia de una etiqueta anchor con el ID
mycontent — que era el enlace de atribución oculto en el footer, estilizado para ser invisible y apuntando al sitio del autor de la plantilla. Si modificabas el footer, ocultabas ese enlace o reemplazabas el texto de copyright (como hice yo), el setInterval se disparaba en un segundo y redirigía a cada visitante fuera de tu blog.
La lógica es interesante: mantén el enlace de atribución o pierde a tu audiencia.
Parecía algo fácil de solucionar: eliminar el código que comprobaba que hubiera un enlace en la plantilla. Pero nada es tan fácil en esta vida.
Analizando la plantilla
Después de eliminar el código de redirección visible, el blog seguía redirigiendo visitantes. Tenía que haber otra capa. Un respaldo. Algo que el autor de la plantilla asumió que nadie encontraría o se molestaría en decodificar.
Una búsqueda más profunda reveló esto, metido dentro de un bloque de script:
var _cIHQLb= "\x65\x76\x61\x6c\x28\x66\x75\x6e\x63\x74\x69\x6f\x6e\x28\x70\x2c\x61\x2c\x63\x2c\x6b\x2c\x65\x2c\x64\x29\x7b..."
Una cadena de 35.000 caracteres hexadecimales, seguida de:
eval(_cIHQLb);
Aquí es donde la cosa se puso interesante.
Capa 1: Codificación hexadecimal
La variable
_cIHQLb contenía una cadena larga donde cada carácter había sido reemplazado por su código ASCII hexadecimal. La secuencia \x65\x76\x61\x6c se decodifica como los cuatro caracteres e, v, a, l. Juntos: "eval".
Esta es la técnica de ofuscación más simple del repertorio. No cifra nada, solo hace que el código sea ilegible para un humano que lo ojea rápidamente. Una máquina lo lee instantáneamente; los ojos de una persona se pierden en él.
Para decodificar el código, hay que reemplazar cada secuencia
\xNN con el carácter cuyo código ASCII es NN en hexadecimal:
\x65 = e \x76 = v \x61 = a \x6c = l
\x28 = ( \x66 = f \x75 = u \x6e = n
\x63 = c \x74 = t \x69 = i \x6f = o
Después de decodificar los 35.000 caracteres, obtienes:
eval(function(p,a,c,k,e,d){ ... }('1J Z=[...', 62, 119, 'word1|word2|...'.split('|'), 0, {}))
Primera capa superada. Debajo hay algo más sofisticado.
Capa 2: El Dean Edwards Packer
El patrón
eval(function(p,a,c,k,e,d){...}) es inmediatamente reconocible para cualquiera que haya pasado cierto tiempo en seguridad web o en optimización JavaScript de la vieja escuela. Es el Dean Edwards p,a,c,k,e,r — una herramienta de compresión y ofuscación de JavaScript que existe desde 2004.
El packer toma tu JavaScript original y construye un diccionario de cada identificador único — nombres de variables, nombres de funciones, palabras clave, cualquier palabra que aparezca más de una vez. Asigna a cada palabra un número y reemplaza cada ocurrencia en el código fuente con ese número codificado en base 62 (dígitos 0–9, luego a–z, luego A–Z).
El wrapper
function(p,a,c,k,e,d) es el decodificador.
En tiempo de ejecución:
- Toma la cadena de payload codificada (
p) y el diccionario de palabras (k) - Reconstruye el código original reemplazando cada token codificado con su palabra correspondiente del diccionario
- Pasa el código reconstruido directamente a
eval()
En nuestro caso, el packer usó base 62 con 119 palabras en el diccionario. El payload parecía ruido comprimido —
"1J Z=[..." — pero una vez decodificado con el diccionario, reveló una tercera capa.
Ahora bien ¿por qué usar el packer?
Porque derrota la inspección casual. La codificación hex hace la capa externa ilegible; el packer hace que la capa interna requiera trabajo real para decodificar. La mayoría de personas se rendirían antes de llegar a lo que hay dentro.
Y aquí no estamos para rendirnos.
Capa 3: El array de strings _0x
Tras el desempaquetado, el resultado era otro código JavaScript aún no legible:
var _0x47b9=["\x24\x28\x27\x61\x23\x45\x27\x29\x2E\x5A\x28\x64\x28\x29\x7B\x69\x20\x61\x3D..."]
La convención de nombres
_0x es la firma de una herramienta de ofuscación completamente diferente, comúnmente asociada con obfuscator.io y servicios similares. Esta técnica almacena todos los literales de string en un único array al inicio del script y reemplaza cada string en el código con un acceso al array como _0x47b9[0], _0x47b9[1], etc.
Los valores del array están codificados en hex otra vez — otra pasada de la codificación
\xNN de la capa uno. Elimina esos, y los strings se convierten en fragmentos de código JavaScript legibles.
Son tres técnicas de ofuscación independientes apiladas una sobre otra:
Cada capa tiene un modo de fallo diferente para las herramientas de análisis:
- La codificación hex rompe la búsqueda de texto simple.
- El packer rompe el pattern matching de identificadores.
- El array de strings rompe el análisis estático de literales de string.
Juntos hacen que la desofuscación automatizada falle en múltiples puntos.
Cómo lo descifré
La estrategia de ataque fue simple: no intentes leer el código — haz que el código se lea a sí mismo, luego intercepta el resultado antes de que se ejecute.
Paso 1: Decodificar hex de la capa uno con Python.
import re
hex_str = match.group(1)
decoded = bytes(hex_str, 'utf-8').decode('unicode_escape')
Esto convierte la cadena:
\xNN
en el código packer:
eval(function(p,a,c,k,e,d){...}).
Paso 2: Implementar el desempaquetado de Dean Edwards en Python.
def decode_num(n, base):
chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
result = ''
while n > 0:
result = chars[n % base] + result
n //= base
return result or '0'
lookup = {}
for i in range(count):
key = decode_num(i, base)
lookup[key] = words[i] if words[i] else key
result = re.sub(r'\b\w+\b',
lambda m: lookup.get(m.group(0), m.group(0)), payload)
Extraer el payload, base, count y diccionario de palabras de la llamada packer, reconstruir la tabla de búsqueda y reemplazar cada token en el payload. Sale el código con el array
_0x47b9.
Paso 3: Para la tercera capa, usé Node.js para interceptar la cadena de
eval.
global.eval = function(code) {
fs.writeFileSync('/tmp/final_decoded.js', code);
return null;
};
El código empaquetado quiere llamar a
eval() con el JavaScript final decodificado. Al reemplazar eval con una función que escribe el código a disco en lugar de ejecutarlo, dejé que el ofuscador se decodificara a sí mismo — y luego leí la salida de forma segura.
Paso 4: Decodificar los strings hex restantes en el array
_0x47b9.
decodedCode = decodedCode.replace(
/"((?:\\x[0-9a-fA-F]{2}|[^"\\]|\\[^x])+)"/g,
(match, inner) => {
const decoded = inner.replace(/\\x([0-9a-fA-F]{2})/g,
(_, h) => String.fromCharCode(parseInt(h, 16)));
return '"' + decoded + '"';
});
Después de los cuatro pasos, el código final ya era completamente legible.
Lo que el código realmente hacía
Después de 35.000 caracteres de ofuscación, tres capas de codificación y dos algoritmos de ofuscación distintos, el payload real era esto:
$('a#mycontent').each(function() {
var a = $(this);
a.attr('href', 'https://www.soratemplates.com/')
.attr('rel', 'dofollow')
.attr('style', 'display: inline-block!important; visibility: visible!important;')
.text('SoraTemplates');
});
setInterval(function() {
if (!$('a#mycontent').length)
document.location.href = 'https://www.soratemplates.com/';
if (!$('a#mycontent:visible').length)
document.location.href = 'https://www.soratemplates.com/';
}, 1000);
Palabra por palabra: exactamente el mismo código que la versión visible.
Comparativa:
Treinta y cinco mil caracteres de ofuscación y multicapa. Tres algoritmos de codificación. Una técnica lo suficientemente compleja como para derrotar la mayoría de herramientas de análisis automatizado. Y el payload era una copia de respaldo de una redirección que asumieron que encontrarías y eliminarías.
El código visible era un señuelo. El código ofuscado era el verdadero hueso.
Conclusión
Las plantillas gratuitas no son gratuitas. El precio a veces son tus visitantes, tu link equity de SEO (esos eran enlaces de atribución dofollow), o la reputación de tu blog si los visitantes se encuentran con redirecciones inesperadas.
Las técnicas usadas aquí — codificación hexadecimal, el Dean Edwards packer y la ofuscación por array de strings
_0x — son comunes; las encontrarás en extensiones de navegador maliciosas, en scripts de inyección de publicidad, en software pirata. El hecho de que aparezcan en una plantilla de Blogger no sorprende a nadie, pero lo que sí sorprende es el nivel de esfuerzo: tres capas de ofuscación para un script de redirección que corre en un tema de Blogger.
No hay comentarios:
Publicar un comentario