Skip to content

Unpacking and Deobfuscation

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.

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.

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 OEP

The 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++.

TechniqueWhat it looks likeTypical toolsNotes
UPX packingUPX0/UPX1 sections, “UPX!” magicupx -d, PE-bearTrivially reversible; still used at volume for malspam loaders
Custom crypter stubsHigh entropy, 3-5 imports, no section namesx64dbg, Scylla, FLOSSThe bread-and-butter of commodity loaders
VMProtectVirtualized handlers, .vmp0/.vmp1 sectionsManual RE, TitanEngineOnly hot paths virtualized; unpack the rest
Themida / WinLicenseAnti-dump, anti-VM, mutating stubx64dbg plugins, Scout [verify]Adds anti-debug layers — see E15
.NET ConfuserExControl-flow flattening, constant/string encryptionde4dot, dnSpyRule sets live in repo config [verify availability]
Single-byte XOR stringsRepeating-char bias in blobsxortool, FLOSSFastest string recovery win
API hashing (ROR13/CRC32)No imports; hash constants near resolver codehashdb, pe-tree, IDA pluginsRebuild hash→API map offline
Control-flow flatteningOne dispatcher loop, state variableangr, D-810, manualCostly; only flatten what matters
Anti-dump tricksWiped PE header, wrong SizeOfImage at dump timeScylla, pecloneDefeats naive MiniDumpWriteDump
Nested packing (UPX→crypter)Unpack once, still entropy 7.9Iterate until entropy dropsAlways re-scan after each unpack

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, struct
import 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])
  • 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 .text sits 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) and FF 25 (jmp [imm32], lines 66-68). A jmp rel32 in 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 main block (lines 100-111) prints a compact report; pipe to tee to keep triage notes attached to the sample directory.

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).

  1. Delivery — packed loader arrives as email attachment “invoice.exe” (T1566.001)
  2. Triage — packed_hunter.py flags entropy 7.94, 5 imports, tail jump at section end
  3. First unpack — upx -d fails (custom crypter), sample moves to dynamic bench
  4. Stub trace — x64dbg breakpoint on VirtualAlloc/VirtualProtect fires twice; second allocation is the payload
  5. OEP capture — memory breakpoint on execute at section boundary; EIP lands on push ebp; mov ebp, esp
  6. Dump and rebuild — Scylla dumps process, fixes IAT (342 imports recovered)
  7. Deobfuscate — FLOSS decrypts 61 stack strings; config block reveals C2 domain
  8. Detection handoff — YARA rule written on unpacked payload (M6); Sysmon hunting query built on EID 10 cross-process events

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.

  • 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 upx binary 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 .unp suffix 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.
#!/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, math
import 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()
  • 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 missing upx binary 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 same records list — 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.

$ 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

$ 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

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.

  1. Detect PE files where AddressOfEntryPoint lands in a section with raw size 0 — a strong custom-packer signal independent of names.
  2. 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.
  3. Chain into floss automatically for string deobfuscation and merge its output into the record.
  4. Support .NET: detect the CLR header and flag ConfuserEx indicators (obfuscated type names, no readable strings) via dnlib [verify current API].
  5. Emit a Sigma rule suggestion per packed family you encounter repeatedly, closing the loop between analysis and detection.
SignalSourceIDHunt query idea
RWX private memory allocation then executionSysmonEID 10 + image load anomaliesEventID=10 AND GrantedAccess~0x1F* AND CallTrace~UNKNOWN
Execute from non-image-backed memorySysmonEID 1 with IntegrityLevel + module gaps [verify field mapping]Process with no on-disk image path correlation
Packed file written by office appSysmonEID 11TargetFilename ends .exe AND ParentImage ~ WINWORD/EXCEL/OUTLOOK
Service install of packed binaryWindows Security7045ServiceFileName entropy check offline
Process injection into explorer/svchostSysmonEID 8, 10Cross-process TargetImageISirectory mismatch [verify]
Packed loader network callback before unpack telemetrySysmonEID 22 / 3DNS+connect within 5s of process creation
Batch triage tooling on endpointSysmonEID 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.

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 -d and 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.

  1. Download a benign utility, pack it with upx --best --lzma, and run packed_hunter.py on both versions; compare section tables side by side in PE-bear and note exactly which fields moved.
  2. Unpack the UPX sample two ways — statically with upx -d and 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.
  3. Take a real-world packed malware sample from a public repository (e.g. MalwareBazaar with tag:upx), confirm the UPX signature is corrupted (upx -d fails), and manually fix the UPX! magic bytes to enable unpacking — this is a common anti-UPX trick.
  4. 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.
  • 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.

M2 (File Infector Analysis) · M5 (String and Artifact Extraction) · M6 (YARA Rule Engineering) · M14 (Executable Packing Analysis) · M15 (Threat Intelligence and Campaign Clustering)