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.
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
| Tipo | Descripcion | Tamaño tipico |
|---|---|---|
| Download and execute | Descarga un archivo y lo ejecuta | 200-500 bytes |
| Reverse shell | Abre conexión TCP inversa al atacante | 300-600 bytes |
| Bind shell | Abre un puerto y espera conexiones | 300-600 bytes |
| Staged | Fase 1 pequeña que descarga fase 2 (más grande) | 50-100 bytes (stager) |
| Egg hunter | Busca en memoria un marker para encontrar el payload principal | 30-60 bytes |
| Meterpreter stager | Stager de Metasploit que carga Meterpreter en memoria | 200-400 bytes |
| Custom | Shellcode propio del atacante | Variable |
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):
| Hash | Funcion |
|---|---|
| 0x0726774C | LoadLibraryA |
| 0x7802F749 | GetProcAddress |
| 0xE8AFE98 | WinExec |
| 0x0E8AFE98 | ExitProcess |
| 0x006B8029 | WSAStartup |
| 0xE0DF0FEA | WSASocketA |
| 0x6174A599 | connect |
| 0x614D6E75 | recv |
| 0x56A2B5F0 | exit |
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:
- Carga ws2_32.dll (networking).
- Crea un socket TCP.
- Conecta a 192.168.1.100:4444.
- Recibe datos (payload de segunda fase).
- Reserva memoria ejecutable.
- 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
| Herramienta | Funcion |
|---|---|
| scdbg | Emulacion de shellcode con hooks de API |
| Unicorn Engine | Emulacion de CPU programable |
| Speakeasy (FireEye) | Emulador de Windows para shellcode y PE |
| jmp2it | Ejecuta shellcode en un proceso controlado |
| shellcode2exe | Convierte shellcode a PE para analizar en debugger |
| blobrunner | Ejecuta shellcode en un proceso debuggeable |
| msfvenom | Genera shellcode (para reconocer patrones) |
| CyberChef | Decodificacion 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
Libros recomendados
Artículos relacionados
Import Address Table: APIs Sospechosas y Resolucion Dinamica
Formato PE de Windows: Estructura Completa del Ejecutable
Tecnicas Anti-Analisis: Anti-Debug, Anti-VM y Anti-Sandbox
Ofuscacion de codigo: tecnicas y estrategias de deofuscacion
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.