IntermedioPEWindowsseccionesentropiaanalisis estatico

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.

MalwareIntel Research··13 min lectura
Serie: Análisis de Binarios — Parte 2

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:

CampoTamanoRelevancia
Name8 bytesNombre de la seccion (puede ser arbitrario)
VirtualSize4 bytesTamano cuando se carga en memoria
VirtualAddress4 bytesRVA donde empieza en memoria
SizeOfRawData4 bytesTamano en el archivo (disco)
PointerToRawData4 bytesOffset en el archivo
Characteristics4 bytesPermisos 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:

AnomaliaIndicadorCausa probable
Entropia mayor de 7.0Contenido cifrado o comprimidoPacker o protector
Permisos RWXCodigo auto-modificableDesempaquetado runtime
VirtualSize mucho mayor que SizeOfRawDataSe expande en memoriaPacker descomprime en .text
Entry point fuera de .textEjecucion empieza en otra seccionPacker, loader custom
.text ausente o renombradaNo existe seccion con nombre .textCompilador 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:

SeccionContenidoDiferencia con .data
.dataVariables inicializadasOcupa espacio en disco
.bssVariables no inicializadasSolo reserva espacio en memoria
.rdataDatos de solo lecturaSin 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:

TecnicaDescripcionEjemplo
Payload cifrado en RT_RCDATAPE o shellcode cifrado como recursoEmotet stages 2-3
Configuracion de C2URLs, IPs y claves en recurso customAgent Tesla configs
PE embebido como recursoEjecutable completo dentro de .rsrcDroppers genericos
Icono imitando app legitimaIcono de Word, Chrome, PDFPhishing executables
Manifest con elevacionrequestedExecutionLevel=requireAdministratorUAC 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 seccionHerramientaNotas
UPX0, UPX1UPXPacker open source mas comun
.aspackASPackPacker comercial
.adataASProtectProtector comercial
.themidaThemidaProtector avanzado
.vmp0, .vmp1VMProtectVirtualizacion de codigo
.ndataNSIS InstallerInstalador Nullsoft
.enigma1Enigma ProtectorProtector comercial
.petitePetitePacker antiguo
CODEDelphi/BorlandCompilador Delphi
.textbssMinGW/GCCCompilador GCC para Windows
.CRTMSVCC 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 entropiaInterpretacionEjemplo
0.0 a 1.0Datos uniformesSeccion llena de ceros
1.0 a 4.0Datos estructuradosTexto ASCII, tablas
4.0 a 6.0Codigo compiladoSeccion .text normal
6.0 a 7.0Datos densosCodigo optimizado, datos mixtos
7.0 a 7.5Probablemente comprimidoUPX, LZMA
7.5 a 8.0Probablemente cifradoAES, 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:

PermisoFlagValor
ExecuteIMAGE_SCN_MEM_EXECUTE0x20000000
ReadIMAGE_SCN_MEM_READ0x40000000
WriteIMAGE_SCN_MEM_WRITE0x80000000

Combinaciones normales vs sospechosas:

CombinacionNormalSospechoso
R (solo lectura).rdata, .rsrcNo
RW (lectura+escritura).data, .bssNo
RX (lectura+ejecucion).textNo
RWX (todo)RaroSi, codigo auto-modificable
WX (escritura+ejecucion sin lectura)Nunca legitimoSi, altamente sospechoso
Sin permisosNuncaSi, 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

Artículos relacionados

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.