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.
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):
| Entropia | Significado | Ejemplo |
|---|---|---|
| 0.0 | Todos los bytes iguales | Archivo lleno de 0x00 |
| 1.0 a 3.0 | Datos muy estructurados | Texto ASCII simple |
| 3.0 a 5.0 | Datos moderadamente estructurados | Tablas, datos numericos |
| 5.0 a 6.5 | Codigo compilado normal | Seccion .text de PE |
| 6.5 a 7.0 | Codigo optimizado o datos mixtos | Seccion .text densa |
| 7.0 a 7.5 | Probablemente comprimido | UPX, LZMA, zlib |
| 7.5 a 8.0 | Probablemente cifrado | AES, RC4, XOR complejo |
| 8.0 | Perfectamente aleatorio | CSPRNG (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
| Seccion | Entropia normal | Sospechoso si |
|---|---|---|
| .text | 5.5 a 6.8 | Mayor de 7.0 (empaquetado) |
| .data | 1.0 a 5.0 | Mayor de 6.5 (datos cifrados) |
| .rdata | 3.0 a 5.5 | Mayor de 7.0 (datos ocultos) |
| .rsrc | 2.0 a 6.0 | Mayor de 7.0 (payload en recursos) |
| .reloc | 4.0 a 5.5 | Mayor 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-cuadrado | Interpretacion |
|---|---|
| 200 a 300 | Datos aleatorios (cifrado fuerte) |
| 300 a 500 | Compresion o cifrado debil |
| 500+ | Datos estructurados |
Combinando entropia y chi-cuadrado:
| Entropia | Chi-cuadrado | Interpretacion |
|---|---|---|
| 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
Libros recomendados
Artículos relacionados
Packing y Unpacking: UPX, Themida, VMProtect y Tecnicas
Secciones PE: .text, .data, .rsrc, .reloc y Anomalias
Analisis Estatico Basico: Strings, Hashes y Metadatos
Formato PE de Windows: Estructura Completa del Ejecutable
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.