Formato ELF de Linux: Estructura y Analisis para Malware
Estructura completa del formato ELF (Executable and Linkable Format) de Linux. ELF header, program headers, section headers, simbolos, linking dinamico y analisis de malware en binarios Linux.
ELF: el formato binario de Linux
Mientras Windows domina el escritorio, Linux gobierna servidores, contenedores, dispositivos IoT y la infraestructura cloud. El malware que apunta a estos entornos (botnets como Mirai, cryptominers, backdoors de APTs) usa el formato ELF. Con el crecimiento del malware Linux, especialmente en contenedores y dispositivos IoT, entender ELF es cada vez mas critico para analistas de malware.
ELF (Executable and Linkable Format) se usa en Linux, FreeBSD, Solaris, Android y otros sistemas basados en Unix. A diferencia del PE de Windows que evolucion desde DOS, ELF fue disenado desde el inicio para ser flexible y extensible.
Estructura general del formato ELF
Un archivo ELF tiene tres componentes principales:
| Componente | Posicion | Funcion |
|---|---|---|
| ELF Header | Inicio del archivo (offset 0) | Identifica el archivo, apunta al resto |
| Program Header Table | Despues del ELF Header | Segmentos para ejecucion |
| Section Header Table | Final del archivo (tipicamente) | Secciones para linking y analisis |
La diferencia conceptual clave: los program headers son la vista de ejecucion (como el kernel carga el binario en memoria), y los section headers son la vista de linking (como el linker organiza los datos). Un binario puede ejecutarse sin section headers, pero no sin program headers.
ELF Header: los primeros 64 bytes
El ELF Header siempre esta al inicio del archivo y ocupa 52 bytes (32-bit) o 64 bytes (64-bit):
| Campo | Tamano | Descripcion |
|---|---|---|
| e_ident[0..3] | 4 bytes | Magic: 0x7F 'E' 'L' 'F' |
| e_ident[4] | 1 byte | Clase: 1=32-bit, 2=64-bit |
| e_ident[5] | 1 byte | Endianness: 1=little, 2=big |
| e_ident[6] | 1 byte | Version ELF (siempre 1) |
| e_ident[7] | 1 byte | OS/ABI: 0=System V, 3=Linux |
| e_type | 2 bytes | Tipo: ET_EXEC, ET_DYN, ET_REL |
| e_machine | 2 bytes | Arquitectura: x86, x86_64, ARM |
| e_entry | 4/8 bytes | Direccion del entry point |
| e_phoff | 4/8 bytes | Offset de Program Header Table |
| e_shoff | 4/8 bytes | Offset de Section Header Table |
| e_phnum | 2 bytes | Numero de program headers |
| e_shnum | 2 bytes | Numero de section headers |
| e_shstrndx | 2 bytes | Indice de la seccion de strings |
Verificacion con readelf
readelf -h sample_elf
# Salida tipica:
# 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: DYN (Position-Independent Executable)
# Machine: Advanced Micro Devices X86-64
# Entry point address: 0x1060
# Start of program headers: 64 (bytes into file)
# Start of section headers: 14280 (bytes into file)
# Number of program headers: 13
# Number of section headers: 31
Tipos de ELF relevantes para malware
| Tipo | Valor | Descripcion | Contexto malware |
|---|---|---|---|
| ET_EXEC | 2 | Ejecutable con direccion fija | Binarios estaticos, malware simple |
| ET_DYN | 3 | Shared object / PIE | Ejecutables modernos, librerias |
| ET_REL | 1 | Archivo objeto reubicable | Rootkits (modulos kernel .ko) |
| ET_CORE | 4 | Core dump | Analisis forense |
Nota sobre PIE: Los ejecutables modernos de Linux se compilan como Position-Independent Executables (tipo ET_DYN, no ET_EXEC). Esto permite ASLR. Si un malware es ET_EXEC con direccion fija, probablemente es antiguo o esta disenado para entornos sin ASLR.
Program Headers: los segmentos de ejecucion
Los program headers definen como el kernel carga el binario en memoria. Cada entrada describe un segmento:
| Campo | Descripcion |
|---|---|
| p_type | Tipo de segmento (PT_LOAD, PT_DYNAMIC, etc.) |
| p_flags | Permisos: PF_R (read), PF_W (write), PF_X (execute) |
| p_offset | Offset del segmento en el archivo |
| p_vaddr | Direccion virtual donde se carga |
| p_memsz | Tamano en memoria |
| p_filesz | Tamano en archivo |
Tipos de segmentos importantes
| Tipo | Valor | Descripcion |
|---|---|---|
| PT_LOAD | 1 | Segmento cargado en memoria |
| PT_DYNAMIC | 2 | Informacion de linking dinamico |
| PT_INTERP | 3 | Ruta del interprete (ld-linux.so) |
| PT_NOTE | 4 | Notas del sistema (build ID, ABI) |
| PT_GNU_STACK | 0x6474E551 | Permisos del stack |
| PT_GNU_RELRO | 0x6474E552 | Read-only relocations |
readelf -l sample_elf
# Program Headers:
# Type Offset VirtAddr FileSiz MemSiz Flg
# INTERP 0x000318 0x0000000000000318 0x00001c 0x00001c R
# LOAD 0x000000 0x0000000000000000 0x000628 0x000628 R
# LOAD 0x001000 0x0000000000001000 0x000185 0x000185 R E
# LOAD 0x002000 0x0000000000002000 0x0000f4 0x0000f4 R
# LOAD 0x002db8 0x0000000000003db8 0x000258 0x000260 RW
# DYNAMIC 0x002dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW
# GNU_STACK 0x000000 0x0000000000000000 0x000000 0x000000 RW
Indicadores de malware en program headers:
- Stack ejecutable (PT_GNU_STACK con flag X): El stack normalmente no deberia ser ejecutable. Si lo es, el binario puede ejecutar shellcode en el stack.
- Segmento LOAD con permisos RWX: Similar a las secciones RWX en PE, indica codigo auto-modificable.
- Sin PT_INTERP: El binario es estaticamente enlazado, no depende de librerias compartidas. Comun en malware que quiere funcionar en cualquier distribucion Linux.
- MemSiz mucho mayor que FileSiz en un segmento LOAD: Se expande en runtime, similar al patron de packers en PE.
Section Headers: la vista de analisis
Las section headers describen las secciones del binario. Son opcionales para la ejecucion pero esenciales para el analisis:
Secciones estandar en ELF
| Seccion | Contenido | Equivalente PE |
|---|---|---|
| .text | Codigo ejecutable | .text |
| .data | Variables inicializadas | .data |
| .bss | Variables no inicializadas | .bss |
| .rodata | Datos de solo lectura, strings | .rdata |
| .symtab | Tabla de simbolos | No tiene equivalente directo |
| .strtab | Strings de nombres de simbolos | Parte de .rdata |
| .dynsym | Simbolos dinamicos | Parte de Import/Export Table |
| .dynstr | Strings de simbolos dinamicos | Parte de Import/Export Table |
| .plt | Procedure Linkage Table | Parte de IAT |
| .got | Global Offset Table | IAT |
| .got.plt | GOT para PLT | IAT |
| .dynamic | Informacion de linking dinamico | Import Directory |
| .init | Codigo de inicializacion | TLS callbacks |
| .fini | Codigo de finalizacion | No tiene equivalente |
| .init_array | Punteros a funciones de init | TLS callbacks |
| .note.* | Notas del compilador | Debug Directory |
| .comment | String del compilador | Rich Header |
readelf -S sample_elf
# Section Headers:
# [Nr] Name Type Address Off Size
# [ 0] NULL 0000000000000000 000000 000000
# [ 1] .interp PROGBITS 0000000000000318 000318 00001c
# [14] .text PROGBITS 0000000000001060 001060 000125
# [16] .rodata PROGBITS 0000000000002000 002000 000011
# [25] .data PROGBITS 0000000000004000 003000 000010
# [26] .bss NOBITS 0000000000004010 003010 000008
Secciones stripped: el problema del analisis
El comando strip elimina los section headers innecesarios para la ejecucion, reduciendo el tamano del binario y dificultando el analisis:
# Binario normal
file sample
# sample: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped
# Despues de strip
strip sample
file sample
# sample: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
El malware suele estar stripped para dificultar el reverse engineering. Sin la tabla de simbolos (.symtab), GDB y otros debuggers no pueden mostrar nombres de funciones, obligando al analista a trabajar con direcciones.
Linking dinamico: .plt, .got y .dynamic
El mecanismo de linking dinamico en ELF es equivalente a la IAT del PE, pero usa un enfoque diferente basado en PLT (Procedure Linkage Table) y GOT (Global Offset Table).
Como funciona la resolucion lazy
- El codigo llama a una funcion externa (ej.
printf) a traves de la PLT. - La primera vez, la PLT redirige al resolver del linker dinamico (ld-linux.so).
- El resolver busca la funcion en las librerias cargadas.
- Escribe la direccion real en la GOT.
- Las llamadas siguientes van directamente de la PLT a la GOT (ya resuelta).
# Ver imports dinamicos
readelf -d sample_elf | grep NEEDED
# 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
# 0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
# Ver simbolos dinamicos importados
readelf --dyn-syms sample_elf
# O con objdump
objdump -T sample_elf
Funciones sospechosas en imports ELF
| Funcion | Libreria | Indicador |
|---|---|---|
| execve / system | libc | Ejecucion de comandos |
| fork / clone | libc | Creacion de procesos |
| socket / connect / send | libc | Comunicacion de red |
| ptrace | libc | Anti-debug o inyeccion |
| mmap / mprotect | libc | Manipulacion de memoria |
| dlopen / dlsym | libdl | Carga dinamica (equiv. LoadLibrary) |
| unlink / rename | libc | Eliminacion de archivos |
| chmod | libc | Cambiar permisos |
| setuid / setgid | libc | Escalada de privilegios |
Simbolos: la informacion que revela el autor
La tabla de simbolos contiene nombres de funciones, variables y otros identificadores. En un ELF no stripped, es una fuente invaluable de informacion:
# Tabla de simbolos completa
readelf -s sample_elf
# Buscar funciones especificas
readelf -s sample_elf | grep -i "crypt\|encrypt\|socket\|exec"
# nm es otra herramienta para simbolos
nm sample_elf
# T = funcion definida en .text
# U = funcion undefined (importada)
# D = variable en .data
En binarios compilados con Go, Rust o C++ (no stripped), los nombres de funciones revelan estructura del codigo, modulos y paquetes usados. El malware compilado en Go es especialmente revelador porque Go incrusta los nombres de paquetes en los simbolos.
Analisis de protecciones de seguridad
La herramienta checksec verifica que protecciones tiene habilitadas un ELF:
checksec --file=sample_elf
# RELRO STACK CANARY NX PIE
# Full RELRO Canary found NX enabled PIE enabled
| Proteccion | Que hace | Implicacion si ausente |
|---|---|---|
| NX (No Execute) | Stack no ejecutable | Shellcode puede ejecutarse en stack |
| PIE | Direcciones aleatorias (ASLR) | Direcciones predecibles |
| RELRO (Full) | GOT de solo lectura | GOT overwrite posible |
| Stack Canary | Detecta buffer overflow | Stack smashing factible |
| FORTIFY | Comprobaciones en funciones de string | Menos proteccion contra overflow |
Patrones de malware:
- Sin NX ni PIE: binario compilado sin protecciones, probablemente malware simple o antiguo.
- Sin RELRO: permite GOT overwrite, tecnica usada por exploits pero rara en malware standalone.
Analisis estatico de ELF: flujo de trabajo
Paso 1: Identificacion basica
file sample_elf
# ELF 64-bit LSB pie executable, x86-64, dynamically linked,
# BuildID[sha1]=abc123, for GNU/Linux 3.2.0, stripped
sha256sum sample_elf
# a1b2c3d4e5f6... sample_elf
Paso 2: Headers y secciones
readelf -h sample_elf # ELF header
readelf -l sample_elf # Program headers (segmentos)
readelf -S sample_elf # Section headers
readelf -d sample_elf # Dynamic section (imports)
Paso 3: Strings y datos
strings -a sample_elf | head -50
strings -el sample_elf # Strings en UTF-16 (raro en Linux)
Paso 4: Protecciones
checksec --file=sample_elf
Paso 5: Dependencias
ldd sample_elf
# No ejecutar ldd en binarios sospechosos (ejecuta el binario).
# Alternativa segura:
readelf -d sample_elf | grep NEEDED
Importante: Nunca ejecutar ldd en un binario sospechoso. ldd ejecuta parcialmente el binario para resolver dependencias. Usar readelf -d en su lugar.
Paso 6: Desemsamblado
objdump -d sample_elf | head -100 # Desensamblado completo
objdump -d -M intel sample_elf # Sintaxis Intel
Tecnicas especificas de malware Linux
Binarios estaticamente enlazados
El malware puede compilarse con todas las librerias incluidas (static linking) para no depender de las librerias del sistema objetivo:
file static_malware
# ELF 64-bit LSB executable, x86-64, statically linked, stripped
Ventaja para el atacante: funciona en cualquier distribucion Linux sin depender de versiones de libc. Desventaja: tamano mucho mayor (varios MB vs cientos de KB).
Binarios Go en malware
Go se ha convertido en un lenguaje popular para malware por:
- Compilacion estatica por defecto (funciona en cualquier Linux)
- Cross-compilation trivial (compilar para Linux desde Windows)
- Tamano grande dificulta analisis (binarios de 5-20 MB)
- Concurrencia nativa (goroutines para C2 y tareas paralelas)
Familias en Go: GoBrut, Kaiji, Chaos, BotenaGo.
# Detectar binarios Go
strings sample_elf | grep "go.buildid"
strings sample_elf | grep "runtime.main"
Rootkits como modulos del kernel
Los rootkits de Linux se distribuyen como modulos del kernel (.ko), que son archivos ELF de tipo ET_REL:
file rootkit.ko
# rootkit.ko: ELF 64-bit LSB relocatable, x86-64
# Examinar informacion del modulo
modinfo rootkit.ko
readelf -s rootkit.ko | grep -i "init_module\|cleanup_module"
Memfd_create: ejecucion sin archivo
El malware avanzado de Linux usa memfd_create para crear un archivo en memoria, escribir un ELF en el y ejecutarlo con fexecve. El binario nunca toca el disco:
# Detectar en imports
readelf --dyn-syms sample_elf | grep "memfd_create\|fexecve"
Herramientas esenciales para ELF
| Herramienta | Funcion |
|---|---|
| readelf | Examinar todas las estructuras ELF |
| objdump | Desensamblar y examinar secciones |
| nm | Listar simbolos |
| strings | Extraer strings |
| file | Identificar tipo de archivo |
| checksec | Verificar protecciones |
| lief (Python) | Parsear ELF programaticamente |
| Ghidra | Decompilacion y analisis avanzado |
| GDB | Debugging |
| strace / ltrace | Trazar syscalls y llamadas a librerias |
Conclusion
El formato ELF es el pilar del analisis de malware en Linux. Su estructura de program headers y section headers, su mecanismo de linking dinamico con PLT/GOT, y las particularidades de binarios Go y modulos del kernel forman el conocimiento base para cualquier analista que trabaje con amenazas en servidores, contenedores y dispositivos IoT.
La clave es dominar las herramientas nativas (readelf, objdump, strings) y entender que un malware Linux stripped y estaticamente enlazado requiere tecnicas de analisis diferentes a un PE de Windows con sus imports expuestas en la IAT.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Formato PE de Windows: Estructura Completa del Ejecutable
Mach-O: Analisis de Binarios macOS e iOS
Analisis Estatico Basico: Strings, Hashes y Metadatos
Analisis de Firmware: Binwalk, Extraccion y Reverse Engineering
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.