Unpacking and Deobfuscation
Definition
Section titled “Definition”Unpacking is the process of recovering the original executable code and data from a binary that has been transformed by a packer — a tool or routine that compresses and/or encrypts the payload and wraps it in a small loader stub that restores everything at runtime. Deobfuscation is the complementary discipline of simplifying deliberately obfuscated logic — encrypted string tables, API resolution by hash, control-flow flattening, junk code insertion — back into a form a human can reason about. The moment the stub finishes running in memory, the original program exists once, briefly, in a recoverable state; unpacking is the craft of capturing that state. Both skills sit at the front of every serious malware analysis engagement, because until the payload is unpacked there is literally nothing to analyze: no strings, no imports, no readable code. Analysts who skip this stage end up writing YARA rules on the packed wrapper, which matches thousands of legitimate utilities and burns detection budget.
Why It Matters
Section titled “Why It Matters”Commercial telemetry consistently reports that a large share of in-the-wild malware arrives packed or obfuscated in some form [verify — exact percentages vary by vendor and year]. UPX alone appears on enormous volumes of commodity loaders, while custom crypters protect most ransomware and stealer builds. If you cannot unpack, you cannot extract the configuration block from a stealer, you cannot recover the ransom note template and wallet address from a locker, and you cannot write a YARA rule that survives the next recompiled build. Unpacking is also the bridge between the two halves of this module series: static analysis (M1) needs a clean binary, and dynamic analysis (M14) depends on knowing what the sample became after its stub ran. On the defensive side, EDRs see the unpacking event itself — a section of memory becoming executable and then executing — which is one of the highest-fidelity generic detections available, as covered in E9 and E11. Understanding the unpacking moment from both sides makes you better at the analysis and better at the detection engineering around it.
Detailed Explanation
Section titled “Detailed Explanation”A packed PE has its original sections compressed or encrypted and a new, small executable section (the stub) prepended or placed first. The entry point points into the stub. At runtime the stub allocates memory, decrypts the payload into it, repairs the import address table (IAT), fixes relocations, transfers control to the original entry point (OEP), and often erases its own traces. The classic tell of a packed file is the combination of high-entropy sections, a tiny code section, few imports (often just LoadLibraryA, GetProcAddress, VirtualAlloc, VirtualProtect, ExitProcess), and section names like UPX0/UPX1 or nonsense strings.
PACKED FILE ON DISK RUNTIME (after stub executes)+---------------------+ +---------------------+| PE header (EP→stub) | | PE header (restored)|+---------------------+ +---------------------+| UPX0 (empty, RW) | <-- grows --> | original .text | decrypted+---------------------+ +---------------------+| UPX1 (RX) | | original .data || [compressed body] | +---------------------+| [stub code] | | original .rsrc |+---------------------+ +---------------------+ entropy ~7.9 entropy ~5.5
Stub lifecycle: entry → VirtualAlloc(RWX) → decrypt loop → IAT rebuild → resolve OEP → VirtualProtect(RX) → jmp OEPThe tail jump is the most reliable static anchor: the last executable instruction of the last executable section is usually a jmp (E9/FF25) or a call that lands in the decompressed region or in VirtualAlloc-returned memory. Analysts set a memory breakpoint on execute at the section boundary and let the stub run once; when the breakpoint fires, the instruction pointer is at or within a few instructions of the OEP. Tools like x64dbg + Scylla then dump the process and rebuild the IAT.
Deobfuscation targets three recurring layers. String obfuscation: strings live XOR/Base64-encrypted in a data blob and are decrypted on demand — recover the key (often a single byte or a short stack-built buffer) and batch-decrypt. API hashing: instead of importing VirtualAlloc, the code computes CRC32/ROR13/djb2 over each export name in kernel32.dll at runtime and compares hashes — recover the algorithm and precompute a hash→name map. Control-flow flattening: a switch dispatcher inside a loop drives every basic block through a state variable, destroying the readable graph — deflating it requires symbolic execution (angr) or careful manual tracing. ConfuserEx does this to .NET; Obfuscator-LLVM (ollvm) does it to native C/C++.
Common Techniques
Section titled “Common Techniques”| Technique | What it looks like | Typical tools | Notes |
|---|---|---|---|
| UPX packing | UPX0/UPX1 sections, “UPX!” magic | upx -d, PE-bear | Trivially reversible; still used at volume for malspam loaders |
| Custom crypter stubs | High entropy, 3-5 imports, no section names | x64dbg, Scylla, FLOSS | The bread-and-butter of commodity loaders |
| VMProtect | Virtualized handlers, .vmp0/.vmp1 sections | Manual RE, TitanEngine | Only hot paths virtualized; unpack the rest |
| Themida / WinLicense | Anti-dump, anti-VM, mutating stub | x64dbg plugins, Scout [verify] | Adds anti-debug layers — see E15 |
| .NET ConfuserEx | Control-flow flattening, constant/string encryption | de4dot, dnSpy | Rule sets live in repo config [verify availability] |
| Single-byte XOR strings | Repeating-char bias in blobs | xortool, FLOSS | Fastest string recovery win |
| API hashing (ROR13/CRC32) | No imports; hash constants near resolver code | hashdb, pe-tree, IDA plugins | Rebuild hash→API map offline |
| Control-flow flattening | One dispatcher loop, state variable | angr, D-810, manual | Costly; only flatten what matters |
| Anti-dump tricks | Wiped PE header, wrong SizeOfImage at dump time | Scylla, peclone | Defeats naive MiniDumpWriteDump |
| Nested packing (UPX→crypter) | Unpack once, still entropy 7.9 | Iterate until entropy drops | Always re-scan after each unpack |
Full Working Code
Section titled “Full Working Code”packed_hunter.py — triages one PE file: per-section entropy, UPX signature detection, tail-jump scan on the last executable section, import-count heuristic, and a packed/not-packed verdict. Dependency: pip install pefile. Runs on any Windows PE including .NET.
#!/usr/bin/env python3"""packed_hunter.py — static packer triage for PE files."""import sys, math, structimport pefile
def shannon_entropy(data: bytes) -> float: """Classic Shannon entropy over byte histogram, 0.0 - 8.0 scale.""" if not data: return 0.0 freq = [0] * 256 for b in data: freq[b] += 1 ent = 0.0 n = len(data) for c in freq: if c: p = c / n ent -= p * math.log2(p) return ent
def section_verdicts(pe): out = [] for s in pe.sections: name = s.Name.rstrip(b"\x00").decode(errors="replace") raw = s.get_data() ent = shannon_entropy(raw) flags = [] if ent > 7.2: flags.append("HIGH-ENTROPY") if not raw and s.Misc_VirtualSize: flags.append("VIRTUAL-ONLY(loaded at runtime)") if name.upper().startswith("UPX"): flags.append("UPX-NAME") # executable? exe = bool(s.Characteristics & 0x20000000) out.append({ "name": name, "entropy": round(ent, 2), "virtual_size": s.Misc_VirtualSize, "raw_size": len(raw), "executable": exe, "flags": flags, }) return out
def find_tail_jump(pe): """Scan last bytes of last executable section for E9/E8/FF25 pattern.""" exec_secs = [s for s in pe.sections if s.Characteristics & 0x20000000 and s.get_data()] if not exec_secs: return None last = exec_secs[-1] data = last.get_data() scan = data[-64:] if len(data) > 64 else data results = [] for i in range(len(scan) - 5): b = scan[i] if b == 0xE9 or b == 0xE8: # jmp/call rel32 rel = struct.unpack("<i", scan[i+1:i+5])[0] results.append({"off_in_sec": len(data) - len(scan) + i, "type": "jmp rel32" if b == 0xE9 else "call rel32", "target_rva": last.VirtualAddress + len(data) - len(scan) + i + 5 + rel}) elif b == 0xFF and i + 5 < len(scan) and scan[i+1] == 0x25: results.append({"off_in_sec": len(data) - len(scan) + i, "type": "jmp [imm32]", "target_rva": None}) return results or None
def import_profile(pe): try: imports = list(pe.DIRECTORY_ENTRY_IMPORT) except AttributeError: return {"count": 0, "note": "no imports / .NET"} names = [] for entry in imports: for imp in entry.imports: if imp.name: names.append(imp.name.decode()) resolver_only = set(["LoadLibraryA", "LoadLibraryW", "GetProcAddress", "VirtualAlloc", "VirtualProtect", "ExitProcess"]) return {"count": len(names), "resolver_only": bool(names) and set(names) <= resolver_only, "sample": names[:10]}
def main(path): pe = pefile.PE(path, fast_load=False) secs = section_verdicts(pe) tail = find_tail_jump(pe) imps = import_profile(pe) upx = any("UPX-NAME" in s["flags"] for s in secs) hi_ent = sum(1 for s in secs if "HIGH-ENTROPY" in s["flags"]) packed = upx or (hi_ent >= 1 and (imps["count"] if imps.get("count") else 0) <= 8) \ or imps.get("resolver_only", False) print(f"== {path} ==") for s in secs: print(f" {s['name']:<10} ent={s['entropy']:<5} " f"vs={s['virtual_size']:<8} raw={s['raw_size']:<8} " f"exe={str(s['executable']):<5} {' '.join(s['flags'])}") print(f" imports : {imps}") print(f" tailjmp : {tail}") print(f" VERDICT : {'PACKED' if packed else 'not obviously packed'}") if upx: print(" hint : try `upx -d` first before dynamic work")
if __name__ == "__main__": main(sys.argv[1])Code Walkthrough
Section titled “Code Walkthrough”- Lines 8-19 implement Shannon entropy on a 256-bin histogram; 8.0 is maximum (uniform random), compressed PE sections typically land at 7.8-7.99 while normal
.textsits at 5.5-6.5. The 7.2 threshold at line 27 is the industry-standard triage cut. - Lines 26-44 build per-section verdicts. Line 33 catches the UPX signature case directly from section names. Line 30-31 flags a section with virtual size but zero raw bytes — the classic UPX0 layout where the decompressed body is written into memory that doesn’t exist on disk.
- Lines 47-68 are the tail-jump scanner. It selects the last non-empty executable section (line 48-50), takes its final 64 bytes, and looks for
E9/E8(jmp/call rel32, lines 61-65) andFF 25(jmp [imm32], lines 66-68). Ajmp rel32in the last 16 bytes of an executable section with a target outside the section is the single strongest static OEP indicator. - Lines 71-81 profile imports: a real GUI or network binary has 30+ imports; a packer stub has 3-8, and if the entire import set is inside the resolver-only set (
LoadLibraryA,GetProcAddress,VirtualAlloc, …) that’s a packer fingerprint without any name matching. - Lines 92-97 compute the verdict: UPX names, or high entropy plus low import count, or resolver-only imports. Any single hit is enough to flag, because false negatives cost analysis time while false positives cost one glance.
- The
mainblock (lines 100-111) prints a compact report; pipe toteeto keep triage notes attached to the sample directory.
How It Reaches The Target
Section titled “How It Reaches The Target”An attacker builds a payload in Visual Studio or a crypter GUI, selects a packer profile, and ships the wrapped binary through their distribution channel — malspam attachment, malicious ad, trojanized installer. The victim downloads the packed file; at this stage every static scanner sees only the stub. On execution the stub decrypts the real payload into memory and jumps to the OEP, and from that instant the payload behaves exactly as designed: it may inject into another process (see M2 for file-infecting variants), beacon to C2 (M7), or stage a secondary payload. Defenders never “see” the packed file doing anything — the detection opportunities are the transition itself (RWX allocation, execute from non-image memory) and the post-unpack behavior. Blue teams that only match signatures on the packed wrapper are always one crypter rebuild behind; the sustainable detections are behavioral, anchored on the unpacking event and what follows it. This is why unpacking matters equally to the red side (understanding which layer a defender can see) and the blue side (recovering the sample that deserves detection).
Exploitation Chain
Section titled “Exploitation Chain”- Delivery — packed loader arrives as email attachment “invoice.exe” (T1566.001)
- Triage — packed_hunter.py flags entropy 7.94, 5 imports, tail jump at section end
- First unpack —
upx -dfails (custom crypter), sample moves to dynamic bench - Stub trace — x64dbg breakpoint on VirtualAlloc/VirtualProtect fires twice; second allocation is the payload
- OEP capture — memory breakpoint on execute at section boundary; EIP lands on
push ebp; mov ebp, esp - Dump and rebuild — Scylla dumps process, fixes IAT (342 imports recovered)
- Deobfuscate — FLOSS decrypts 61 stack strings; config block reveals C2 domain
- Detection handoff — YARA rule written on unpacked payload (M6); Sysmon hunting query built on EID 10 cross-process events
Building Your Own Tool
Section titled “Building Your Own Tool”unpw.py — a batch unpack-triage pipeline: walks a directory of samples, runs the entropy/tail-jump/import triage on each, attempts an automatic upx -d where the signature allows, and produces a JSON + markdown triage report you can drop straight into an engagement note.
Design decisions
Section titled “Design decisions”- Reuse the triage logic from the module above as an importable module rather than copy-pasting it — one implementation, one set of bugs.
- Shell out to the real
upxbinary instead of reimplementing its decompressor; UPX’s format has multiple versions and edge cases you do not want to own. - Never overwrite the original sample: unpacked outputs land in
unpacked/with a.unpsuffix and a SHA-256 recorded in the report. - JSON as the canonical output (machine-ingestible), markdown as the human view — generated from the same dict, so they can’t disagree.
Full source
Section titled “Full source”#!/usr/bin/env python3"""unpw.py — batch packer triage + auto-UPX unpacker with report.Usage: python unpw.py <samples_dir> [--out report]Deps: pefile; `upx` must be on PATH for auto-unpack."""import argparse, hashlib, json, os, shutil, subprocess, sys, mathimport pefile
RESOLVER = {"LoadLibraryA", "LoadLibraryW", "GetProcAddress", "VirtualAlloc", "VirtualProtect", "ExitProcess"}
def entropy(data: bytes) -> float: if not data: return 0.0 freq = [0] * 256 for b in data: freq[b] += 1 n = len(data) return -sum((c / n) * math.log2(c / n) for c in freq if c)
def triage(path: str) -> dict: rec = {"file": os.path.basename(path), "sha256": "", "packed": False, "upx": False, "sections": [], "imports": None, "notes": []} try: data = open(path, "rb").read() except OSError as e: rec["notes"].append(f"unreadable: {e}") return rec rec["sha256"] = hashlib.sha256(data).hexdigest() if not data[:2] == b"MZ": rec["notes"].append("not MZ") return rec try: pe = pefile.PE(path, fast_load=False) except pefile.PEFormatError as e: rec["notes"].append(f"PE parse error: {e}") return rec hi = 0 for s in pe.sections: name = s.Name.rstrip(b"\x00").decode(errors="replace") raw = s.get_data() ent = round(entropy(raw), 2) exe = bool(s.Characteristics & 0x20000000) rec["sections"].append({"name": name, "entropy": ent, "exe": exe, "raw": len(raw)}) if ent > 7.2: hi += 1 if name.upper().startswith("UPX"): rec["upx"] = True try: names = [i.name.decode() for e in pe.DIRECTORY_ENTRY_IMPORT for i in e.imports if i.name] except AttributeError: names = [] rec["imports"] = {"count": len(names), "resolver_only": bool(names) and set(names) <= RESOLVER} rec["packed"] = rec["upx"] or hi >= 1 and len(names) <= 8 \ or rec["imports"]["resolver_only"] return rec
def try_upx(path: str, outdir: str) -> dict: """Attempt `upx -d` into outdir; returns unpack result record.""" out_path = os.path.join(outdir, os.path.basename(path) + ".unp") shutil.copy2(path, out_path) try: r = subprocess.run(["upx", "-d", "-q", out_path], capture_output=True, text=True, timeout=60) ok = r.returncode == 0 msg = (r.stdout + r.stderr).strip().splitlines() msg = msg[-1] if msg else "" except FileNotFoundError: return {"attempted": False, "ok": False, "msg": "upx not on PATH"} except subprocess.TimeoutExpired: return {"attempted": True, "ok": False, "msg": "timeout"} if not ok: os.remove(out_path) return {"attempted": True, "ok": ok, "msg": msg, "output": out_path if ok else None}
def main(): ap = argparse.ArgumentParser() ap.add_argument("dir") ap.add_argument("--out", default="unpw_report") args = ap.parse_args() if not os.path.isdir(args.dir): sys.exit(f"not a directory: {args.dir}") outdir = os.path.join(args.out, "unpacked") os.makedirs(outdir, exist_ok=True) records = [] for root, _, files in os.walk(args.dir): for f in sorted(files): p = os.path.join(root, f) rec = triage(p) if rec["upx"]: rec["unpack"] = try_upx(p, outdir) records.append(rec) packed = [r for r in records if r["packed"]] report = {"total": len(records), "packed_count": len(packed), "records": records} jpath = os.path.join(args.out, "report.json") with open(jpath, "w") as fh: json.dump(report, fh, indent=2) mpath = os.path.join(args.out, "report.md") with open(mpath, "w") as fh: fh.write(f"# Unpack triage — {len(records)} files, " f"{len(packed)} packed\n\n") fh.write("| file | sha256 | packed | UPX | imports | note |\n") fh.write("|---|---|---|---|---|---|\n") for r in records: imp = r["imports"]["count"] if r["imports"] else "-" note = "; ".join(r["notes"]) or ("unpacked" if r.get("unpack", {}).get("ok") else "") fh.write(f"| {r['file']} | `{r['sha256'][:16]}…` | " f"{'YES' if r['packed'] else 'no'} | " f"{'Y' if r['upx'] else '-'} | {imp} | {note} |\n") print(f"[+] {len(records)} triaged, {len(packed)} packed") print(f"[+] {jpath}\n[+] {mpath}")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”triage()(lines 22-58) is the per-file brain: MZ check, PE parse guarded by try/except (malware samples are frequently corrupt on purpose), entropy and section scan, import profile. Everything is recorded even when the verdict is “not packed” — the report is a dataset, not just a yes/no.try_upx()(lines 61-82) copies before touching (line 64-66) — you never decompress in place; a failed run must leave the original intact. A missingupxbinary and a timeout are distinguished (lines 70-75) so the report explains why an attempt didn’t happen.main()walks recursively (lines 95-101), unpacks only UPX-signed files (line 103), and writes both report formats from the samerecordslist — the markdown table at lines 115-126 is generated, never hand-edited, so the two views can’t drift.- The SHA-256 is computed on the raw bytes (line 40) — the identity you’ll quote in reports and match against VT later, not the filename.
Compilation and test procedure
Section titled “Compilation and test procedure”install dependency and ensure upx exists
Section titled “install dependency and ensure upx exists”$ pip install pefile && which upx || sudo apt install upx-ucl
build a test corpus: pack a legit utility with UPX, leave one unpacked
Section titled “build a test corpus: pack a legit utility with UPX, leave one unpacked”$ mkdir -p /tmp/unpw-corpus $ cp /bin/ps.exe /tmp/unpw-corpus/ 2>/dev/null || cp $(which powershell.exe) /tmp/unpw-corpus/ 2>/dev/null $ upx -o /tmp/unpw-corpus/packed_sample.exe some_clean.exe
run the tool
Section titled “run the tool”$ python unpw.py /tmp/unpw-corpus —out /tmp/unpw-out [+] 2 triaged, 1 packed [+] /tmp/unpw-out/report.json [+] /tmp/unpw-out/report.md
verify the auto-unpacked output still runs and re-triage it
Section titled “verify the auto-unpacked output still runs and re-triage it”$ upx -t /tmp/unpw-out/unpacked/packed_sample.exe.unp $ python packed_hunter.py /tmp/unpw-out/unpacked/packed_sample.exe.unp
Detection considerations
Section titled “Detection considerations”Your own tooling is also visible to defenders. A batch unpacker writing dozens of .unp files, spawning upx in a loop, and reading executable files at high rate is exactly the profile of an analyst workstation — which is fine on a lab machine and a red flag on an endpoint. On the detection side of the house: the unpacking event on a real victim (RWX VirtualAlloc followed by execution from non-image memory) is gold, and your YARA rules (M6) should always be authored against the unpacked payload, never the wrapper, because the wrapper changes with every crypter rebuild while the payload family persists for months. Keep the JSON report — it is structured IOC provenance you can feed to a threat-intel platform without manual transcription (M15).
Detection considerations (hunter quality bar)
Section titled “Detection considerations (hunter quality bar)”Calibrate the entropy threshold on your own corpus: legitimate installers (NSIS, Inno Setup) routinely hit 7.3-7.6 on their compressed payload sections. If you flag those as packed you’ll desensitize yourself to true positives. The import-count heuristic is the tiebreaker that separates “installer with a compressed payload section” from “packer stub” — installers keep 40+ imports, packers keep under 10.
Improvements to add
Section titled “Improvements to add”- Detect PE files where
AddressOfEntryPointlands in a section with raw size 0 — a strong custom-packer signal independent of names. - Add a Capstone-based disassembly of the last 64 bytes of the last executable section to confirm tail jumps are real instructions, not data that happens to contain E9.
- Chain into
flossautomatically for string deobfuscation and merge its output into the record. - Support .NET: detect the CLR header and flag ConfuserEx indicators (obfuscated type names, no readable strings) via
dnlib[verify current API]. - Emit a Sigma rule suggestion per packed family you encounter repeatedly, closing the loop between analysis and detection.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Hunt query idea |
|---|---|---|---|
| RWX private memory allocation then execution | Sysmon | EID 10 + image load anomalies | EventID=10 AND GrantedAccess~0x1F* AND CallTrace~UNKNOWN |
| Execute from non-image-backed memory | Sysmon | EID 1 with IntegrityLevel + module gaps [verify field mapping] | Process with no on-disk image path correlation |
| Packed file written by office app | Sysmon | EID 11 | TargetFilename ends .exe AND ParentImage ~ WINWORD/EXCEL/OUTLOOK |
| Service install of packed binary | Windows Security | 7045 | ServiceFileName entropy check offline |
| Process injection into explorer/svchost | Sysmon | EID 8, 10 | Cross-process TargetImageISirectory mismatch [verify] |
| Packed loader network callback before unpack telemetry | Sysmon | EID 22 / 3 | DNS+connect within 5s of process creation |
| Batch triage tooling on endpoint | Sysmon | EID 1 (upx.exe spawned in loops) | Alert on upx.exe outside lab hosts |
Wazuh: map Sysmon channel events to custom rules — exact rule IDs vary by deployment [verify against your ruleset]. Suricata: packed-payload-over-HTTP has no reliable signature; use custom/local rules, e.g. sid:1000001 alerting on executable downloads followed by intra-host execution correlation in your SIEM rather than at the NDR layer.
Evasion Techniques
Section titled “Evasion Techniques”The packer author’s goals are the mirror of yours: prevent static recovery, survive the dump, and resist automated unpackers. The specific moves: stage the decryption so the key comes from the environment (PEB address, timestamp, a value fetched from a live C2) — no amount of sandbox replay recovers a key that arrives over the network. Wipe the PE header in the allocated buffer after unpacking, so a naive dump produces a headerless blob that tools refuse to load; the analyst must reconstruct the header with Scylla or peclone. Vary the stub per build — generate the decryptor from templates with randomized register allocation and instruction scheduling — so even the stub’s byte signature rotates. Abuse sections: mark the payload section non-executable, have the stub call VirtualProtect to flip it to RX at the last moment (defeating simple “section must be RX” heuristics), or use NtCreateSection+NtMapViewOfSection pairs so the code exists as mapped image memory rather than private RWX, which dodges the loudest Sysmon EID 10 signatures. Sign the wrapper: apply a stolen code-signing certificate or a legit Apple/Windows notarization pipeline to the packed outer layer so the file crosses reputation gates before detonation.
⚠️ What caught me: I once burned six hours on a sample whose entropy refused to drop after two unpack rounds — every
upx -dand manual stub trace produced another packed layer. The answer was not a third layer: the payload’s config block alone was encrypted with a key fetched from its C2, and the “stub” I kept tracing was the network decryptor. Check whether the opacity you’re fighting is the code or just the data before you escalate to heavier tooling.
Lab Practice
Section titled “Lab Practice”- Download a benign utility, pack it with
upx --best --lzma, and runpacked_hunter.pyon both versions; compare section tables side by side in PE-bear and note exactly which fields moved. - Unpack the UPX sample two ways — statically with
upx -dand dynamically with x64dbg: set a memory breakpoint on execute at the start of the UPX0 section, run to OEP, dump with Scylla, rebuild IAT — then diff the two recovered binaries; they will differ in relocations, which is itself a lesson. - Take a real-world packed malware sample from a public repository (e.g. MalwareBazaar with
tag:upx), confirm the UPX signature is corrupted (upx -dfails), and manually fix theUPX!magic bytes to enable unpacking — this is a common anti-UPX trick. - Write a YARA rule against the unpacked payload only, run it against a directory containing both packed and unpacked variants of the same family, and confirm it matches the unpacked builds — then read M6 and iterate until it also survives a recompile.
Interview Questions
Section titled “Interview Questions”- Technical: Walk me through what happens between the entry point of a UPX-packed binary and the original entry point. What memory operations occur and in what order?
- Technical: How would you find the OEP of a custom packer that has no recognizable signature and clears the PE header after decompression?
- Scenario: A sandbox report shows a sample with entropy 7.9 that spawns powershell and connects to a known-bad domain, but your static analysis shows no strings. What happened, and what do you do next?
- Conceptual: Why is import-table size such a strong packer indicator, and how do packers that do preserve a full import table defeat that heuristic?
- Conceptual: What is the difference between packing, crypting, and virtualization, and how does your analysis workflow change for each?
- Deep-dive: How does executing code from a section-mapped view (NtMapViewOfSection) differ from private RWX memory from a telemetry standpoint, and why do attackers prefer it?
- Practical: Your YARA rule matches the packed wrapper of a family and fires on 4,000 benign files. Diagnose and fix.
Related Modules
Section titled “Related Modules”M2 (File Infector Analysis) · M5 (String and Artifact Extraction) · M6 (YARA Rule Engineering) · M14 (Executable Packing Analysis) · M15 (Threat Intelligence and Campaign Clustering)