Avanzadoshellcodeemulacionexploitsreverse engineeringdeteccionanalisis avanzado

Analisis de Shellcode: Emulacion, Decodificacion y Deteccion

Analisis de shellcode en malware. Tipos de shellcode, decodificacion de stubs, emulacion con scdbg y Unicorn Engine, patrones comunes de shellcode en.

MalwareIntel Research··10 min lectura
Serie: Análisis de Binarios — Parte 13

Shellcode: código sin contexto

TL;DR: El shellcode es una secuencia de instrucciones de máquina pura, carente de cabeceras o loaders, diseñada para ejecutarse de forma independiente en memoria. A diferencia de los ejecutables PE convencionales, estos varían en tamaño desde pequeños stagers de 50 bytes hasta complejos códigos de descarga de 500 bytes. Para una detección eficaz, es fundamental emplear herramientas de emulación avanzada como scdbg que permitan analizar su comportamiento dinámico.

El shellcode es la forma más primitiva y poderosa de payload. No es un ejecutable con cabeceras y secciones: es una secuencia raw de instrucciones de máquina que puede ejecutarse desde cualquier posicion de memoria. No tiene imports, no tiene entry point definido por el sistema operativo, y no depende de un loader.

Esta independencia lo hace perfecto para exploits (inyectar en buffers vulnerables), para malware (inyectar en procesos remotos) y para evasion (no tiene la estructura de un PE que los antivirus puedan analizar).

Analizar shellcode requiere técnicas diferentes a analizar un PE o ELF: no hay cabeceras que parsear, no hay imports que listar, y las herramientas estandar de análisis estatico no funcionan directamente.

Tipos de shellcode

TipoDescripcionTamaño tipico
Download and executeDescarga un archivo y lo ejecuta200-500 bytes
Reverse shellAbre conexión TCP inversa al atacante300-600 bytes
Bind shellAbre un puerto y espera conexiones300-600 bytes
StagedFase 1 pequeña que descarga fase 2 (más grande)50-100 bytes (stager)
Egg hunterBusca en memoria un marker para encontrar el payload principal30-60 bytes
Meterpreter stagerStager de Metasploit que carga Meterpreter en memoria200-400 bytes
CustomShellcode propio del atacanteVariable

Estructura tipica del shellcode

Shellcode simple (sin cifrar)

[PEB access]        ; Obtener base de kernel32.dll via PEB
[API resolution]    ; Resolver funciones por hash (GetProcAddress equiv)
[Payload logic]     ; Codigo funcional (download, shell, etc.)

Shellcode con decoder stub

[NOP sled]          ; Opcional, 0x90 bytes para tolerancia de alineacion
[Decoder stub]      ; Descifra el payload cifrado
[Encoded payload]   ; Payload real cifrado (XOR, ADD, etc.)

El decoder stub es la parte mas reconocible del shellcode:

; Decoder stub XOR tipico (x86)
jmp short end_of_payload    ; salta al final
decode:
    pop esi                  ; ESI = direccion del payload cifrado
    xor ecx, ecx            ;
    mov cl, PAYLOAD_LEN      ; tamano del payload
loop_decode:
    xor byte [esi], 0xAA     ; descifra un byte con clave 0xAA
    inc esi                   ;
    loop loop_decode          ;
    jmp short decoded_payload ; salta al payload descifrado
end_of_payload:
    call decode               ; CALL pone la direccion siguiente en stack
decoded_payload:
    ; ... payload cifrado (XOR 0xAA) aqui ...

La tecnica JMP/CALL/POP es clasica: el JMP salta al CALL, el CALL pone la direccion del payload en el stack, y el POP recupera esa direccion. Esto permite que el shellcode conozca su propia posicion en memoria (position-independent).

Resolucion de APIs en shellcode

El shellcode no tiene Import Address Table. Necesita resolver las direcciones de las APIs de Windows por si mismo:

Acceso al PEB (Process Environment Block)

; x86: acceso al PEB
mov eax, fs:[0x30]       ; PEB
mov eax, [eax + 0x0C]    ; PEB->Ldr (PEB_LDR_DATA)
mov eax, [eax + 0x14]    ; InMemoryOrderModuleList
; Primer modulo = ejecutable actual
; Segundo modulo = ntdll.dll
; Tercer modulo = kernel32.dll
mov eax, [eax]           ; siguiente entrada
mov eax, [eax]           ; siguiente entrada (kernel32)
mov eax, [eax + 0x10]    ; DllBase de kernel32.dll

; x64: acceso al PEB
mov rax, gs:[0x60]       ; PEB en x64
mov rax, [rax + 0x18]    ; PEB->Ldr
mov rax, [rax + 0x20]    ; InMemoryOrderModuleList

Resolucion por hash de nombre

Una vez con la base de kernel32.dll, el shellcode itera sobre la Export Directory para encontrar funciones:

; Pseudocodigo: encontrar funcion por hash
for each export_name in kernel32.exports:
    calculated_hash = hash_function(export_name)
    if calculated_hash == target_hash:
        return export_address

Hashes comunes (algoritmo ROR13, usado por Metasploit):

HashFuncion
0x0726774CLoadLibraryA
0x7802F749GetProcAddress
0xE8AFE98WinExec
0x0E8AFE98ExitProcess
0x006B8029WSAStartup
0xE0DF0FEAWSASocketA
0x6174A599connect
0x614D6E75recv
0x56A2B5F0exit

Analisis con scdbg

scdbg (shellcode debugger) emula la ejecucion del shellcode, hookeando las APIs de Windows:

# Analisis basico
scdbg /f shellcode.bin

# Con mas detalle
scdbg /f shellcode.bin /v

# Limitar instrucciones
scdbg /f shellcode.bin /s 100000

# Buscar en archivo mayor (eg. un documento con exploit)
scdbg /f document.doc /findsc

# Dump del shellcode decodificado
scdbg /f shellcode.bin /d

# Salida tipica:
# 4010a8  LoadLibraryA(ws2_32)
# 4010ba  WSAStartup(190)
# 4010d1  WSASocket(af=2, type=1, protocol=0)
# 4010e8  connect(h=68, host: 192.168.1.100, port: 4444)
# 401105  recv(h=68, buf=12fc68, len=4096)
# 401125  VirtualAlloc(base=0, sz=4096, flags=1000, prot=40)
# 40113c  recv(h=68, buf=10000, len=4096)
# 401165  Execution transferred to 0x10000

Este output revela que el shellcode:

  1. Carga ws2_32.dll (networking).
  2. Crea un socket TCP.
  3. Conecta a 192.168.1.100:4444.
  4. Recibe datos (payload de segunda fase).
  5. Reserva memoria ejecutable.
  6. Ejecuta el payload descargado.

Es un stager clasico de Metasploit reverse_tcp.

Emulacion con Unicorn Engine

Para analisis mas controlado, Unicorn Engine permite emular CPU sin un sistema operativo:

from unicorn import Uc, UC_ARCH_X86, UC_MODE_32
from unicorn.x86_const import (
    UC_X86_REG_EAX, UC_X86_REG_EBX,
    UC_X86_REG_ECX, UC_X86_REG_ESP,
    UC_X86_REG_EIP
)

# Leer shellcode
with open("shellcode.bin", "rb") as f:
    shellcode = f.read()

# Crear emulador x86-32
mu = Uc(UC_ARCH_X86, UC_MODE_32)

# Mapear memoria
BASE = 0x400000
STACK = 0x500000
mu.mem_map(BASE, 0x100000)       # Codigo
mu.mem_map(STACK, 0x100000)      # Stack
mu.mem_write(BASE, shellcode)
mu.reg_write(UC_X86_REG_ESP, STACK + 0x80000)

# Hook para interceptar instrucciones
instruction_count = 0

def hook_code(uc, address, size, user_data):
    global instruction_count
    instruction_count += 1
    if instruction_count > 10000:
        uc.emu_stop()

mu.hook_add(1, hook_code)  # UC_HOOK_CODE = 1

# Emular
try:
    mu.emu_start(BASE, BASE + len(shellcode), timeout=5000000)
except Exception as e:
    print("Emulation stopped:", str(e))

# Leer memoria modificada (shellcode decodificado)
decoded = mu.mem_read(BASE, len(shellcode))
print("Decoded bytes:", bytes(decoded[:50]).hex())

Unicorn con hooks de memoria

def hook_mem_access(uc, access, address, size, value, user_data):
    """Hook para detectar acceso al PEB"""
    if access == 16:  # UC_MEM_READ
        if address == 0x7FFE0030:  # Posible acceso PEB
            print("PEB access at EIP:", hex(uc.reg_read(UC_X86_REG_EIP)))
    elif access == 17:  # UC_MEM_WRITE
        print(
            "Write:", hex(value),
            "to:", hex(address),
            "at EIP:", hex(uc.reg_read(UC_X86_REG_EIP))
        )

# Simular PEB y kernel32
PEB_ADDR = 0x7FFE0000
mu.mem_map(PEB_ADDR, 0x1000)
# Configurar PEB con datos simulados...

Patrones comunes de deteccion

NOP Sled

Secuencia de instrucciones NOP (0x90) o equivalentes antes del shellcode. Aumenta la probabilidad de acertar en el entry point cuando la direccion exacta es imprecisa:

# Buscar NOP sleds en un archivo
python3 -c "
data = open('sample.bin', 'rb').read()
nop_count = 0
for i, b in enumerate(data):
    if b == 0x90:
        nop_count += 1
        if nop_count >= 20:
            print('NOP sled at offset', hex(i - nop_count + 1), 'length', nop_count)
    else:
        nop_count = 0
"

Decoder stubs

Buscar patrones de decoder:

  • EB xx 5E: JMP short + POP ESI (JMP/CALL/POP)
  • E8 FF FF FF FF C0: CALL + INC EAX (variante de auto-referencia)
  • 31 C9 B1 xx 80 34: XOR ECX,ECX + MOV CL + XOR [ESI] (decoder loop)

Acceso al PEB

# Buscar patrones de acceso al PEB en x86
# fs:[0x30] = 64 A1 30 00 00 00
# gs:[0x60] = 65 48 8B 04 25 60 00 00 00

python3 -c "
data = open('sample.bin', 'rb').read()
# PEB access x86
peb_x86 = bytes([0x64, 0xA1, 0x30, 0x00, 0x00, 0x00])
pos = data.find(peb_x86)
if pos >= 0:
    print('PEB access (x86) at offset', hex(pos))

# PEB via mov eax, fs:[30h]
peb_x86_v2 = bytes([0x64, 0x8B, 0x15, 0x30, 0x00, 0x00, 0x00])
pos = data.find(peb_x86_v2)
if pos >= 0:
    print('PEB access v2 (x86) at offset', hex(pos))
"

Hash constants

Buscar constantes de hashing conocidas:

HASH_CONSTANTS = dict()
HASH_CONSTANTS[0x5381] = "djb2 init"
HASH_CONSTANTS[0x1505] = "djb2 init (alt)"
HASH_CONSTANTS[0xEDB88320] = "CRC32 polynomial"
HASH_CONSTANTS[0x0D] = "ROR13 rotation"

import struct

def find_hash_constants(filepath):
    with open(filepath, "rb") as f:
        data = f.read()

    for offset in range(len(data) - 3):
        dword = struct.unpack_from("<I", data, offset)[0]
        if dword in HASH_CONSTANTS:
            print(
                "Found", HASH_CONSTANTS[dword],
                "at offset", hex(offset)
            )

Shellcode en documentos Office

Los exploits para Office (CVE-2017-11882, CVE-2017-0199, etc.) embeben shellcode dentro de documentos:

# Extraer shellcode de documentos Office con olevba
olevba document.doc

# rtfdump para documentos RTF
rtfdump.py document.rtf

# Buscar shellcode en OLE streams
oledump.py document.doc

# Extraer y analizar con scdbg
oledump.py -s 3 -d document.doc > extracted.bin
scdbg /f extracted.bin /findsc

Shellcode en PowerShell

Muchos ataques usan PowerShell para ejecutar shellcode en memoria:

# Patron tipico (obfuscado en malware real)
$buf = [byte[]]@(0xfc,0xe8,0x82,0x00,0x00,0x00,...)
$ptr = [Runtime.InteropServices.Marshal]::AllocHGlobal($buf.Length)
[Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, $buf.Length)
# Crear thread en la memoria asignada

Para analizar: extraer el array de bytes, guardarlo como archivo binario y analizarlo con scdbg.

Herramientas especializadas

HerramientaFuncion
scdbgEmulacion de shellcode con hooks de API
Unicorn EngineEmulacion de CPU programable
Speakeasy (FireEye)Emulador de Windows para shellcode y PE
jmp2itEjecuta shellcode en un proceso controlado
shellcode2exeConvierte shellcode a PE para analizar en debugger
blobrunnerEjecuta shellcode en un proceso debuggeable
msfvenomGenera shellcode (para reconocer patrones)
CyberChefDecodificacion de shellcode cifrado (XOR, base64)

Convertir shellcode a PE para debugging

# Con shellcode2exe (convierte a PE ejecutable)
shellcode2exe shellcode.bin output.exe

# Abrir output.exe en x64dbg para debugging paso a paso

Analisis con Speakeasy

Speakeasy de Mandiant emula un entorno Windows completo para shellcode:

import speakeasy

se = speakeasy.Speakeasy()
sc_addr = se.load_shellcode(
    "shellcode.bin",
    arch="x86"
)
se.run_shellcode(sc_addr)

# Speakeasy reporta todas las API calls:
# kernel32.LoadLibraryA("ws2_32.dll")
# ws2_32.WSASocketA(AF_INET, SOCK_STREAM, 0)
# ws2_32.connect(socket, 192.168.1.100:4444)

YARA rules para deteccion de shellcode

rule shellcode_peb_access
{
    meta:
        description = "Detects PEB access patterns common in shellcode"
        author = "MalwareIntel Research"

    strings:
        $peb_x86_1 = { 64 A1 30 00 00 00 }
        $peb_x86_2 = { 64 8B (0? | 1? | 2? | 3?) 30 00 00 00 }
        $peb_x64 = { 65 48 8B (04 | 0C | 14 | 1C) 25 60 00 00 00 }

    condition:
        any of ($peb_*)
}

rule shellcode_decoder_stub
{
    meta:
        description = "Detects common shellcode decoder stubs"

    strings:
        $jmp_call_pop = { EB ?? 5? (31 C9 | 33 C9) B1 }
        $call_pop = { E8 FF FF FF FF (C0 | C3) 5? }
        $xor_loop = { 80 (34 | 74) ?? ?? (4? | E2) }

    condition:
        any of them
}

Conclusion

El shellcode es la forma más cruda y poderosa de payload: código puro sin las estructuras que las herramientas de seguridad esperan encontrar. Su análisis requiere un enfoque diferente: emulacion con scdbg para obtener resultados rapidos, Unicorn Engine para control granular, y reconocimiento de patrones (PEB access, decoder stubs, hash constants) para detección. Dominar el análisis de shellcode es fundamental para entender exploits, payloads de frameworks como Metasploit y Cobalt Strike, y las fases iniciales de ataques sofisticados.

Preguntas frecuentes

Artículos relacionados

Este contenido tiene fines exclusivamente educativos y de investigación en ciberseguridad defensiva. No se proporcionan binarios maliciosos ni payloads ejecutables. El uso indebido de esta información es responsabilidad exclusiva del usuario. Leer disclaimer completo.