IntermediopackingunpackingUPXThemidaVMProtectreverse engineering

Packing y Unpacking: UPX, Themida, VMProtect y Tecnicas

Guia completa sobre packing y unpacking de malware. UPX, Themida, VMProtect, ASPack, custom packers. Tecnicas de unpacking manual, busqueda del OEP, tail jump y herramientas especializadas.

MalwareIntel Research··11 min lectura
Serie: Análisis de Binarios — Parte 6

Por que el malware se empaqueta

Mas del 80% del malware en circulacion usa algun tipo de packing. Las razones son claras:

  1. Evasion de antivirus: Los motores de firmas no pueden detectar patrones en codigo cifrado o comprimido.
  2. Dificultar el analisis: El codigo real no es visible hasta que se ejecuta y desempaqueta en memoria.
  3. Reducir tamano: Packers como UPX reducen el tamano del ejecutable significativamente.
  4. Proteccion de IP: Protectores comerciales como Themida se usan para proteger software legitimo de pirateria, pero el malware abusa de la misma tecnologia.

Entender packing y unpacking es una habilidad fundamental. Sin desempaquetar, el analisis estatico es practicamente inutil: no se pueden ver las imports reales, los strings son ilegibles y el codigo esta ofuscado.

Como funciona un packer

El proceso de packing sigue un patron general:

Fase de empaquetado (offline):

  1. El packer lee el PE original completo.
  2. Comprime o cifra el codigo y datos.
  3. Genera un nuevo PE con un stub de desempaquetado.
  4. El contenido cifrado se almacena en una o mas secciones del nuevo PE.
  5. El entry point del nuevo PE apunta al stub.

Fase de desempaquetado (runtime):

  1. El stub se ejecuta primero.
  2. Descomprime o descifra el codigo original en memoria.
  3. Reconstruye la Import Address Table.
  4. Salta al OEP (Original Entry Point) del programa original.

Packers comunes y sus caracteristicas

UPX (Ultimate Packer for eXecutables)

El packer mas usado, open source. Solo comprime (no cifra) usando algoritmos como LZMA, NRV y UCL.

Deteccion:

  • Secciones nombradas UPX0, UPX1, UPX2
  • UPX0 tiene SizeOfRawData = 0 (se expande en runtime)
  • UPX1 tiene entropia alta (7.0+)
  • String "UPX!" al inicio de la seccion comprimida

Unpacking trivial:

upx -d packed_sample.exe -o unpacked_sample.exe

Nota: El malware a veces modifica los headers de UPX para que el comando upx -d falle. En ese caso, hay que reparar los magic bytes de UPX o usar unpacking manual.

ASPack

Packer comercial popular. Compresion razonable con poco overhead.

Deteccion:

  • Seccion .aspack
  • Entry point en seccion .aspack
  • Patron de codigo: PUSHAD al inicio del stub

Unpacking: Manual. Buscar el tail jump (JMP al OEP) despues del POPAD que restaura los registros.

Themida / WinLicense

Protector comercial avanzado de Oreans Technologies. Usa multiples capas de proteccion:

CapaTecnica
1Cifrado del codigo original
2Virtualizacion de codigo (VM interna)
3Anti-debugging (multiples tecnicas)
4Anti-VM y anti-sandbox
5Mutacion de codigo (cada copia es diferente)
6Integridad de memoria (detecta breakpoints)

Deteccion:

  • Seccion .themida
  • Tamano de la seccion .themida desproporcionado
  • Multiples secciones RWX
  • Imports minimas

Unpacking: Extremadamente dificil. Requiere scripts especializados para x64dbg, parcheado de anti-debug y mucha paciencia. Puede llevar horas o dias.

VMProtect

Virtualiza el codigo convirtiendolo en bytecode para una VM interna. El codigo original nunca existe como instrucciones x86/x64 en memoria.

Deteccion:

  • Secciones .vmp0, .vmp1
  • Entry point en seccion .vmp
  • Tamano del binario significativamente mayor que el original

Unpacking: No se puede "desempaquetar" en el sentido tradicional. El codigo esta transformado a un ISA (Instruction Set Architecture) custom. Se necesitan herramientas de devirtualizacion como:

  • Oreans UnVirtualizer (plugin IDA)
  • NoVMP (open source, soporte parcial)
  • Analisis manual de la VM handler table

Otros packers relevantes

PackerTipoDeteccionDificultad
PECompactCompresionSeccion .pec, PECompact2Baja
PetiteCompresionSeccion .petiteBaja
MPRESSCompresionSecciones .MPRESS1, .MPRESS2Baja
EnigmaProteccionSeccion .enigma1Alta
ObsidiumProteccionAnti-debug extensivoAlta
ConfuserEx.NET protectorMetadata ofuscadaMedia
.NET Reactor.NET protectorCodigo IL cifradoMedia

Custom packers: el desafio real

Las familias de malware sofisticadas usan packers propios que no tienen firma en ningun detector. Estos packers se identifican por comportamiento, no por nombre:

  • Entry point en seccion sin nombre o con nombre aleatorio
  • Entropia alta sin match de packer conocido
  • Imports minimas (solo GetProcAddress, VirtualAlloc, LoadLibrary)
  • Patron de PUSHAD/POPAD (guardar/restaurar registros) seguido de jump largo

Tecnicas de unpacking manual

Paso 1: Deteccion del packer

# Con Detect It Easy (die)
diec sample.exe

# Con PEiD (si disponible)
peid sample.exe

# Manual: verificar secciones y entropia
python3 -c "
import pefile
pe = pefile.PE('sample.exe')
for s in pe.sections:
    name = s.Name.decode().rstrip(chr(0))
    print(name, round(s.get_entropy(), 2), hex(s.SizeOfRawData), hex(s.Misc_VirtualSize))
"

Paso 2: Buscar el OEP

El OEP (Original Entry Point) es donde el codigo original comienza a ejecutarse despues de que el stub del packer termina su trabajo. Encontrarlo es el objetivo principal.

Tecnica 1: Tail Jump

El stub del packer siempre termina con un salto al OEP. Este salto final (tail jump) suele ser:

  • Un JMP largo a una direccion que esta en otra seccion
  • Un PUSH + RET (push de la direccion del OEP seguido de return)
  • Un CALL + JMP indirecto

En x64dbg, se puede buscar este patron poniendo un hardware breakpoint en ejecucion en la primera seccion (donde estara el codigo original) y dejando que el stub se ejecute.

Tecnica 2: PUSHAD/POPAD

Muchos packers empiezan con PUSHAD (guardar todos los registros) y terminan con POPAD (restaurarlos) antes del tail jump:

Entry Point (stub):
  PUSHAD                ; guarda registros originales
  ... (codigo del stub, desempaquetado)
  POPAD                 ; restaura registros
  JMP OEP               ; salta al programa original

En x64dbg: poner breakpoint en la instruccion PUSHAD, ejecutar, anotar el valor de ESP, poner hardware breakpoint on access en esa direccion de stack. Cuando para, el proximo JMP o RET lleva al OEP.

Tecnica 3: Breakpoints en APIs de memoria

El stub necesita desempaquetar codigo en memoria. Poner breakpoints en:

  • VirtualAlloc / VirtualAllocEx (reservar memoria)
  • VirtualProtect (cambiar permisos a ejecutable)
  • WriteProcessMemory (si se inyecta en otro proceso)

Cuando VirtualProtect cambia una region a ejecutable (PAGE_EXECUTE_READ), el codigo desempaquetado esta listo. La direccion de esa region suele contener el OEP.

Tecnica 4: Breakpoint en API conocida del programa original

Si se sabe que funcion llama el programa original (por ejemplo MessageBox si es una aplicacion GUI), poner breakpoint en MessageBoxA/W y ejecutar. Cuando para, examinar el call stack para encontrar el caller, que estara en el codigo desempaquetado.

Paso 3: Dump de memoria

Una vez localizado el OEP, hacer un dump de la memoria del proceso. Las herramientas comunes:

Scylla (x64dbg plugin):

  1. Con el proceso parado en el OEP, abrir Scylla.
  2. Verificar que el OEP detectado es correcto.
  3. Click en "IAT Autosearch" para localizar la Import Address Table.
  4. Click en "Get Imports" para reconstruir las imports.
  5. "Dump" para volcar la memoria a un archivo.
  6. "Fix Dump" para aplicar las imports reconstruidas al dump.

OllyDumpEx / OllyDump (OllyDbg plugin): Funcionalidad similar a Scylla pero para OllyDbg.

Paso 4: Reconstruccion de la IAT

El dump contiene el codigo desempaquetado, pero la IAT apunta a direcciones de memoria que ya no son validas fuera del proceso. Hay que reconstruirla:

  1. Scylla automatiza este proceso en la mayoria de los casos.
  2. Import REConstructor (ImpREC): Herramienta legacy pero todavia util.
  3. Manual: Identificar las direcciones en la IAT, determinar a que funcion corresponden y reconstruir la Import Directory.

Unpacking de UPX modificado

Cuando el malware modifica los headers de UPX para que upx -d falle:

upx -d sample.exe
# Error: CantUnpackException: file is possibly modified/hacked/protected

# Solucion 1: reparar el magic
# Buscar y restaurar los bytes "UPX!" en el header de la seccion

# Solucion 2: unpacking manual
# En x64dbg:
# 1. Breakpoint en el entry point
# 2. F9 para ejecutar el stub
# 3. El tail jump de UPX es siempre un JMP largo a la seccion UPX0
# 4. Dump con Scylla

Unpacking automatizado

Herramientas de unpacking automatico

HerramientaDescripcionTipo
UnipackerUnpacker automatico basado en emulacionOpen source
PE-sieveDetecta y dumpa modulos modificados en memoriaOpen source (hasherezade)
Mal-UnpackUnpacking via ejecucion controladaOpen source
ANY.RUNSandbox que permite dump post-ejecucionServicio online
Unpac.meServicio de unpacking automatizadoServicio online

Script de deteccion de packing

import pefile
import math

def detect_packing(filepath):
    pe = pefile.PE(filepath)
    indicators = []
    score = 0

    # Verificar entropia de secciones
    for section in pe.sections:
        name = section.Name.decode().rstrip("\x00")
        entropy = section.get_entropy()

        if entropy > 7.0:
            indicators.append(
                "Entropia alta en " + name + ": " + str(round(entropy, 2))
            )
            score += 3

    # Verificar imports minimas
    import_count = 0
    has_loadlibrary = False
    has_getprocaddress = False

    if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
        for entry in pe.DIRECTORY_ENTRY_IMPORT:
            for imp in entry.imports:
                import_count += 1
                if imp.name:
                    name = imp.name.decode()
                    if "LoadLibrary" in name:
                        has_loadlibrary = True
                    if "GetProcAddress" in name:
                        has_getprocaddress = True

    if import_count < 10:
        indicators.append(
            "Solo " + str(import_count) + " imports"
        )
        score += 2

    if has_loadlibrary and has_getprocaddress and import_count < 15:
        indicators.append("Solo LoadLibrary + GetProcAddress (resolucion dinamica)")
        score += 3

    # Verificar secciones RWX
    for section in pe.sections:
        chars = section.Characteristics
        if chars & 0xE0000000 == 0xE0000000:
            name = section.Name.decode().rstrip("\x00")
            indicators.append("Seccion " + name + " tiene permisos RWX")
            score += 2

    # Verificar nombres de secciones de packers conocidos
    known_packer_sections = [
        "UPX", ".aspack", ".adata", ".vmp", ".themida",
        ".enigma", ".petite", ".MPRESS", "pec", ".packed"
    ]
    for section in pe.sections:
        name = section.Name.decode().rstrip("\x00")
        for packer_name in known_packer_sections:
            if packer_name.lower() in name.lower():
                indicators.append(
                    "Seccion de packer conocido: " + name
                )
                score += 4

    # Verificar ratio VirtualSize / SizeOfRawData
    for section in pe.sections:
        name = section.Name.decode().rstrip("\x00")
        vs = section.Misc_VirtualSize
        raw = section.SizeOfRawData
        if raw > 0 and vs > raw * 5:
            indicators.append(
                name + " se expande " + str(round(vs/raw, 1))
                + "x en memoria"
            )
            score += 2
        if vs > 0 and raw == 0:
            indicators.append(
                name + " tiene tamano en disco = 0 pero VirtualSize = "
                + hex(vs)
            )
            score += 3

    # Resultado
    print("=== Packing Detection Report ===")
    print("File:", filepath)
    print()

    if score >= 6:
        print("VEREDICTO: Probablemente empaquetado (score: " + str(score) + ")")
    elif score >= 3:
        print("VEREDICTO: Posiblemente empaquetado (score: " + str(score) + ")")
    else:
        print("VEREDICTO: Probablemente no empaquetado (score: " + str(score) + ")")

    if indicators:
        print("\nIndicadores:")
        for ind in indicators:
            print("  [!] " + ind)

    return score, indicators

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

Estrategias ante protectores avanzados

Para protectores como Themida y VMProtect, el unpacking manual tradicional no funciona. Alternativas:

Analisis dinamico directo: En lugar de desempaquetar, ejecutar en sandbox y observar el comportamiento. Las llamadas a APIs, conexiones de red y archivos creados revelan la funcionalidad sin necesidad de ver el codigo.

Hooking de APIs: Instrumentar las APIs del sistema para capturar parametros y valores de retorno. Herramientas como API Monitor, Frida o DBI (Dynamic Binary Instrumentation) con Intel PIN.

Memory forensics: Ejecutar el malware, hacer un dump de toda la memoria del proceso y buscar el codigo desempaquetado con herramientas como pe-sieve o hollows-hunter.

Snapshots de VM: Tomar snapshots antes y despues de la ejecucion del stub, comparar la memoria para encontrar el codigo desempaquetado.

Conclusion

El packing es la primera barrera que un analista debe superar. Para packers simples como UPX, el proceso es trivial. Para packers comerciales como ASPack o PECompact, el unpacking manual con x64dbg y Scylla es el camino estandar. Para protectores avanzados como Themida y VMProtect, el analisis dinamico y el memory forensics son alternativas mas practicas que intentar un unpacking completo.

La clave es saber clasificar rapidamente el nivel de proteccion del binario para elegir la estrategia correcta: no tiene sentido pasar horas intentando desempaquetar Themida si un analisis dinamico en sandbox puede responder las preguntas del caso en minutos.

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.