Packer Detection & Unpacking

Overview

Packers compress or encrypt executable code to reduce file size, obfuscate functionality, and evade signature-based detection. Packed malware must unpack itself at runtime before executing its real payload. Detecting packers and unpacking samples is often required before meaningful static analysis can proceed.

How Packers Work

         Original binary
                │
                ▼
       ┌──────────────────┐
       │   Packer Tool    │  Compresses/encrypts original code + data
       └──────────────────┘
                │
                ▼
┌──────────────────────────────────┐
│   Packed Binary                  │
│  ┌───────────────────────────┐   │
│  │  Unpacking stub           │   │  ← Entry point runs this first
│  ├───────────────────────────┤   │
│  │  Compressed/encrypted     │   │  ← Original code (packed)
│  │  original code + data     │   │
│  └───────────────────────────┘   │
└──────────────────────────────────┘
                │
                ▼ (at runtime)
┌──────────────────────────────────┐
│  1. Stub allocates memory        │
│  2. Stub decompresses/decrypts   │
│  3. Stub fixes imports           │
│  4. Stub jumps to OEP            │  (Original Entry Point)
└──────────────────────────────────┘

Packer Detection

Detect It Easy (DIE)

# Detect It Easy
# https://github.com/horsicq/Detect-It-Easy

# Standard scan
diec sample.exe

# Deep scan (more thorough detection)
diec -d sample.exe

# Heuristic scan (detect unknown packers)
diec -u sample.exe

# Aggressive scan
diec -g sample.exe

# Show entropy (high entropy = likely packed)
diec -e sample.exe

# JSON output for scripting
diec -j sample.exe

# Recursive scan (archives)
diec -r archive.zip

DIE identifies specific packers (UPX, Themida, VMProtect, ASPack, etc.), compilers (MSVC, GCC, Delphi, Go), and linkers.

Entropy Analysis

Entropy measures the randomness of data. Compressed or encrypted data has high entropy (close to 8.0 for byte-level), while normal code and data typically range between 4.0 and 6.5.

# radare2
# https://github.com/radareorg/radare2

# Show section layout (size, virtual size, permissions, type, name)
rabin2 -S sample.exe
# rabin2 -S does not display per-section entropy; use the pefile Python snippet below
# pefile
# https://github.com/erocarrera/pefile
import pefile
import math

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

for section in pe.sections:
    name = section.Name.decode().rstrip('\x00')
    entropy = section.get_entropy()
    status = "SUSPICIOUS" if entropy > 7.0 else "normal"
    print(f"{name:10s}  entropy: {entropy:.2f}  [{status}]")
Entropy Range Interpretation
0.0 - 1.0 Very uniform data (null padding, repeated patterns)
4.0 - 6.5 Normal compiled code and data
6.5 - 7.0 Possibly compressed or encoded
7.0 - 8.0 Almost certainly compressed or encrypted (packed)

Section-Based Indicators

# pefile
# https://github.com/erocarrera/pefile
import pefile

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

for section in pe.sections:
    name = section.Name.decode().rstrip('\x00')
    vsize = section.Misc_VirtualSize
    rsize = section.SizeOfRawData
    entropy = section.get_entropy()
    chars = section.Characteristics

    flags = []
    if entropy > 7.0:
        flags.append("HIGH_ENTROPY")
    if rsize == 0 and vsize > 0:
        flags.append("EMPTY_RAW")
    if vsize > rsize * 5 and rsize > 0:
        flags.append("VSIZE>>RSIZE")
    if chars & 0x80000000 and chars & 0x20000000:
        flags.append("WRITE+EXEC")

    print(f"{name:10s}  VSize: {vsize:8d}  RSize: {rsize:8d}  "
          f"Entropy: {entropy:.2f}  {' '.join(flags)}")
Indicator Meaning
High entropy section Compressed or encrypted content
Raw size = 0, virtual size > 0 Section populated at runtime (unpacking buffer)
Virtual size >> raw size Section will expand at runtime
Write + execute section Self-modifying code (unpacking stub)
Very few imports Imports resolved dynamically after unpacking
Entry point in non-.text section Entry in packer stub section

Common Packer Section Names

Section Name Packer
UPX0, UPX1, UPX2 UPX
.themida Themida
.vmp0, .vmp1 VMProtect
.aspack, .adata ASPack
.petite Petite
.nsp0, .nsp1 NSPack
.enigma1, .enigma2 Enigma Protector
.MPRESS1, .MPRESS2 MPRESS

Import Table Indicators

# pefile
# https://github.com/erocarrera/pefile
import pefile

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

total_imports = 0
dll_count = 0
has_loadlib = False
has_getproc = False

if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        dll_count += 1
        for imp in entry.imports:
            total_imports += 1
            if imp.name:
                name = imp.name.decode()
                if 'LoadLibrary' in name:
                    has_loadlib = True
                if 'GetProcAddress' in name:
                    has_getproc = True

print(f"DLLs: {dll_count}, Total imports: {total_imports}")
print(f"LoadLibrary: {has_loadlib}, GetProcAddress: {has_getproc}")

if total_imports < 10 and has_loadlib and has_getproc:
    print("WARNING: Likely packed — minimal imports with dynamic resolution")

Unpacking Techniques

UPX Unpacking

UPX is the most common packer. It has a built-in decompression flag.

# UPX
# https://github.com/upx/upx

# Check if file is UPX-packed
upx -t sample.exe

# List compression info
upx -l sample.exe

# Decompress (unpack)
upx -d sample.exe

# Decompress to a different file
upx -d sample.exe -o unpacked.exe

# Force decompression (for modified UPX headers)
upx -d -f sample.exe -o unpacked.exe

Some malware modifies UPX headers (e.g., changes section names from UPX0/UPX1) to prevent upx -d from working. In that case, restore the original section names or use dynamic unpacking.

Manual Unpacking (General Approach)

When automated tools fail, manual unpacking with a debugger follows this general process:

1. Load packed binary in debugger
2. Set breakpoints on:
   - VirtualAlloc / VirtualProtect (memory allocation for unpacked code)
   - Tail jumps (JMP to distant address = jump to OEP)
3. Run until unpacking stub finishes
4. Identify the Original Entry Point (OEP)
5. Dump the process memory
6. Fix the import table (IAT)

Dynamic Unpacking with radare2

# radare2
# https://github.com/radareorg/radare2

# Open in debug mode
r2 -d packed_sample.exe

# Analyze
aa

# Set breakpoint on VirtualProtect
db sym.imp.kernel32.dll_VirtualProtect

# Continue execution
dc

# After break, examine registers and step
dr
ds

# When OEP is reached, dump memory
# Seek to image base, then write memory to file
s 0x400000
wtf unpacked_dump.bin 0x10000

Dumping with GDB (Linux)

# GDB
# https://www.sourceware.org/gdb/

# Debug the packed binary
gdb ./packed_sample

# Set breakpoint on mprotect (used to make unpacked code executable)
b mprotect

# Run
r

# After breaking, examine memory maps
info proc mappings

# When OEP is reached, dump memory
dump binary memory unpacked.bin 0x400000 0x410000

Fixing Imports After Dumping

After dumping an unpacked binary from memory, the import table (IAT) usually needs to be reconstructed because it contains runtime addresses rather than the original import descriptors. Tools for IAT reconstruction include:

  • Scylla (Windows) — GUI tool that attaches to a running process, locates the IAT, and rebuilds import tables
  • Impscan (Volatility plugin) — reconstructs imports from memory dumps
  • PE-bear (Windows) — PE editor with import table repair capabilities

Identifying Unknown Packers

When DIE does not identify the packer:

  1. Check section names — custom or unusual names suggest a packer
  2. Check entropy — high entropy across multiple sections indicates packing
  3. Check imports — very few imports with LoadLibrary/GetProcAddress
  4. Check entry point — entry in a non-.text section
  5. Look for unpacking patterns in the entry point code:
  6. Loops with XOR operations (decryption)
  7. Calls to VirtualAlloc / VirtualProtect
  8. Large JMP to a distant address (jump to OEP)
# radare2
# https://github.com/radareorg/radare2

# Disassemble entry point to look for unpacking patterns
r2 -A sample.exe -c 'pdf @ entry0' -q

Common Packers and Protectors

Packer Type Notes
UPX Compressor Most common, easily unpacked with upx -d
MPRESS Compressor Similar to UPX, less common
ASPack Compressor Older packer, still seen in legacy malware
Themida Protector Anti-debug, anti-VM, code virtualization
VMProtect Protector Converts code to virtual machine bytecode
Enigma Protector Protector Licensing + anti-analysis
.NET Reactor .NET Protector Obfuscates .NET assemblies
ConfuserEx .NET Obfuscator Open-source .NET obfuscation

References

Tools

Further Reading