Ayer me descargué una plantilla de Blogger llamada "Sora". Tras horas buscando algo decente, encontré por fin una plantilla chula para este blog 🤩
Empecé a editarla y, al probarla, me encontré con algo curioso: el blog redirigía a la página
www.soratemplates.com al quitar el copyright del footer...
A partir de aquí, me puse a investigar y a aprender. Y aprendí mucho sobre técnicas de ofuscación de código, código que al final pude leer 😄 y modficar 😁
Esta es la historia completa de esta hazaña.
El código
Después de instalar la plantilla, modificarla y subirla a Blogger, vi que al visitar el blog este te rerigiía 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 el código fuente en busca de una etiqueta anchor con el ID
mycontent. En esta etiqueta había un enlace apuntando al sitio del autor de la plantilla. Si modificabas el footer, ocultabas ese enlace o reemplazabas el texto de copyright, el setInterval se disparaba y redirigía a cada visitante a la web del autor.
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 ese enlace en la plantilla y listo. Pero nada es tan fácil en esta vida.
Ofuscación
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
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; una persona se pierde 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.
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 la creación de webs. O al menos, para 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 este caso, el packer usó base 62 con 119 palabras en el diccionario. El payload parecía ruido -
"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.
Pero 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). Traduce esos valores, 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 en texto plano.
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 y ofuscación 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 un payload calcado a la versión legible, parece una broma 😂
Conclusión
Las plantillas gratuitas no siempre son gratuitas. El precio a pagar es a veces la atención de tus visitantes, tu link equity de SEO (los enlaces de atribución dofollow que tanto empeño puso la plantilla en mantener), o la reputación de tu blog si los visitantes se encuentran con redirecciones inesperadas.
Lo que más me llamó la atención no fue encontrar codificación hexadecimal, el empaquetador de Dean Edwards o la ofuscación mediante arrays
_0x (recursos empleados en extensiones maliciosas, scripts publicitarios o software pirateado) sino el esfuerzo invertido en proteger un simple enlace.
Aun así, por elaborada que parezca la protección, sigue siendo solo una barrera técnica más, y como cualquier otra, puede analizarse y desmontarse paso a paso y con paciencia. Dicho esto, gracias Sora Templates por el desafío ✌️
No hay comentarios:
Publicar un comentario