Secciones PE: .text, .data, .rsrc, .reloc y Anomalias
Analisis detallado de las secciones del formato PE de Windows. Seccion .text, .data, .rdata, .rsrc, .reloc, .bss y secciones personalizadas. Entropia, permisos anomalos y deteccion de empaquetado.
Las secciones: donde vive el contenido real
Si el formato PE es un libro, las cabeceras son el indice y las secciones son los capitulos. Cada seccion contiene un tipo especifico de datos (codigo, variables, recursos, imports) con permisos que determinan como el sistema operativo las trata en memoria.
Para el analisis de malware, las secciones son reveladoras: sus nombres, permisos, tamanos y entropia cuentan una historia. Un PE empaquetado tiene secciones con entropia alta y permisos de escritura+ejecucion. Un PE con payload oculto tiene una seccion .rsrc desproporcionadamente grande. Un PE compilado con un framework exotico tiene nombres de secciones no estandar.
Anatomia de un Section Header
Cada seccion del PE esta descrita por un Section Header de 40 bytes. Los campos criticos para el analisis:
| Campo | Tamano | Relevancia |
|---|---|---|
| Name | 8 bytes | Nombre de la seccion (puede ser arbitrario) |
| VirtualSize | 4 bytes | Tamano cuando se carga en memoria |
| VirtualAddress | 4 bytes | RVA donde empieza en memoria |
| SizeOfRawData | 4 bytes | Tamano en el archivo (disco) |
| PointerToRawData | 4 bytes | Offset en el archivo |
| Characteristics | 4 bytes | Permisos y atributos |
La relacion entre VirtualSize y SizeOfRawData es uno de los indicadores mas utiles. En un PE normal, ambos valores son similares. En un PE empaquetado, VirtualSize suele ser mucho mayor: la seccion se expande en memoria cuando el packer descomprime el codigo original.
Seccion .text: el codigo ejecutable
La seccion .text contiene las instrucciones de maquina que el procesador ejecuta. Es la seccion mas importante y la primera que un analista examina.
Caracteristicas normales:
- Permisos: Read + Execute (0x60000020)
- Entropia: entre 5.5 y 6.8 (codigo compilado con patrones repetitivos)
- Tamano: proporcional a la complejidad del programa
- Entry point: normalmente apunta dentro de .text
Anomalias que delatan malware:
| Anomalia | Indicador | Causa probable |
|---|---|---|
| Entropia mayor de 7.0 | Contenido cifrado o comprimido | Packer o protector |
| Permisos RWX | Codigo auto-modificable | Desempaquetado runtime |
| VirtualSize mucho mayor que SizeOfRawData | Se expande en memoria | Packer descomprime en .text |
| Entry point fuera de .text | Ejecucion empieza en otra seccion | Packer, loader custom |
| .text ausente o renombrada | No existe seccion con nombre .text | Compilador no estandar o packer |
import pefile
pe = pefile.PE("sample.exe")
for section in pe.sections:
name = section.Name.decode().rstrip("\x00")
if name == ".text":
entropy = section.get_entropy()
ratio = section.Misc_VirtualSize / max(section.SizeOfRawData, 1)
chars = section.Characteristics
print("Entropia:", round(entropy, 2))
print("Ratio VS/Raw:", round(ratio, 2))
print("RWX:", bool(chars & 0xE0000000 == 0xE0000000))
Seccion .data: variables globales
La seccion .data almacena variables globales inicializadas del programa. Son datos que el codigo necesita leer y modificar durante la ejecucion.
Caracteristicas normales:
- Permisos: Read + Write (0xC0000040)
- Entropia: entre 1.0 y 5.0 (datos estructurados, muchos ceros)
- Tamano: pequeno en comparacion con .text
Variantes relacionadas:
| Seccion | Contenido | Diferencia con .data |
|---|---|---|
| .data | Variables inicializadas | Ocupa espacio en disco |
| .bss | Variables no inicializadas | Solo reserva espacio en memoria |
| .rdata | Datos de solo lectura | Sin permiso de escritura |
Anomalias en .data:
- Entropia alta (mayor de 6.5): Datos cifrados almacenados como variables globales. El malware a menudo guarda su configuracion de C2 cifrada en .data.
- Tamano desproporcionado: Una seccion .data que ocupa mas que .text sugiere datos embebidos (payload, configuracion extensa).
- Strings legibles en .data: Pueden revelar URLs de C2, claves de registro, rutas de archivos o nombres de procesos objetivo.
Seccion .rdata: datos de solo lectura
La seccion .rdata contiene datos que el programa lee pero nunca modifica. Aqui se encuentran:
- Tablas de imports y exports (Import Directory, IAT)
- Strings constantes
- Tablas de funciones virtuales (vtables en C++)
- Datos de la tabla de debug
Permisos normales: Read only (0x40000040)
Relevancia para malware: La seccion .rdata aloja la Import Address Table. Examinar esta seccion revela todas las APIs que el binario resuelve en tiempo de carga. Si .rdata es pequena o esta ausente, el malware probablemente resuelve sus imports dinamicamente con GetProcAddress.
En algunos compiladores y linkers, .rdata y .idata (import data) son secciones separadas. En otros, .idata esta embebida dentro de .rdata.
Seccion .rsrc: el escondite favorito del malware
La seccion .rsrc almacena recursos del ejecutable: iconos, menus, dialogos, manifests, tablas de version y datos arbitrarios. Es una de las secciones mas abusadas por el malware.
Estructura interna: Los recursos se organizan en un arbol de tres niveles:
.rsrc/
RT_ICON/ (tipo 3: iconos)
1/
1033/ (ingles)
RT_VERSION/ (tipo 16: informacion de version)
1/
0/
RT_MANIFEST/ (tipo 24: manifest XML)
1/
1033/
RT_RCDATA/ (tipo 10: datos arbitrarios)
101/
0/ <-- Aqui se ocultan payloads
Usos maliciosos de .rsrc:
| Tecnica | Descripcion | Ejemplo |
|---|---|---|
| Payload cifrado en RT_RCDATA | PE o shellcode cifrado como recurso | Emotet stages 2-3 |
| Configuracion de C2 | URLs, IPs y claves en recurso custom | Agent Tesla configs |
| PE embebido como recurso | Ejecutable completo dentro de .rsrc | Droppers genericos |
| Icono imitando app legitima | Icono de Word, Chrome, PDF | Phishing executables |
| Manifest con elevacion | requestedExecutionLevel=requireAdministrator | UAC bypass |
Deteccion:
import pefile
pe = pefile.PE("sample.exe")
if hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"):
for res_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
# RT_RCDATA = 10
if res_type.id == 10:
for entry in res_type.directory.entries:
data_entry = entry.directory.entries[0].data
size = data_entry.struct.Size
offset = data_entry.struct.OffsetToData
data = pe.get_data(offset, size)
# Verificar si empieza con MZ (PE embebido)
if data[:2] == b"MZ":
print("PE embebido encontrado en recurso")
print("Tamano:", size, "bytes")
Senales de alerta en .rsrc:
- Tamano de .rsrc mayor al 50% del tamano total del PE
- Recursos RT_RCDATA con entropia mayor de 7.0 (datos cifrados)
- Recursos con tamanos muy grandes (cientos de KB o mas)
- Recursos que empiezan con "MZ" (PE embebido sin cifrar)
Seccion .reloc: tabla de relocaciones
La seccion .reloc contiene la Base Relocation Table, que indica que direcciones del codigo necesitan ajustarse si el PE no se carga en su ImageBase preferida (lo cual ocurre siempre con ASLR habilitado).
Caracteristicas normales:
- Permisos: Read only (0x42000040, con flag DISCARDABLE)
- Entropia: 4.0 a 5.5
- Tamano: proporcional al numero de direcciones absolutas en el codigo
Relevancia para malware:
- Ausencia de .reloc: Si un .exe no tiene .reloc y ASLR esta deshabilitado en DllCharacteristics, el binario debe cargarse en una direccion fija. Esto es comun en malware antiguo o shellcode convertido a PE.
- .reloc con datos extra: Algunos packers ocultan datos en la seccion .reloc porque muchas herramientas de analisis la ignoran.
Secciones no estandar: huellas de packers y compiladores
Los nombres de secciones son arbitrarios (hasta 8 caracteres ASCII). Los compiladores estandar usan nombres convencionales, pero los packers y protectores crean los suyos:
| Nombre seccion | Herramienta | Notas |
|---|---|---|
| UPX0, UPX1 | UPX | Packer open source mas comun |
| .aspack | ASPack | Packer comercial |
| .adata | ASProtect | Protector comercial |
| .themida | Themida | Protector avanzado |
| .vmp0, .vmp1 | VMProtect | Virtualizacion de codigo |
| .ndata | NSIS Installer | Instalador Nullsoft |
| .enigma1 | Enigma Protector | Protector comercial |
| .petite | Petite | Packer antiguo |
| CODE | Delphi/Borland | Compilador Delphi |
| .textbss | MinGW/GCC | Compilador GCC para Windows |
| .CRT | MSVC | C Runtime initialization |
La presencia de secciones de packer no significa automaticamente que el binario sea malicioso. UPX se usa legitimamente para reducir tamano. Pero combinado con otros indicadores (sin imports, entropia alta, sin firma digital), es una senal fuerte.
Analisis de entropia por seccion
La entropia mide la aleatoriedad del contenido. Es un indicador fundamental para detectar cifrado o compresion:
| Rango entropia | Interpretacion | Ejemplo |
|---|---|---|
| 0.0 a 1.0 | Datos uniformes | Seccion llena de ceros |
| 1.0 a 4.0 | Datos estructurados | Texto ASCII, tablas |
| 4.0 a 6.0 | Codigo compilado | Seccion .text normal |
| 6.0 a 7.0 | Datos densos | Codigo optimizado, datos mixtos |
| 7.0 a 7.5 | Probablemente comprimido | UPX, LZMA |
| 7.5 a 8.0 | Probablemente cifrado | AES, XOR con clave larga |
import pefile
import math
def calculate_entropy(data):
if not data:
return 0.0
freq = dict()
for byte in data:
freq[byte] = freq.get(byte, 0) + 1
entropy = 0.0
length = len(data)
for count in freq.values():
p = count / length
if p > 0:
entropy -= p * math.log2(p)
return entropy
pe = pefile.PE("sample.exe")
print("Seccion Entropia Tamano Permisos")
print("-" * 55)
for section in pe.sections:
name = section.Name.decode().rstrip("\x00").ljust(16)
entropy = round(section.get_entropy(), 2)
size = section.SizeOfRawData
chars = section.Characteristics
r = "R" if chars & 0x40000000 else "-"
w = "W" if chars & 0x80000000 else "-"
x = "X" if chars & 0x20000000 else "-"
flag = ""
if entropy > 7.0:
flag = " [PACKED/ENCRYPTED]"
elif chars & 0xE0000000 == 0xE0000000:
flag = " [RWX]"
print(name, str(entropy).ljust(8), str(size).ljust(9), r+w+x + flag)
Permisos de secciones: la matriz de sospecha
Los permisos de una seccion determinan que operaciones permite el sistema operativo sobre esa region de memoria:
| Permiso | Flag | Valor |
|---|---|---|
| Execute | IMAGE_SCN_MEM_EXECUTE | 0x20000000 |
| Read | IMAGE_SCN_MEM_READ | 0x40000000 |
| Write | IMAGE_SCN_MEM_WRITE | 0x80000000 |
Combinaciones normales vs sospechosas:
| Combinacion | Normal | Sospechoso |
|---|---|---|
| R (solo lectura) | .rdata, .rsrc | No |
| RW (lectura+escritura) | .data, .bss | No |
| RX (lectura+ejecucion) | .text | No |
| RWX (todo) | Raro | Si, codigo auto-modificable |
| WX (escritura+ejecucion sin lectura) | Nunca legitimo | Si, altamente sospechoso |
| Sin permisos | Nunca | Si, posible anomalia del packer |
El principio W^X (Write XOR Execute) establece que una region de memoria debe poder escribirse O ejecutarse, pero no ambas cosas simultaneamente. Las protecciones modernas como DEP (Data Execution Prevention) aplican este principio. El malware que necesita desempaquetar codigo en runtime viola W^X intencionalmente.
Superposicion de secciones y cavidades PE
Overlay: datos despues del PE
El overlay es cualquier dato que existe despues de la ultima seccion del PE. El loader de Windows lo ignora, pero el programa puede leerlo con funciones de archivo estandar.
Usos maliciosos:
- Configuracion de C2 anexada al final del PE
- Payload cifrado que el dropper lee y descifra
- Datos de instalador (NSIS, Inno Setup usan overlay legitimamente)
overlay_offset = pe.get_overlay_data_start_offset()
if overlay_offset:
with open("sample.exe", "rb") as f:
f.seek(overlay_offset)
overlay = f.read()
print("Overlay encontrado:", len(overlay), "bytes")
print("Entropia:", round(calculate_entropy(overlay), 2))
Cavidades PE (PE caves)
Las cavidades son regiones de ceros (padding) entre secciones. Existen porque las secciones se alinean al FileAlignment (tipicamente 0x200 bytes). Si una seccion termina a mitad de un bloque de alineacion, el espacio restante se rellena con ceros.
El malware puede inyectar shellcode en estas cavidades sin modificar el tamano del PE. Las cavidades tipicas van de decenas a cientos de bytes, suficiente para un stub de desempaquetado o un desvio de flujo.
Casos practicos: perfiles de secciones
PE normal (notepad.exe)
.text Entropia: 6.1 Tamano: 0x1A000 R-X
.rdata Entropia: 4.8 Tamano: 0x0C000 R--
.data Entropia: 3.2 Tamano: 0x02000 RW-
.pdata Entropia: 5.1 Tamano: 0x03000 R--
.rsrc Entropia: 3.4 Tamano: 0x01000 R--
.reloc Entropia: 5.0 Tamano: 0x01000 R--
PE empaquetado con UPX
UPX0 Entropia: 0.0 Tamano: 0x00000 RWX [VIRTUAL]
UPX1 Entropia: 7.8 Tamano: 0x14000 RWX [PACKED]
UPX2 Entropia: 4.2 Tamano: 0x00200 R--
UPX0 tiene SizeOfRawData = 0 pero VirtualSize grande: el stub en UPX1 descomprime el codigo original en UPX0 en runtime.
PE con payload en recursos
.text Entropia: 6.0 Tamano: 0x08000 R-X
.rdata Entropia: 4.5 Tamano: 0x02000 R--
.data Entropia: 2.1 Tamano: 0x01000 RW-
.rsrc Entropia: 7.6 Tamano: 0x4C000 R-- [SUSPICIOUS]
.reloc Entropia: 4.8 Tamano: 0x01000 R--
La seccion .rsrc es 10 veces mas grande que .text y tiene entropia de 7.6: contiene un payload cifrado.
PE protegido con Themida
.text Entropia: 6.9 Tamano: 0x0A000 R-X
.rdata Entropia: 4.3 Tamano: 0x04000 R--
.data Entropia: 3.1 Tamano: 0x02000 RW-
.themida Entropia: 7.9 Tamano: 0x80000 RWX [PROTECTED]
.reloc Entropia: 0.0 Tamano: 0x00200 R--
La seccion .themida es enorme, con entropia casi maxima y permisos RWX. Contiene el motor de virtualizacion que ejecuta el codigo original en una maquina virtual interna.
Script de triaje automatizado
Este script analiza las secciones de un PE y genera un informe de triaje:
import pefile
import sys
def triage_sections(filepath):
pe = pefile.PE(filepath)
alerts = []
print("=== Section Triage Report ===")
print("File:", filepath)
print()
for section in pe.sections:
name = section.Name.decode().rstrip("\x00")
entropy = section.get_entropy()
vs = section.Misc_VirtualSize
raw = section.SizeOfRawData
chars = section.Characteristics
is_exec = bool(chars & 0x20000000)
is_write = bool(chars & 0x80000000)
is_read = bool(chars & 0x40000000)
is_rwx = is_read and is_write and is_exec
# Alertas
if entropy > 7.0:
alerts.append(
"ALTO: " + name + " tiene entropia "
+ str(round(entropy, 2))
+ " (probable cifrado/compresion)"
)
if is_rwx:
alerts.append(
"ALTO: " + name
+ " tiene permisos RWX (codigo auto-modificable)"
)
if vs > 0 and raw == 0:
alerts.append(
"MEDIO: " + name
+ " tiene VirtualSize > 0 pero SizeOfRawData = 0"
+ " (se expande en runtime)"
)
if raw > 0 and vs > raw * 5:
alerts.append(
"MEDIO: " + name
+ " se expande " + str(round(vs/raw, 1))
+ "x en memoria"
)
# Verificar overlay
overlay_start = pe.get_overlay_data_start_offset()
if overlay_start:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
overlay_size = file_size - overlay_start
if overlay_size > 1024:
alerts.append(
"INFO: Overlay de "
+ str(overlay_size)
+ " bytes detectado"
)
print("=== Alertas ===")
if alerts:
for alert in alerts:
print("[!]", alert)
else:
print("Sin alertas. Secciones dentro de parametros normales.")
return alerts
if __name__ == "__main__":
triage_sections(sys.argv[1])
Conclusion
Las secciones son la radiografia del PE. Su nombre, entropia, permisos y relacion de tamanos entre disco y memoria revelan si un binario esta empaquetado, tiene payloads ocultos o usa tecnicas de evasion. Dominar el analisis de secciones es el segundo paso fundamental (despues de las cabeceras) para cualquier analista de malware.
En el siguiente articulo examinaremos la Import Address Table, donde las funciones que importa un binario revelan sus intenciones reales.
Preguntas frecuentes
Libros recomendados
Artículos relacionados
Formato PE de Windows: Estructura Completa del Ejecutable
Entropia: Detectar Empaquetado y Cifrado en Binarios
Packing y Unpacking: UPX, Themida, VMProtect y Tecnicas
Import Address Table: APIs Sospechosas y Resolucion Dinamica
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.