#!/usr/bin/env python3 """Analyze Office documents for malicious content. Extracts and analyzes VBA macros, embedded objects, DDE links, auto-execution indicators, and suspicious patterns. """ from __future__ import annotations import argparse import hashlib import json import os import re import struct import sys import zipfile from datetime import datetime from pathlib import Path from xml.etree import ElementTree # Suspicious VBA patterns SUSPICIOUS_KEYWORDS = { "execution": [ "Shell", "WScript.Shell", "CreateObject", "GetObject", "CallByName", "Eval", "Execute", "Run", "ShellExecute", "CreateProcess", "WinExec", ], "download": [ "URLDownloadToFile", "XMLHTTP", "WinHttp", "InternetOpen", "Msxml2.XMLHTTP", "Microsoft.XMLHTTP", "ADODB.Stream", "New-Object Net.WebClient", "Invoke-WebRequest", "DownloadString", "DownloadFile", ], "obfuscation": [ "Chr(", "ChrW(", "Asc(", "StrReverse", "Replace(", "Mid(", "Left(", "Right(", "Split(", "FromBase64String", "Convert.FromBase64", ], "persistence": [ "RegWrite", "RegCreate", "ScheduledTasks", "Startup", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ], "anti_analysis": [ "Application.Name", "Application.Version", "Environ(", "GetTickCount", "Sleep", "Application.MouseAvailable", ], "process_injection": [ "VirtualAlloc", "WriteProcessMemory", "CreateRemoteThread", "NtCreateThread", "RtlMoveMemory", ], } AUTO_EXECUTE_TRIGGERS = [ "AutoOpen", "AutoClose", "AutoExec", "AutoExit", "AutoNew", "Document_Open", "Document_Close", "Document_New", "Workbook_Open", "Workbook_Close", "Workbook_BeforeClose", "Workbook_Activate", "Workbook_Deactivate", "Auto_Open", "Auto_Close", ] def compute_hash(data) -> dict: """Compute SHA256 hash.""" return hashlib.sha256(data).hexdigest() def analyze_ooxml(filepath) -> list: """Analyze OOXML format (.docx, .xlsx, .pptx).""" findings = { "format": "OOXML", "embedded_objects": [], "external_references": [], "dde_links": [], "macros_present": False, } try: with zipfile.ZipFile(filepath, "r") as zf: names = zf.namelist() # Check for VBA macros if any("vbaProject" in n for n in names): findings["macros_present"] = True # Check for external template references for name in names: if name.endswith(".rels"): try: content = zf.read(name).decode("utf-8", errors="ignore") # Look for external targets ext_refs = re.findall( r'Target="(https?://[^"]+)"', content ) for ref in ext_refs: findings["external_references"].append({ "source": name, "url": ref, "type": "template_injection" if ".dot" in ref.lower() else "external_reference" }) except Exception: pass # Check for DDE in document content for name in names: if "document.xml" in name or "sheet" in name: try: content = zf.read(name).decode("utf-8", errors="ignore") dde_matches = re.findall( r'(DDEAUTO|DDE)\s+[^}]+', content, re.IGNORECASE ) for match in dde_matches: findings["dde_links"].append({ "source": name, "content": match[:200], }) except Exception: pass # Check for embedded objects for name in names: if "embeddings" in name or "oleObject" in name: try: data = zf.read(name) findings["embedded_objects"].append({ "name": name, "size": len(data), "hash": compute_hash(data), }) except Exception: pass except zipfile.BadZipFile: findings["error"] = "Not a valid ZIP/OOXML file" return findings def extract_vba_strings(filepath) -> list: """Extract potential VBA macro content from OLE/OOXML files.""" findings = { "auto_execute": [], "suspicious_keywords": {}, "urls": [], "ips": [], "strings": [], } try: data = Path(filepath).read_bytes() # Extract readable strings strings = re.findall(rb'[\x20-\x7e]{6,}', data) decoded = [s.decode("ascii", errors="ignore") for s in strings] # Check for auto-execute for trigger in AUTO_EXECUTE_TRIGGERS: if any(trigger.lower() in s.lower() for s in decoded): findings["auto_execute"].append(trigger) # Check for suspicious keywords for category, keywords in SUSPICIOUS_KEYWORDS.items(): found = [] for keyword in keywords: if any(keyword.lower() in s.lower() for s in decoded): found.append(keyword) if found: findings["suspicious_keywords"][category] = found # Extract URLs url_pattern = re.compile( r'https?://[a-zA-Z0-9\-._~:/?#\[\]@!$&\'()*+,;=%]{5,}' ) findings["urls"] = list(set( url_pattern.findall(" ".join(decoded)) )) # Extract IPs ip_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b') findings["ips"] = list(set( ip_pattern.findall(" ".join(decoded)) )) except Exception as e: findings["error"] = str(e) return findings def analyze_office_document(filepath) -> dict: """Main analysis function for Office documents.""" path = Path(filepath) if not path.exists(): print(f"[!] File not found: {filepath}", file=sys.stderr) sys.exit(1) data = path.read_bytes() result = { "file": str(filepath), "file_size": len(data), "sha256": compute_hash(data), "timestamp": datetime.now().isoformat(), } # Determine format if data[:4] == b"PK\x03\x04": result["format"] = "OOXML" result["ooxml_analysis"] = analyze_ooxml(filepath) elif data[:8] == b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1": result["format"] = "OLE2 (Legacy Office)" else: result["format"] = "Unknown" # Extract VBA indicators result["vba_analysis"] = extract_vba_strings(filepath) # Risk assessment risk_score = 0 risk_reasons = [] if result["vba_analysis"]["auto_execute"]: risk_score += 30 risk_reasons.append(f"Auto-execute triggers: {result['vba_analysis']['auto_execute']}") suspicious = result["vba_analysis"]["suspicious_keywords"] if "execution" in suspicious: risk_score += 25 risk_reasons.append(f"Execution methods: {suspicious['execution']}") if "download" in suspicious: risk_score += 25 risk_reasons.append(f"Download capabilities: {suspicious['download']}") if "obfuscation" in suspicious: risk_score += 15 risk_reasons.append(f"Obfuscation indicators: {suspicious['obfuscation']}") if result["vba_analysis"]["urls"]: risk_score += 10 risk_reasons.append(f"URLs found: {len(result['vba_analysis']['urls'])}") if result.get("ooxml_analysis", {}).get("external_references"): risk_score += 30 risk_reasons.append("External template references (template injection)") if result.get("ooxml_analysis", {}).get("dde_links"): risk_score += 30 risk_reasons.append("DDE links detected") result["risk_assessment"] = { "score": min(risk_score, 100), "level": "critical" if risk_score >= 60 else ("high" if risk_score >= 40 else ("medium" if risk_score >= 20 else "low")), "reasons": risk_reasons, } return result def main() -> None: parser = argparse.ArgumentParser( description="Analyze Office documents for malicious content" ) parser.add_argument("--input", "--file", help="Office document to analyze") parser.add_argument("--output", "-o", help="Output file (JSON)") parser.add_argument("--format", choices=["json", "text"], default="text") args = parser.parse_args() result = analyze_office_document(args.file) if args.format == "json": output = json.dumps(result, indent=2) else: output = f"=== Office Document Analysis ===\n" output += f"File: {result['file']}\n" output += f"Format: {result['format']}\n" output += f"SHA256: {result['sha256']}\n" output += f"Size: {result['file_size']} bytes\n\n" risk = result["risk_assessment"] output += f"Risk: {risk['level'].upper()} (score: {risk['score']}/100)\n" for reason in risk["reasons"]: output += f" - {reason}\n" vba = result["vba_analysis"] if vba["auto_execute"]: output += f"\nAuto-Execute Triggers: {', '.join(vba['auto_execute'])}\n" if vba["urls"]: output += f"\nURLs Found:\n" for url in vba["urls"][:10]: output += f" {url}\n" if vba["ips"]: output += f"\nIPs Found: {', '.join(vba['ips'][:10])}\n" if args.output: Path(args.output).write_text(output) print(f"[+] Report saved to {args.output}") else: print(output) if __name__ == "__main__": main()