Skip to content

Cheat sheet — Unpacking & Obfuscation

Companion to Module 09 — Unpacking & Obfuscation · CC BY 4.0 — print it, pin it, share it.

Last reviewed: 2026-07

Only handle live malware in an isolated, disposable analysis VM with no bridged network. A static upx -d doesn't run the sample — but manual/dynamic unpacking (below) does.

Spot the packer — entropy + section names

diec sample                            # Detect It Easy: names UPX / MPRESS / Themida
python3 - <<'PY'                        # per-section entropy — the packer signal
import pefile
pe = pefile.PE("sample.exe")
for s in pe.sections:
    print(s.Name.rstrip(b"\x00").decode(errors="replace"), f"{s.get_entropy():.2f}")
PY
  • Benign .text runs ~5–6 bits/byte; UPX pushes it past 7.5. That jump is the first signal. UPX also renames sections to UPX0 / UPX1 — trivial static detection.

UPX — the clean static unpack

upx -d sample -o sample.unpacked       # decompress (-d); write to a new file
upx -l sample                          # list: is it UPX, and what compression level
upx -t sample                          # test integrity of a packed file
# Verify by hash equivalence — evidence, not a sanity check:
sha256sum original sample.unpacked      # a UPX round-trip should restore identical bytes
  • Workflow: identify packer → apply static unpacker if one exists → verify by hash. Matching a known-clean corpus sample means the payload isn't novel; matching nothing means a new artifact worth full analysis. The hash goes in your YARA meta: and your case notes.

When upx -d fails — dump from memory

Most real packers have NO clean static unpacker. Next move:
  let the sample unpack ITSELF in a controlled dynamic run, then dump the
  reconstructed image from memory at the OEP (original entry point).
  → this is Module 10 territory; don't assume every sample yields to one command.

Decode obfuscated strings from a loader

The scripting-world cousin of packing: encoded blob → short decode loop → use (as a URL / registry key / filename). Recognise that shape and extract the IOC without reversing the rest.

# base64 layer
echo 'aHR0cDovL2V2aWwuZXhhbXBsZS9wYXk=' | base64 -d ; echo

# single-byte XOR — recover with the key you read from the loop:
python3 -c 'b=bytes.fromhex("2f2b3e..."); print(bytes(x^0x5a for x in b).decode())'

# nested (base64 of XOR of base64) — peel one layer, re-inspect, repeat
strings sample.exe | grep -iE 'frombase64|xor|-enc|char\['   # find the decode idiom

YARA — detect the packer

rule upx_packed {
    meta:
        attck = "T1027.002"            // software packing
    strings:
        $u0 = "UPX0"
        $u1 = "UPX1"
        $sig = "UPX!"                  // stub magic
    condition:
        uint16(0) == 0x5A4D and 2 of them
}
yara rule.yar packed_sample            # must fire on the PACKED binary
yara rule.yar sample.unpacked          # must stay QUIET on the unpacked one

Gotchas worth remembering

  • Hand a packed binary to a decompiler and you see the stub, not the payload — a few hundred instructions that decompress the real code and jump to it. Nothing meaningful until you defeat the packer.
  • A packer changes on-disk bytes only; runtime behaviour is unchanged — that's why the entropy jump is on disk and the OEP still runs the same code.
  • Hash equivalence is evidence, not a sanity check. Hash before packing and after unpacking; the match (or miss) is a finding that goes in the report.
  • > 7.0 entropy can be a legit installer or compressed asset — check .text specifically, and read it against the section that should hold code.
  • AI decodes a XOR/base64 blob fast but garbage-in/garbage-out — wrong key width or offset returns confident nonsense; sanity-check the output is a valid URL/filename before it becomes an IOC.

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).