Dissecting Malicious Office Documents: VBA Macros, Stomping, and IOC Extraction

Malware & C2
Time it takes to read this article 6 minutes.

Disclaimer. This article is for education, defensive research, and authorized engagements only. Analyze live malware exclusively inside an isolated, disposable lab. Detonating or distributing the samples discussed here against systems you do not own or lack written permission to test is illegal. Treat every sample as live ordnance.

Introduction / Overview

Despite a decade of "macros are dead" predictions, malicious Office documents (maldocs) remain a workhorse of initial-access tradecraft. They map cleanly to MITRE ATT&CK T1566.001 (Phishing: Spearphishing Attachment) and T1204.002 (User Execution: Malicious File). A weaponized .doc, .xls, or .docm is small, bypasses naive content filters, and leverages the most reliable exploit primitive of all: a user clicking Enable Content.

This post walks through triaging a maldoc the way an analyst or red-team operator would: parsing the OLE/OOXML container, extracting and deobfuscating VBA, defeating VBA stomping, and pulling IOCs for a report. The toolchain of choice is Didier Stevens' and Philippe Lagadec's oletools suite, with olevba doing most of the heavy lifting.

How it works / Background

Office files come in two container formats:

  • OLE2 / Compound File Binary (CFB) — the legacy .doc/.xls/.ppt. A mini-filesystem of "storages" and "streams." VBA lives in a Macros/VBA or _VBA_PROJECT_CUR/VBA storage.
  • OOXML — the modern .docx/.xlsx. These are ZIP archives. The benign .docx cannot hold macros; the macro-enabled .docm/.xlsm embeds an OLE blob named word/vbaProject.bin inside the ZIP.

Inside the VBA storage, two things matter. The compressed source code (what you see in the VBA editor) and the P-code (pre-compiled, tokenized bytecode the VBA engine actually executes). They are supposed to agree.

VBA stomping abuses that assumption. An attacker compiles the macro, then overwrites or strips the compressed source while leaving the P-code intact. The document still runs because the engine prefers P-code (when the host Office version matches), but any tool that only reads the source sees benign or empty code. This defeats source-only scanners and bumps detection evasion. The catch: P-code is version-specific, so stomped maldocs often pin a target Office version.

Prerequisites / Lab setup

Use a disconnected analysis VM (REMnux is ideal). Install the tooling in a virtualenv:

python3 -m venv ~/maldoc-venv
source ~/maldoc-venv/bin/activate
pip install -U oletools
# olevba, oleid, rtfobj, mraptor, oledump-style parsing all ship here
olevba --version
Bash

Pull a couple of companion tools you will reach for repeatedly:

# Didier Stevens' suite for raw stream surgery and P-code disassembly
pip install oletools[full]      # enables pcodedmp integration
git clone https://github.com/decalage2/ViperMonkey.git  # VBA emulation (optional)
Bash

Never open the sample in a real Office install on the host. If you must detonate, snapshot the VM first.

Walkthrough / PoC

1. Identify the container

Start with oleid for a fast risk triage. It flags VBA presence, encryption, and known-suspicious indicators without executing anything.

oleid suspicious_invoice.doc
Bash

Typical output highlights VBA Macros: Yes, Encrypted: No, and any Flash/embedded-object red flags. For an unknown extension, confirm the real type:

file suspicious_invoice.doc
# -> Composite Document File V2 Document  (OLE2)
# -> Microsoft Word 2007+ / Zip archive   (OOXML)
Bash

2. Extract and deobfuscate VBA with olevba

olevba is the core tool. It decompresses every VBA module, auto-deobfuscates common tricks (Chr() chains, StrReverse, Base64, hex), and runs a built-in IOC/keyword triage engine.

olevba --decode --deobf suspicious_invoice.doc
Bash

The tail of the report is the gold. olevba prints a table of suspicious keywords (AutoOpen, Shell, CreateObject, WScript.Shell, Environ), IOCs (URLs, IPs, file paths), and AutoExec triggers. For a machine-readable artifact feeding a pipeline:

olevba --json suspicious_invoice.doc > triage.json
Bash

To dump only the auto-extracted indicators across a folder of samples:

olevba --triage *.doc *.docm *.xls
Bash

--triage prints a one-line-per-file matrix (A=AutoExec, S=Suspicious, I=IOC, H=Hex, B=Base64, etc.) — perfect for bulk corpus sweeps.

3. Detect and defeat VBA stomping

This is where source-only analysis lies to you. The tell: olevba reports trivial or empty source, but the file clearly executes. olevba ships a P-code disassembler hook. Compare the decompressed source against the P-code:

# Dump the P-code (compiled tokens) directly:
pcodedmp suspicious_invoice.doc

# Or let olevba flag the mismatch and show both:
olevba --show-pcode suspicious_invoice.doc
Bash

If the P-code disassembly contains real logic (string concatenation, Shell, network calls) while the source modules are blank or innocuous, you are looking at a stomped document. olevba explicitly emits a warning like "VBA Stomping was detected" when the source/P-code lengths diverge. Reconstruct intent from the P-code opcodes — the LitStr, Ld, and FuncCall tokens reveal the strings and APIs the engine will execute.

4. Reconstruct the kill chain and extract IOCs

Most maldoc droppers follow the same shape: an AutoOpen/Document_Open trigger assembles an obfuscated PowerShell or mshta/certutil command, then hands it to Shell or WMI. Pull the final payload string and any C2 endpoints. ViperMonkey can emulate the VBA to surface the deobfuscated command without running it:

vmonkey suspicious_invoice.doc
# Emulates the macro, prints final Shell/CreateObject arguments + extracted URLs
Bash

Capture every IOC for the report: dropped file paths, scheduled-task names, registry keys, staging URLs, and the spawned process tree. RTF samples that embed objects (a common CVE-2017-11882 / Equation Editor vector) need a different parser:

rtfobj suspicious_exploit.rtf      # extracts embedded OLE objects from RTF
Bash

5. Maldoc execution flow

Dissecting Malicious Office Documents: VBA Macros, Stomping, and IOC Extraction diagram 1

The diagram shows how the social-engineering prompt gates execution, and how stomping routes the real logic through P-code while source-only scanners see nothing.

Detection & Defense (Blue Team)

Maldocs are one of the most defendable initial-access vectors because the mitigations are mature and largely free.

1. Block macros by policy — the single highest-impact control. Microsoft now blocks VBA macros in files marked with the Mark-of-the-Web (MotW) from the internet by default. Enforce it via GPO/Intune:

  • Block macros from running in Office files from the Internet = Enabled.
  • Disable Trust access to the VBA project object model and Trust VBA macros that are digitally signed unless your environment genuinely needs signed macros.

2. Strip MotW only on trusted shares. Attackers wrap maldocs in .zip/.iso/.img containers precisely to break MotW propagation (mounted ISOs historically did not stamp MotW). Block or quarantine .iso/.img email attachments, and apply MotW-preserving extraction policies.

3. Attack Surface Reduction (ASR) rules. Enable Microsoft Defender ASR rules in block mode:

  • Block all Office applications from creating child processes (D4F940AB-401B-4EFC-AADC-AD5F3C50688A).
  • Block Win32 API calls from Office macros.
  • Block Office applications from creating executable content.

These break the winword.exe -> powershell.exe lineage that nearly every dropper relies on.

4. Hunt the process tree. The most durable detection is behavioral, not signature-based. Alert on any Office binary (winword.exe, excel.exe, mshta.exe) spawning powershell.exe, cmd.exe, wscript.exe, cscript.exe, certutil.exe, or regsvr32.exe. A representative Sysmon Event ID 1 / EDR query:

parent_process IN ("winword.exe","excel.exe","powerpnt.exe")
AND child_process IN ("powershell.exe","cmd.exe","mshta.exe","wscript.exe","certutil.exe","rundll32.exe")
Plaintext

5. Pre-delivery static analysis at the gateway. Run mraptor (ships with oletools) in your mail/sandbox pipeline — it heuristically flags malicious macros by combining AutoExec + Write + Execute triggers:

mraptor suspicious_invoice.doc   # exit code 20 = SUSPICIOUS
Bash

Crucially, mraptor and olevba --show-pcode catch stomped documents that source-only AV misses. Patch the classic embedded-object exploits (CVE-2017-11882, CVE-2017-0199) so RTF/Equation Editor vectors die regardless of macro policy. For deeper static triage of the dropped payloads, pivot to a disassembler — see my notes on getting started with Ghidra and analyzing PowerShell loaders.

Conclusion

Maldoc analysis is a repeatable pipeline: identify the container, extract VBA with olevba, verify source against P-code to catch stomping, emulate to recover the final command, and log every IOC. On the defensive side, default macro blocking plus ASR child-process rules neutralize the overwhelming majority of these campaigns — the offense only wins when policy is misconfigured. For broader context on what the recovered payload does next, see building a malware analysis lab.

References

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL