Script and Macro Malware Analysis
Definition
Section titled “Definition”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.
Why It Matters
Section titled “Why It Matters”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.
Detailed Explanation
Section titled “Detailed Explanation”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 | OneNoteThe 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.
Common Techniques
Section titled “Common Techniques”| Technique | Carrier | What it does | Analysis counter |
|---|---|---|---|
| VBA AutoOpen chain | .docm/.doc | Execute on open | olevba first; read event hooks |
| Chr/concat obfuscation | VBA/VBS | Hide command strings | Decode mechanically (tool below) |
| PowerShell -enc | Dropper stage | UTF-16LE base64 command | Decode + pretty-print, read flags |
| Remote template injection | .docx (clean) | Fetch macro .dotm from attacker host | Inspect word/_rels/settings.xml.rels |
| Excel 4.0 (XLM) macros | .xlsm | Legacy macro sheet revival | olevba --xlm [verify flag]; sheet dump |
| DDE/field abuse | .docx | No macro; fields execute | Check w:fldChar/DDEAUTO strings |
| LNK argument abuse | .lnk | Icon+arguments run commands | Parse LNK structure; inspect args |
| CHM payload | .chm | Compiled help executes HTML+JS | hh.exe decompile; check sections |
| JSE/VBE encoding | .jse/.vbe | Encoded script format | Official decoder (JScript.Encode) |
| VBA stomping | vbaProject.bin | Source removed; p-code only [verify] | p-code disassembly tools |
| AMSI bypass in script | PowerShell | Patch AmsiScanBuffer (E8) | Look for patch byte patterns |
| Macro-prompt gating | VBA | Execute only if user clicks | Detonate with automation+monitoring |
Full Working Code
Section titled “Full Working Code”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 3stdlib 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' -> abcdefVAR_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()Code Walkthrough
Section titled “Code Walkthrough”- Lines 10-24 define the pattern library.
ENC_REmatches 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'intoabcdef, andBACKTICK_REstrips PowerShell’s escape-backtick padding. try_decode_b64()(lines 27-40) encodes the format detective work:-encpayloads are UTF-16LE (every second byte NUL — detected at lines 31-34),FromBase64Stringpayloads 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 — theprev != sloop 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 | IEXchains fall out cleanly.- The main loop (lines 95-108) iterates to a fixpoint with a
seenset 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.
How It Reaches The Target
Section titled “How It Reaches The Target”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.
Exploitation Chain
Section titled “Exploitation Chain”- Delivery — phishing mail “Overdue_Invoice.docm” passes gateway (T1566.001)
- Open — user enables content; AutoOpen fires inside WINWORD.EXE
- Obfuscation — VBA assembles command from Chr() fragments; no static strings
- Execution — WScript.Shell runs powershell -nop -w hidden -enc
(T1059.001) - Decode — stage-1 script downloads shellcode from https://cdn-check[.]top/a.xll
- Load — shellcode injected into own process; loader beacon (T1055-style injection)
- Delivery 2 — loader pulls RedLine stealer (M9); credentials and cookies exfil
- Detection — 4104 script-block + EID 1 parent chain flagged; psdeob recovers full command for IR
Building Your Own Tool
Section titled “Building Your Own Tool”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.
Design decisions
Section titled “Design decisions”- 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.
Full source
Section titled “Full source”#!/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, zipfileimport 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()Walkthrough
Section titled “Walkthrough”- The container sniff (lines 108-115) dispatches on magic bytes, not extension — maldocs routinely lie about their extension to cross gateways, and
PKvs the OLE CFB magic (d0 cf 11 e0) is the only reliable split. scan_ooxml()(lines 25-62) checks the four OOXML abuse surfaces:vbaProjectpresence (line 33-36), external relationships (lines 39-46 — the remote-template injection marker; the regex requiresTargetMode="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.
Compilation and test procedure
Section titled “Compilation and test procedure”install the OLE dependency
Section titled “install the OLE dependency”$ 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
<<
Detection considerations
Section titled “Detection considerations”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.
Improvements to add
Section titled “Improvements to add”- Add MS-OVBA decompression for
vbaProject.binstreams so the hook/API scan sees full compressed content without olevba. - Parse LNK (Shell Link) structures — extra blocks and argument strings — to extend coverage beyond Office containers.
- Add OneNote triage (embedded files inventory) reflecting the 2023 abuse wave [verify current state].
- Emit a JSON verdict schema so mail-pipeline integration is a config task, not a code task.
- Fuzz
psdeob.pyagainst public obfuscation frameworks’ outputs (e.g., Invoke-Obfuscation token modes [verify]) and extend the pattern table by measured miss rate.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Office process spawns script host | Sysmon | EID 1 (parent WINWORD/EXCEL → powershell/cmd/wscript) | Near-zero FP, alert-grade |
| PowerShell script blocks | PowerShell operational | 4104 | Captures decoded content |
| Encoded-command usage | PowerShell / 4688 | 4104 + EID 1 cmdline regex | -enc/-w hidden flag combos |
| AMSI content verdicts | AMSI consumers / Defender | Defender 1116/1117; E8 interplay | Catches decoded stages |
| Payload/template fetch | Sysmon | EID 22 (DNS) + EID 3 (flow) | Template injection marker |
| Dropped stage from Office | Sysmon | EID 11 (writer = Office/script host) | Hash → M9/M8 chain |
| Macro from MOTW zone blocked | Office/Defender | 5001/5007 policy events | Policy-working confirmation |
| Suspicious driver/LNK/CHM execution | Sysmon | EID 1 with script host lineage | Non-Office dropper classes |
| Wazuh correlation of the above | HIDS/SIEM | [verify rule IDs per deployment] | Join Office→script→network chains |
| Template fetch over HTTP | Suricata | custom/local sid:1000001+ | Legacy chain only (TLS hides) |
Evasion Techniques
Section titled “Evasion Techniques”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.
Lab Practice
Section titled “Lab Practice”- Build a macro test doc in a lab Office VM: record a trivial macro, add an
AutoOpenthat runs aWrite-HostPowerShell command viaShell, then run both tools —maldoc_scan.pyshould surface the structure,psdeob.pyshould decode your-encwrapper. - 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.
- 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. - 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.
- 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.
Interview Questions
Section titled “Interview Questions”- 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?
Related Modules
Section titled “Related Modules”M5 (String and Artifact Extraction) · M9 (Infostealer Analysis) · M13 (Delivery Chain Analysis) · E8 (AMSI Analysis) · E11 (Defender Detection Logic)