82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Analyze a loader/dropper: locate staged payloads and identify decode/download/execution APIs.
|
|
|
|
Detects embedded PE/archive signatures, scans strings for download/decode/execution APIs and
|
|
URLs, and computes Shannon entropy over windows to flag large high-entropy (packed/encrypted)
|
|
blobs. Reads bytes statically; executes nothing.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
ASCII_RE = re.compile(rb"[\x20-\x7e]{4,}")
|
|
UTF16_RE = re.compile(rb"(?:[\x20-\x7e]\x00){4,}")
|
|
RE_URL = re.compile(r"https?://[^\s\"'<>]+", re.I)
|
|
|
|
EMBED_SIGS = [(b"MZ", "PE"), (b"PK\x03\x04", "ZIP/Office"), (b"Rar!\x1a\x07", "RAR"),
|
|
(b"\x1f\x8b", "GZIP"), (b"7z\xbc\xaf\x27\x1c", "7z")]
|
|
|
|
DOWNLOAD_API = ["urldownloadtofile", "internetreadfile", "internetopenurl", "winhttp",
|
|
"httpopenrequest", "wininet"]
|
|
DECODE_API = ["cryptdecrypt", "cryptstringtobinary", "base64", "rc4", "xor"]
|
|
EXEC_API = ["createprocess", "shellexecute", "winexec", "createremotethread", "loadlibrary",
|
|
"ntcreateuserprocess", "wow64"]
|
|
|
|
|
|
def _strings(data: bytes) -> str:
|
|
parts = [m.decode("ascii", "ignore") for m in ASCII_RE.findall(data)]
|
|
parts += [m.decode("utf-16-le", "ignore") for m in UTF16_RE.findall(data)]
|
|
return "\n".join(parts)
|
|
|
|
|
|
def _entropy(chunk: bytes) -> float:
|
|
if not chunk:
|
|
return 0.0
|
|
counts = Counter(chunk)
|
|
total = len(chunk)
|
|
return -sum((c / total) * math.log2(c / total) for c in counts.values())
|
|
|
|
|
|
def stage(path: str) -> dict:
|
|
data = Path(path).read_bytes()
|
|
embedded = []
|
|
for sig, label in EMBED_SIGS:
|
|
idx = data.find(sig, 1) # skip offset 0 (the loader's own header)
|
|
if idx != -1:
|
|
embedded.append({"offset": idx, "type": label})
|
|
win = 4096
|
|
high_entropy = sum(1 for i in range(0, len(data), win)
|
|
if _entropy(data[i:i + win]) > 7.2)
|
|
blob = _strings(data)
|
|
low = blob.lower()
|
|
urls = sorted({u.replace("http", "hxxp").replace(".", "[.]") for u in RE_URL.findall(blob)})
|
|
return {
|
|
"embedded_signatures": embedded,
|
|
"high_entropy_windows": high_entropy,
|
|
"download_apis": sorted({a for a in DOWNLOAD_API if a in low}),
|
|
"decode_apis": sorted({a for a in DECODE_API if a in low}),
|
|
"execution_apis": sorted({a for a in EXEC_API if a in low}),
|
|
"urls": urls[:50],
|
|
"is_likely_loader": bool(embedded) or bool(urls and any(a in low for a in DOWNLOAD_API)),
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Analyze loader/dropper staging")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("stage")
|
|
p.add_argument("file")
|
|
args = parser.parse_args(argv)
|
|
print(json.dumps(stage(args.file), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|