Intermedioentropiapackingcifradodeteccionanalisis estaticoherramientas

Entropia: Detectar Empaquetado y Cifrado en Binarios

Uso de la entropia de Shannon para detectar empaquetado y cifrado en binarios. Calculo de entropia por seccion, visualizacion, umbrales de deteccion y analisis de anomalias en archivos PE y ELF.

MalwareIntel Research··10 min lectura
Serie: Análisis de Binarios — Parte 9

Entropia: el detector de contenido oculto

La entropia es la herramienta mas rapida para responder una pregunta fundamental: este binario esta empaquetado o tiene datos cifrados? Sin examinar instrucciones de ensamblador ni buscar firmas de packers, un calculo de entropia revela en segundos si el contenido de un binario es "normal" o no.

La idea es simple: el codigo compilado tiene patrones repetitivos (instrucciones comunes, secuencias de NOPs, alineaciones). Los datos comprimidos o cifrados eliminan esos patrones, produciendo datos que parecen aleatorios. La entropia mide exactamente eso: cuanta aleatoriedad hay en los datos.

Entropia de Shannon: la formula

Claude Shannon definio la entropia de informacion en 1948. Para una secuencia de bytes:

H = - SUM(p(i) * log2(p(i)))  para i = 0..255

Donde p(i) es la probabilidad de que un byte aleatorio del dato tenga el valor i.

Escala para bytes (0 a 8):

EntropiaSignificadoEjemplo
0.0Todos los bytes igualesArchivo lleno de 0x00
1.0 a 3.0Datos muy estructuradosTexto ASCII simple
3.0 a 5.0Datos moderadamente estructuradosTablas, datos numericos
5.0 a 6.5Codigo compilado normalSeccion .text de PE
6.5 a 7.0Codigo optimizado o datos mixtosSeccion .text densa
7.0 a 7.5Probablemente comprimidoUPX, LZMA, zlib
7.5 a 8.0Probablemente cifradoAES, RC4, XOR complejo
8.0Perfectamente aleatorioCSPRNG (teorico)

Implementacion en Python

import math

def shannon_entropy(data):
    """Calcula la entropia de Shannon para una secuencia de bytes."""
    if not data:
        return 0.0

    # Contar frecuencia de cada byte
    freq = dict()
    for byte in data:
        freq[byte] = freq.get(byte, 0) + 1

    length = len(data)
    entropy = 0.0

    for count in freq.values():
        if count > 0:
            p = count / length
            entropy -= p * math.log2(p)

    return entropy


# Ejemplo: entropia de diferentes tipos de datos
texto = b"Hello World! This is a normal text string with repetitive patterns."
print("Texto ASCII:", round(shannon_entropy(texto), 2))
# ~4.1

codigo = bytes(range(256)) * 10  # Distribucion uniforme
print("Uniforme:", round(shannon_entropy(codigo), 2))
# ~8.0

ceros = b"\x00" * 1000
print("Ceros:", round(shannon_entropy(ceros), 2))
# 0.0

Entropia por seccion en PE

Analizar la entropia del archivo completo no es informativo: un PE con una seccion .text normal y un recurso cifrado tendra una entropia global media. Lo relevante es la entropia de cada seccion individual:

import pefile

pe = pefile.PE("sample.exe")

print("Seccion          Entropia  Tamano      Veredicto")
print("-" * 60)

for section in pe.sections:
    name = section.Name.decode().rstrip("\x00").ljust(16)
    data = section.get_data()
    entropy = round(shannon_entropy(data), 2)
    size = len(data)

    if entropy > 7.5:
        verdict = "CIFRADO"
    elif entropy > 7.0:
        verdict = "COMPRIMIDO"
    elif entropy > 6.5:
        verdict = "DENSO"
    elif entropy < 1.0 and size > 100:
        verdict = "VACIO"
    else:
        verdict = "NORMAL"

    print(name, str(entropy).ljust(9), str(size).ljust(11), verdict)

Perfiles de entropia por tipo de seccion

SeccionEntropia normalSospechoso si
.text5.5 a 6.8Mayor de 7.0 (empaquetado)
.data1.0 a 5.0Mayor de 6.5 (datos cifrados)
.rdata3.0 a 5.5Mayor de 7.0 (datos ocultos)
.rsrc2.0 a 6.0Mayor de 7.0 (payload en recursos)
.reloc4.0 a 5.5Mayor de 7.0 (datos ocultos en relocs)

Excepciones legitimas:

  • .rsrc puede tener entropia alta si contiene imagenes PNG/JPEG (ya comprimidas).
  • Binarios firmados tienen datos de certificado con entropia alta.
  • Recursos que embeben archivos ZIP tienen entropia natural alta.

Entropia por bloques: visualizacion granular

En lugar de calcular una sola entropia por seccion, dividir los datos en bloques de tamano fijo y calcular la entropia de cada uno. Esto produce un perfil de entropia del archivo completo:

import math

def block_entropy(data, block_size=256):
    """Calcula entropia por bloques, retorna lista de valores."""
    entropies = []
    for i in range(0, len(data), block_size):
        block = data[i:i+block_size]
        if len(block) >= block_size // 2:  # ignorar bloques muy pequenos
            entropies.append(shannon_entropy(block))
    return entropies

def entropy_profile(filepath, block_size=256):
    """Genera perfil de entropia de un archivo completo."""
    with open(filepath, "rb") as f:
        data = f.read()

    entropies = block_entropy(data, block_size)

    # Estadisticas
    avg = sum(entropies) / len(entropies)
    max_e = max(entropies)
    min_e = min(entropies)

    # Contar bloques por rango
    high = sum(1 for e in entropies if e > 7.0)
    medium = sum(1 for e in entropies if 5.0 <= e <= 7.0)
    low = sum(1 for e in entropies if e < 5.0)

    print("=== Entropy Profile ===")
    print("Bloques:", len(entropies))
    print("Media:", round(avg, 2))
    print("Min:", round(min_e, 2), "Max:", round(max_e, 2))
    print()
    print("Distribucion:")
    print("  Alta (>7.0):  ", high, "bloques",
          "(" + str(round(high/len(entropies)*100)) + "%)")
    print("  Media (5-7):  ", medium, "bloques")
    print("  Baja (<5.0):  ", low, "bloques")

    if high / len(entropies) > 0.5:
        print("\n[!] Mas del 50% del archivo tiene entropia alta")
        print("    Probable empaquetado completo")
    elif high / len(entropies) > 0.1:
        print("\n[!] Bloques de alta entropia detectados")
        print("    Posible payload cifrado embebido")

    return entropies

Visualizacion de entropia

La visualizacion de entropia como grafico permite identificar patrones rapidamente:

def print_entropy_bar(filepath, block_size=1024):
    """Imprime un grafico de barras ASCII de entropia."""
    with open(filepath, "rb") as f:
        data = f.read()

    entropies = block_entropy(data, block_size)

    print("=== Entropy Visualization ===")
    print("Cada linea = " + str(block_size) + " bytes")
    print("| = 0.5 unidades de entropia\n")

    for i, e in enumerate(entropies):
        offset = hex(i * block_size).ljust(10)
        bar_len = int(e * 4)  # escala: 8.0 -> 32 caracteres
        bar = "|" * bar_len

        # Color segun nivel
        if e > 7.0:
            label = " [!]"
        elif e > 6.0:
            label = ""
        elif e < 2.0:
            label = " [empty]"
        else:
            label = ""

        print(offset + str(round(e, 1)).ljust(5) + bar + label)

Herramientas con visualizacion grafica

binvis.io: Herramienta web que genera visualizaciones de entropia de archivos binarios con colores. Permite identificar regiones cifradas, comprimidas y de codigo de un vistazo.

Detect It Easy (DIE): Incluye un grafico de entropia por seccion integrado.

PE-bear: Muestra la entropia de cada seccion en su interfaz grafica.

binwalk -E: Genera un grafico de entropia en la terminal o exporta datos para gnuplot.

binwalk -E sample.exe
# Genera grafico de entropia del archivo completo

Chi-cuadrado: test complementario

La entropia de Shannon no distingue entre datos comprimidos y datos cifrados (ambos tienen entropia alta). El test chi-cuadrado complementa midiendo cuanto se desvian las frecuencias de bytes de una distribucion uniforme:

def chi_squared(data):
    """Test chi-cuadrado para uniformidad de bytes."""
    if not data:
        return 0.0

    expected = len(data) / 256.0
    freq = [0] * 256
    for byte in data:
        freq[byte] += 1

    chi2 = 0.0
    for count in freq:
        diff = count - expected
        chi2 += (diff * diff) / expected

    return chi2

# Interpretacion:
# Datos aleatorios (cifrado): chi2 ~256 (cerca del valor esperado)
# Datos comprimidos: chi2 mayor (distribucion no uniforme)
# Datos estructurados: chi2 mucho mayor
Chi-cuadradoInterpretacion
200 a 300Datos aleatorios (cifrado fuerte)
300 a 500Compresion o cifrado debil
500+Datos estructurados

Combinando entropia y chi-cuadrado:

EntropiaChi-cuadradoInterpretacion
Alta (mayor de 7.5)Bajo (200-300)Cifrado (AES, RC4)
Alta (mayor de 7.0)Medio (300-500)Compresion (zlib, LZMA)
Media (5-7)Alto (500+)Codigo compilado normal
Baja (menor de 3)Muy alto (1000+)Texto o datos repetitivos

Casos practicos de deteccion por entropia

Caso 1: PE empaquetado con UPX

.UPX0   Entropia: 0.00  [VACIO - se llena en runtime]
.UPX1   Entropia: 7.82  [COMPRIMIDO - contenido UPX]
.UPX2   Entropia: 4.21  [NORMAL - import data]

La seccion UPX1 tiene toda la carga comprimida. UPX0 tiene entropia 0 porque esta vacia en disco y se llena con el codigo descomprimido en runtime.

Caso 2: Dropper con payload en .rsrc

.text   Entropia: 6.12  [NORMAL]
.rdata  Entropia: 4.53  [NORMAL]
.data   Entropia: 3.21  [NORMAL]
.rsrc   Entropia: 7.91  [CIFRADO - payload AES]
.reloc  Entropia: 5.02  [NORMAL]

Todo normal excepto .rsrc, que contiene un PE cifrado como recurso RT_RCDATA.

Caso 3: Malware con strings cifradas

.text   Entropia: 6.45  [NORMAL - codigo]
.rdata  Entropia: 6.87  [ELEVADO - strings cifradas entre imports]
.data   Entropia: 7.12  [ALTO - configuracion cifrada]
.rsrc   Entropia: 3.45  [NORMAL - icono y manifest]

La seccion .data tiene entropia alta porque la configuracion de C2 esta cifrada. La seccion .rdata tiene entropia elevada por los strings cifrados mezclados con la tabla de imports.

Caso 4: Binario .NET ofuscado

.text   Entropia: 7.34  [ALTO - IL code ofuscado con ConfuserEx]
.rsrc   Entropia: 5.21  [NORMAL]
.reloc  Entropia: 4.89  [NORMAL]

ConfuserEx cifra el IL code, produciendo entropia alta en .text aunque tecnicamente no es un packer sino un protector .NET.

Limitaciones de la entropia como detector

La entropia es un indicador poderoso pero no perfecto:

Falsos positivos:

  • Binarios que embeben imagenes JPEG/PNG (compresion nativa).
  • Instaladores que contienen archivos comprimidos (NSIS, Inno Setup).
  • Binarios con firma digital (bloque de certificado tiene entropia alta).
  • Datos multimedia embebidos (audio, video).

Falsos negativos:

  • XOR con clave de un byte no cambia la entropia significativamente.
  • Cifrado por sustitucion simple (Caesar, ROT13) mantiene la distribucion de frecuencias.
  • Codigo polimófico que inserta NOP sleds puede tener entropia baja a pesar de ser malicioso.

Mitigacion: Nunca usar entropia como unico indicador. Combinar con:

  • Analisis de imports (pocas imports = probable packing).
  • Deteccion de packer (DIE, PEiD).
  • Nombres de secciones.
  • Ratio VirtualSize / SizeOfRawData.

Script de triaje por entropia

import pefile
import math
import sys
import os

def full_entropy_triage(filepath):
    """Triaje completo basado en entropia."""
    with open(filepath, "rb") as f:
        raw = f.read()

    file_entropy = shannon_entropy(raw)

    print("=== Entropy Triage Report ===")
    print("File:", filepath)
    print("Size:", os.path.getsize(filepath), "bytes")
    print("File entropy:", round(file_entropy, 2))
    print()

    try:
        pe = pefile.PE(filepath)
    except Exception:
        print("No es un PE. Entropia global solo.")
        return

    # Entropia por seccion
    alerts = []
    packed_score = 0

    for section in pe.sections:
        name = section.Name.decode().rstrip("\x00")
        data = section.get_data()
        entropy = shannon_entropy(data)
        size = len(data)

        if entropy > 7.5:
            alerts.append("CRITICO: " + name + " entropia " + str(round(entropy, 2)) + " (cifrado)")
            packed_score += 4
        elif entropy > 7.0:
            alerts.append("ALTO: " + name + " entropia " + str(round(entropy, 2)) + " (comprimido)")
            packed_score += 3
        elif entropy > 6.5 and name in [".data", ".rsrc"]:
            alerts.append("MEDIO: " + name + " entropia " + str(round(entropy, 2)) + " (sospechoso para esta seccion)")
            packed_score += 1

        if section.Misc_VirtualSize > 0 and section.SizeOfRawData == 0:
            alerts.append("ALTO: " + name + " se expande desde 0 bytes en runtime")
            packed_score += 3

    # Overlay
    overlay_offset = pe.get_overlay_data_start_offset()
    if overlay_offset:
        overlay_data = raw[overlay_offset:]
        overlay_entropy = shannon_entropy(overlay_data)
        if overlay_entropy > 7.0:
            alerts.append("MEDIO: Overlay de " + str(len(overlay_data)) + " bytes con entropia " + str(round(overlay_entropy, 2)))
            packed_score += 1

    # Resultado
    if alerts:
        print("Alertas:")
        for alert in alerts:
            print("  [!] " + alert)
    else:
        print("Sin alertas de entropia.")

    print()
    if packed_score >= 6:
        print("VEREDICTO: Binario empaquetado o con datos cifrados significativos")
    elif packed_score >= 3:
        print("VEREDICTO: Posible empaquetado parcial o payload embebido")
    else:
        print("VEREDICTO: Entropia dentro de parametros normales")

if __name__ == "__main__":
    full_entropy_triage(sys.argv[1])

Conclusion

La entropia de Shannon es una de las herramientas mas rapidas y universales para detectar contenido oculto en binarios. Un calculo que toma milisegundos revela si un PE esta empaquetado, si tiene payloads cifrados en sus recursos, o si sus strings estan ofuscadas. Combinada con el analisis de secciones y la deteccion de packers, la entropia forma parte del triaje inicial que todo analista aplica antes de invertir tiempo en reverse engineering profundo.

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.