74 lines
2.8 KiB
Python
Executable File
74 lines
2.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Analyze extracted Excel 4.0 (XLM) macro formulas for malicious behavior.
|
|
|
|
Reads a text dump of macro-sheet formulas and flags auto-run entry points, execution/download
|
|
primitives (EXEC/CALL/REGISTER), and obfuscation (CHAR concatenation, FORMULA.FILL self-writing).
|
|
Parses text only; executes nothing and never opens the workbook. Defangs recovered URLs.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
AUTO_NAMES = ["auto_open", "auto_close", "auto_activate", "auto_deactivate"]
|
|
EXEC_PRIMS = ["exec(", "call(", "register(", "fopen(", "fwrite("]
|
|
DOWNLOAD_HINTS = ["urlmon", "urldownloadtofile", "http", "msxml2", "winhttp", "shellexecute"]
|
|
EXECUTORS = ["regsvr32", "rundll32", "mshta", "powershell", "cmd.exe", "wscript", "cscript"]
|
|
OBFUSCATION = ["char(", "formula.fill", "formula(", "mid(", "&", "concatenate("]
|
|
RE_URL = re.compile(r"https?://[^\s\"'<>)]+", re.I)
|
|
RE_CHAR = re.compile(r"CHAR\((\d{1,3})\)", re.I)
|
|
|
|
|
|
def _defang(s: str) -> str:
|
|
return s.replace("http", "hxxp").replace(".", "[.]")
|
|
|
|
|
|
def _reconstruct_char(line: str) -> str:
|
|
chars = RE_CHAR.findall(line)
|
|
if len(chars) < 3:
|
|
return ""
|
|
try:
|
|
return "".join(chr(int(c)) for c in chars if 0 <= int(c) < 256)
|
|
except ValueError:
|
|
return ""
|
|
|
|
|
|
def analyze(path: str) -> dict:
|
|
text = Path(path).read_text(encoding="utf-8-sig", errors="replace")
|
|
low = text.lower()
|
|
auto = sorted({n for n in AUTO_NAMES if n in low})
|
|
exec_prims = sorted({p.rstrip("(") for p in EXEC_PRIMS if p in low})
|
|
downloads = sorted({d for d in DOWNLOAD_HINTS if d in low})
|
|
executors = sorted({e for e in EXECUTORS if e in low})
|
|
obf = sorted({o.rstrip("(") for o in OBFUSCATION if o in low})
|
|
urls = sorted({_defang(u) for u in RE_URL.findall(text)})
|
|
reconstructed = sorted({r for line in text.splitlines()
|
|
if (r := _reconstruct_char(line))})[:25]
|
|
return {
|
|
"auto_run_triggers": auto,
|
|
"execution_primitives": exec_prims,
|
|
"download_indicators": downloads,
|
|
"executors": executors,
|
|
"obfuscation_techniques": obf,
|
|
"urls": urls[:50],
|
|
"reconstructed_char_strings": reconstructed,
|
|
"is_likely_malicious_xlm": bool(auto and (exec_prims or downloads)),
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Analyze Excel 4.0 (XLM) macros")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("analyze")
|
|
p.add_argument("file")
|
|
args = parser.parse_args(argv)
|
|
print(json.dumps(analyze(args.file), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|