reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract and triage VBA macros from Office documents.
|
||||
|
||||
Prefers ``oletools`` (olevba) for robust extraction; falls back to locating ``vbaProject.bin``
|
||||
inside OOXML (ZIP) containers and flagging suspicious indicators in the raw bytes. Never opens
|
||||
the document in Office and never executes anything.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
AUTOEXEC = ["AutoOpen", "Document_Open", "AutoClose", "Document_Close",
|
||||
"Workbook_Open", "Auto_Open", "Auto_Close"]
|
||||
SUSPICIOUS = ["Shell", "WScript.Shell", "CreateObject", "powershell", "cmd",
|
||||
"URLDownloadToFile", "XMLHTTP", "ADODB.Stream", "Environ",
|
||||
"winmgmts", "Base64", "Chr(", "GetObject"]
|
||||
|
||||
|
||||
def _scan_text(text: str) -> dict:
|
||||
found_auto = [k for k in AUTOEXEC if k.lower() in text.lower()]
|
||||
found_sus = [k for k in SUSPICIOUS if k.lower() in text.lower()]
|
||||
urls = sorted(set(re.findall(r"https?://[^\s\"'<>)]+", text)))
|
||||
return {"auto_exec": found_auto, "suspicious": found_sus, "urls": urls}
|
||||
|
||||
|
||||
def extract_with_olevba(path: str) -> dict:
|
||||
from oletools.olevba import VBA_Parser # type: ignore
|
||||
vp = VBA_Parser(path)
|
||||
macros = []
|
||||
if vp.detect_vba_macros():
|
||||
for _, _, name, code in vp.extract_macros():
|
||||
macros.append({"name": name, **_scan_text(code),
|
||||
"lines": code.count("\n") + 1})
|
||||
vp.close()
|
||||
return {"engine": "olevba", "has_macros": bool(macros), "macros": macros}
|
||||
|
||||
|
||||
def extract_minimal(path: str) -> dict:
|
||||
p = Path(path)
|
||||
data = p.read_bytes()
|
||||
indicators = {}
|
||||
if zipfile.is_zipfile(path):
|
||||
with zipfile.ZipFile(path) as zf:
|
||||
vba = [n for n in zf.namelist() if n.lower().endswith("vbaproject.bin")]
|
||||
if not vba:
|
||||
return {"engine": "minimal", "has_macros": False,
|
||||
"note": "no vbaProject.bin in OOXML container"}
|
||||
blob = zf.read(vba[0])
|
||||
# vbaProject.bin is OLE/compressed; scan recoverable ASCII for indicators
|
||||
text = blob.decode("latin-1", "replace")
|
||||
indicators = _scan_text(text)
|
||||
return {"engine": "minimal", "has_macros": True,
|
||||
"stream": vba[0], "indicators": indicators,
|
||||
"note": "install oletools for full VBA source extraction"}
|
||||
# legacy OLE2: scan whole file bytes for indicator strings
|
||||
text = data.decode("latin-1", "replace")
|
||||
return {"engine": "minimal", "has_macros": b"Attribut" in data,
|
||||
"indicators": _scan_text(text),
|
||||
"note": "legacy OLE2; install oletools for reliable extraction"}
|
||||
|
||||
|
||||
def analyze(path: str) -> dict:
|
||||
try:
|
||||
return extract_with_olevba(path)
|
||||
except ImportError:
|
||||
return extract_minimal(path)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Analyze Office macros")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
p = sub.add_parser("extract")
|
||||
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())
|
||||
Reference in New Issue
Block a user