#Nitto: Evadiendo la detección de firmas mediante ofuscación (y un toque de emails)

| 12 MIN DE LECTURA
Muy pronto...

Llevo más de un mes dedicando casi todas mis tardes al curso de desarrollo de malware de Maldev Academy, y me está flipando. Es verdad que los primeros 20-30 módulos son un poco “secos”, en el sentido de que son casi todo teoría con poca chicha “práctica”. Pero eso no quita que no puedas ir montando tus cosillas por tu cuenta; hasta de los proyectos más simples se puede aprender una barbaridad.

Y eso es exactamente lo que os quiero presentar hoy: una herramienta simple pero funcional para evadir la detección de AV basada en firmas ENLACE EXTERNO A:

https://www.sentinelone.com/blog/what-is-a-malware-file-signature-and-how-does-it-work/
en Windows usando ofuscación y algo de cifrado.

He creado esta herramienta por dos motivos principalmente: curiosidad y mejorar mis habilidades en desarrollo de malware (y sí, la he creado yo con estas manos tan preciosas que tengo). Con esto quiero decir que es normal (e incluso de esperar) que encuentres cositas que se puedan mejorar o refactorizar. Si lo haces, háblame al LinkedIn ENLACE EXTERNO A:

https://www.linkedin.com/in/carlosbrnl/
porfa, me interesa bastante escuchar lo que tienes que decir. Cualquier cosa que pueda mejorar o aprender de ti es más que bienvenida :).

Esta herramienta también implementa una nueva técnica de ofuscaciónNo he visto ninguna técnica similar todavía, de ahí lo de nueva. que he creado yo mismo: ofuscación basada en emails, o EmailFuscation para los amigos, que convierte chunks de 6 bytes en correos electrónicos con el formato: [nombre].[inicial].[apellido][numero]@[dominio].[tld].

Si le echas un ojo al código, verás que es bastante simple, y ahí está la gracia. No es necesario complicar las cosas así porque sí. Especialmente en un mundo donde el desarrollo con agentes puede crear algo bastante decente desde cero (si le das suficiente contexto, claro). De hecho, probablemente podría haber creado esto mismo en una fracción de lo que he tardado si hubiese utilizado alguna de estas herramientas… Pero lo dicho, el objetivo era aprender algo de verdad, darme de cabezazos contra la pared y pelearme con los maravillosos punteros de este nuestro C, y eso es justo lo que hecho.

¿Qué es la ofuscación?

La ofuscación consiste en “disfrazar” el código malicioso como si este fuera benigno con el objetivo de ocultar su verdadera naturaleza, pero manteniendo intacta su funcionalidad. Esto hace que el malware sea más difícil de analizar para los humanos y más complicado de detectar por las diferentes herramientas de seguridad.

La técnica IPFuscation ENLACE EXTERNO A:

https://www.sentinelone.com/blog/hive-ransomware-deploys-novel-ipfuscation-technique/
y sus variantes (MAC y UUID) no son nuevas; existen desde principios del 2021. Estas técnicas convierten los bytes de la shellcode en strings con formato IPv4, IPv6, MAC o UUID.

Dentro del binario, lo que a primera vista parece un simple array de cadenas de IPs:

IPs array dentro del binario

…es en realidad una shellcode oculta. Por ejemplo, la primera IP “252.72.131.228” equivale a 0xFC4883E4 (en little endian), y la segunda “240.232.192.0” se corresponde con 0xF0E8C000. Al combinarlas, el resultado es el siguiente:

Bytes deofuscados en la memoria heap

Que corresponde con la bootstrap header estándar de un shellcode para Windows x64, típico de herramientas como msfvenom. Su función es alinear el stack y dejar la CPU lista para la siguientes instrucciones:

Instrucciones desensambladas

Para entender cómo funciona la deofuscación vas a tener que aguantar un pelín. Esto lo explico en detalle en una sección más abajo .

Entra Nitto en Escena

Nitto (abreviatura de incógnito) es mi criatura: una herramienta simple para ofuscar o cifrar payloads, y luego revertir las transformaciones. Puedes encontrar el código en este repo ENLACE EXTERNO A:

https://github.com/m1urah/nitto
. Me he currado un logo y todo!

Logo de Nitto

Nitto implementa varias técnicas de ofuscación y cifrado, junto con sus inversas. La herramienta se divide en dos módulos. Por un lado, el código en Python implementa la ofuscación y el cifrado. Por otro, el código en C, implementa la parte inversa, es decir, la deofuscación y el descifrado. También he incluido un main.c de ejemplo que demuestra cómo se aplican cada una de estas técnicas.

  • Cifrado: soporte para cifrados (AES, ChaCha20, RC4 y XOR) con diferentes modos de operación
  • Ofuscación: convierte el payload en una lista de strings de IP, MAC, UUID o correos electrónicos

En este post me voy a centrar en la ofuscación. Para el cifrado en C, no me he querido calentar la cabeza y he tirado de implementaciones existentes; la lógica en Python y el descifrador XOR sí que son “cosecha propia”. Mi objetivo no era reinventar la rueda del cifrado ni mucho menos, sino mancharme las manos y entender mejor cómo funciona la ofuscación a más bajo nivel. Ojo, que el cifrado es muy útil para camuflar acciones (shellcode incluido), pero haberme picado los algoritmos desde cero me habría llevado una eternidad. Y seamos sinceros, el resultado habría sido mucho peor comparado con algo ya testeado mil y una veces.

Nitto en acción

Nitto está pensada para usarse de forma secuencial, pero al final, cada persona es de su madre y de su padre:

  1. Usa el script de Python para generar el payload transformado.
  2. Integra los archivos necesarios de la implementación en C (src/obfuscation/ e include/obfuscation/) en tu código para realizar la operación inversa:
    • Para ofuscación basada en emails: email2byte.c, email2byte.h y las lookup tables en include/hashTables/.
    • Para la basada en IPs: ip2byte.c e ip2byte.h.
    • Para otras (MAC o UUID) se sigue el mismo patrón que en la de IPs, pero con sus archivos .c y .h correspondientes.
  3. Incluye el shellcode ofuscado en tu código y llama a la función de deofuscación en runtime.

En el siguiente gif puedes ver el módulo de Python ofuscando el payload de un calc.exe de msfvenom usando las 5 técnicas (email, GUIDOjo, ‘GUID’ y no UUID. Lo que estoy generando es el formato de UUID específico de Windows ENLACE EXTERNO A:

https://en.wikipedia.org/wiki/Universally_unique_identifier#Endianness
, ese que parecer tener little y big endian mezclado, cuando en realidad no es así ENLACE EXTERNO A:

https://devblogs.microsoft.com/oldnewthing/20220928-00/?p=107221
, aunque lo parezca.
, MAC, IPv4 e IPv6):

Ofuscación en Python

Para generar el bicho, usé: msfvenom -p windows/x64/exec CMD="calc.exe" EXITFUNC=thread -a x64 --platform windows -o calc.bin, que lanza la calculadora para confirmar que el payload se está ejecutando. El flag EXITFUNC=thread asegura una terminación limpia cuando se ejecuta en un thread, que es exactamente lo que uso en el código C de abajo.

Y aquí tienes al main.c en acción. El programa itera sobre las 5 técnicas: deofusca el payload, lo ejecuta (haciendo que aparezca la calculadora) y espera a que pulses Enter para limpiar la memoria y pasar a la siguiente técnica:

Flujo completo en acción

Cómo funciona la deofuscación

El flujo a alto nivel es el siguiente:

  1. Iterar a través de una lista de strings ASCII (IPs, MACs, UUIDs, emails, etc.)
  2. Convertir cada una a binario para ir recuperando la shellcode byte a byte
  3. Ejecutamos
  4. Profit

Deofuscación de IPFuscation

En Nitto, del paso 2 se encarga Ipv4StringToAddress. Es una función que he creado y hace básicamente lo mismo (más/menos) que la famosa RtlIpv4StringToAddressA de la WinAPI. Itera por el string de la IP carácter por carácter (2 -> 5 -> 2 -> . -> …), extrae cada octeto y lo convierte a hexadecimal. El resultado se guarda en el heap. Por ejemplo, para la IP “252.72.131.228”, el proceso es este:

  • 252 -> 0xFC
  • 72 -> 0x48
  • 131 -> 0x83
  • 228 -> 0xE4

En cada iteración, el chunk actual de 4 bytes se añade al anterior, reconstruyendo el payload completo.

Una vez que la shellcode está lista en memoria, solo queda ejecutarla. No me voy a meter en la ejecución, pero la esencia es que, en este punto, los bytes originales de la shellcode están totalmente reconstruidos en memoria y listos para ser ejecutados. Esto lo podemos ver perfectamente en x64dbg: los mismos bytes de la shellcode que se encuentran en memoria, se interpretan como instrucciones reales de la CPU (CLD, CALL, etc.).

Shellcode reconstruida en memoria y su interpretación en x64dbg

Evadiendo la detección

Aquí es donde está parte de la chicha. La técnica de IPFuscation ENLACE EXTERNO A:

https://www.sentinelone.com/blog/hive-ransomware-deploys-novel-ipfuscation-technique/
y sus variantes (MAC y UUID) suelen tirar de DLLs del sistema (Rpcrt4.dll o Ntdll.dll) para reconstruir la shellcode con funciones como:

  • RtlIpv4StringToAddressA
  • RtlIpv6StringToAddressA
  • RtlEthernetStringToAddressA
  • UuidFromStringA

¿El problema? Que los AVs y EDRs modernos no son tontos y monitorizan este tipo de llamadas a la API, especialmente cuando van seguidas de funciones de asignación de memoria como VirtualAlloc o WriteProcessMemory. Si no cargamos esas DLLs o pasamos olímpicamente de llamar a sus funciones (ejem, mi implementación custom, ejem), tenemos muchas más papeletas de pasar desapercibidos

Y por eso precisamente he decidido implementar mis propias funciones de deofuscación en lugar de usar llamadas a WinAPI o NTDLL:

Así, funciones como RtlIpv4StringToAddressA o UuidFromStringA no aparecen en la IAT ENLACE EXTERNO A:

https://en.wikipedia.org/wiki/Portable_Executable#Import_table
del PE ENLACE EXTERNO A:

https://es.wikipedia.org/wiki/Portable_Executable
de Nitto:

Vista de la IAT desde IDA

Ofuscación Basada en Emails: Mi Granito de Arena

Lo bueno de esta técnica es que a una IA o a una herramienta de análisis le cuesta mucho más deofuscarlo de un vistazo. Claro que, si se extraen las lookup tables del binario (tienen que estar empaquetadas dentro para que la deofuscación funcione), podrían llegar a revertirlo con suficiente contexto. Pero al fin y al cabo, esto pasa con cualquier técnica.

Mientras que otras técnicas de ofuscación convierten cada byte a su representación decimal (IPv4) o hexadecimal (IPv6, MAC y UUID), con los emails la cosa se complica. Los caracteres válidos en una dirección de correo están limitados a alfanuméricos (a-Z, 0-9), puntos (ni al principio, ni al final, ni consecutivos), barras bajas, guiones y signos más (+). Aunque los espacios y otros caracteres especiales ((),:;<>@[\\]) se consideran válidos según las reglas de la RFC correspondiente ENLACE EXTERNO A:

https://en.wikipedia.org/wiki/Email_address#Local-part
, generalmente se desaconsejan o incluso se prohíben. Estas restricciones nos impiden hacer una conversión directa de byte a ASCII.

Digamos que tenemos la siguiente secuencia de bytes: \xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50, la misma que vimos hace unos párrafos. Muchos de estos bytes representan controles no imprimibles o caracteres no alfanuméricos: \xfc=ü, \xc0=À, \x83=(ASCII Extendido/No imprimible), \x00=(Carácter nulo), etc. Esto quiere decir que no se pueden convertir en caracteres ASCII sin perder información.

Aquí entran dos enfoques distintos de ofuscación por email que he desarrollado, cada uno con sus pros y sus contras:

  1. Conversión basada en tablas de búsqueda (lookup tables): el payload se divide a nivel de bit y se utilizan tablas de búsqueda para convertir los bytes del payload en componentes de email que parecen reales
  2. Conversión de byte a alfanumérico (en desarrollo): convierte un byte dado en su representación alfanumérica. Su principal beneficio es que te ahorras tener que incluir lookup tables en tu código

Mientras que el primer método genera direcciones más largas pero con una apariencia muy creíble, el segundo es más eficiente en cuanto a tamaño, pero los resultados no son realistas ni de lejos.

Conversión basada en tablas de búsqueda

Este método prioriza el realismo utilizando tablas precalculadas de nombres comunes, dominios y TLDs.

  1. Se divide el payload en bloques de 6 bytes (48 bits), añadiendo padding si es necesario
  2. Se particiona cada bloque de 48 bits en 6 fragmentos:
    • 12 bits (índice 0-4095) -> tabla de nombres (ejemplo john)
    • 5 bits (índice 0-31) -> iniciales del segundo nombre (ejemplo .j)
    • 12 bits (índice 0-4095) -> tabla de apellidos (ejemplo .doe)
    • 9 bits (índice 0-511) -> número (0-511) (ejemplo 27)
    • 5 bits (índice 0-31) -> tabla de dominios (ejemplo @gmail)
    • 5 bits (índice 0-31) -> tabla de TLDs (ejemplo .com)
  3. Se convierte cada fragmento a su representación decimal para usarlo como índice. Por ejemplo: 000000011011 (binario) = 27 (decimal) -> índice en la tabla de nombres
  4. Se busca cada índice en su tabla para recuperar el elemento
  5. Se concatena: [nombre].[inicial].[apellido][numero]@[dominio].[tld]

Así:

EmailFuscation 48-Bit Block Particionamiento

Las tablas de nombres y apellidos se generan utilizando el paquete Faker ENLACE EXTERNO A:

https://pypi.org/project/Faker/
de Python, que se encarga de crear los datos falsos por nosotros. El resto se basa en valores estáticos: los dominios y TLDs más comunes, números del 0 al 511 (9 bits) y las letras del alfabeto (sin la ñ) más 6 pares aleatorios para alcanzar las 32 entradas (5 bits). Después, asignamos un índice a cada elemento según su posición en la lista. Una vez que las listas están completas, usamos gperf ENLACE EXTERNO A:

https://www.gnu.org/software/gperf/
para crear una función de hash perfecta ENLACE EXTERNO A:

https://es.wikipedia.org/wiki/Funci%C3%B3n_hash#Inyectividad_y_funci%C3%B3n_hash_perfecta
en menos de un minuto. Todo esto está automatizado mediante wordListGenerator.py.

Este método depende de que uses las mismas tablas para ofuscar y deofuscar (firstNames.h, lastNames.h, etc. en scripts/helpers/wordLists). Si las cambias (por ejemplo, usando wordListGenerator.py), los emails ofuscados serán completamente diferentes para el mismo input.

Ejemplo con:

Toggle Line Numbers
Copy Code
1
2
3
4
\xfc\x48\x83\xe4\xf0\xe8\xc0
\x00\x00\x00\x41\x51\x41\x50
\x52\x51\x56\x48\x31\xd2\x65
\x48\x8b\x52\x60\x48\x8b\x52

Resultado:

  • Bloque 1: \xfc\x48\x83\xe4\xf0\xe8 -> meike.r.hofinger316@protonmail.co
  • Bloque 2: \xc0\x00\x00\x00\x41\x51 -> estrella.a.rivera16@aol.cn
  • Bloque 3: \x41\x50\x52\x51\x56\x48 -> yael.a.hendricks85@comcast.co
  • Bloque 4: \x31\xd2\x65\x48\x8b\x52 -> alex.e.schutz34@virginmedia.br
  • Bloque 5: \x60\x48\x8b\x52\x02\x02 -> friedo.r.cuesta134@outlook.co

Al último bloque le faltaban 2 bytes y se le añadió padding (\x02\x02) usando el algoritmo PKCS#7 ENLACE EXTERNO A:

https://en.wikipedia.org/wiki/PKCS_7
.

Pasamos de 14 bytes a 155.

Conversión de byte a alfanumérico

Con este método, lo que buscamos es que el payload ocupe lo mínimo, dejando el realismo en un segundo plano.

  1. Itera por cada byte:
    • Si el byte representa una letra minúscula (a-z, \x61-\x7a), se usa el carácter directamente (O mayúsculas también si tratamos los emails como case-sensitive)
    • De lo contrario, se convierte a su representación decimal
    • Los bytes de dígitos ASCII (\x30-\x39) se usan tal cual, no se convierten a números; es decir, \x30 sería igual a 60 (en decimal), y no al carácter 0
  2. Los bytes se reparten en emails de longitud variable (4-10 bytes cada uno) y se unen los números (no las letras) con puntos, guiones o barras bajas para crear una parte local pseudo-realista
  3. Añade @dominio.tld para crear la dirección de correo

Ejemplo con:

Toggle Line Numbers
Copy Code
1
2
3
4
\xfc\x48\x83\xe4\xf0\xe8\xc0
\x00\x00\x00\x41\x51\x41\x50
\x52\x51\x56\x48\x31\xd2\x65
\x48\x8b\x52\x60\x48\x8b\x52

Conversiones de bytes utilizadas:

Toggle Line Numbers
Copy Code
1
2
3
4
5
6
\xfc -> 252    \x48 -> H      \x83 -> 131    \xe4 -> 228    \xf0 -> 240
\xe8 -> 232    \xc0 -> 192    \x00 -> 0      \x00 -> 0      \x00 -> 0
\x41 -> 65     \x51 -> 81     \x41 -> 65     \x50 -> 80     \x52 -> 82
\x51 -> 81     \x56 -> 86     \x48 -> H      \x31 -> 1      \xd2 -> 210
\x65 -> e      \x48 -> H      \x8b -> 139    \x52 -> 82     \x60 -> 96
\x48 -> H      \x8b -> 139    \x52 -> 82

Resultado (email de tamaño variable pseudo-aleatorio):

  • 252h_131.228@gmail.com
  • 240-232_0.65_81.65.80_82@yahoo.com
  • 81_86.h1.210e_h139@gmail.com
  • 82.96_h139.82@outlook.com

Pasamos de 14 bytes a 109.

A tener en cuenta:

  • la mayoría de los bytes acaban convertidos en números de 2 o 3 dígitos debido a la propia entropía de la shellcode
  • no suelen aparecer muchas letras minúsculas
  • el resultado parece menos realista, pero se reduce el número de bytes necesarios

Y eso es todo amigos ENLACE EXTERNO A:

https://www.youtube.com/watch?v=Ga_RwPmx-N0
, espero que te haya gustado.

Sed buenos!