75 lines
2.6 KiB
Python
Executable File
75 lines
2.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Hunt for malware configuration indicators in a process memory dump.
|
|
|
|
Scans a raw memory dump for URLs, IPv4:port pairs, mutex-like tokens, and base64 blobs, and
|
|
attempts single-byte XOR / base64 decoding of candidate blobs to surface hidden endpoints.
|
|
Reads bytes statically; executes nothing. Defangs recovered hosts.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import binascii
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
RE_URL = re.compile(rb"https?://[\x21-\x7e]{4,}")
|
|
RE_IPPORT = re.compile(rb"\b(?:\d{1,3}\.){3}\d{1,3}:\d{1,5}\b")
|
|
RE_MUTEX = re.compile(rb"(?:Global|Local)\\[\x21-\x7e]{4,40}")
|
|
RE_B64 = re.compile(rb"[A-Za-z0-9+/]{24,}={0,2}")
|
|
|
|
|
|
def _defang(s: str) -> str:
|
|
return s.replace("http", "hxxp").replace(".", "[.]")
|
|
|
|
|
|
def _xor_scan(blob: bytes) -> list[str]:
|
|
found = []
|
|
for key in range(1, 256):
|
|
dec = bytes(b ^ key for b in blob)
|
|
for m in RE_URL.findall(dec) + RE_IPPORT.findall(dec):
|
|
found.append(f"xor:{key:#04x} {_defang(m.decode('ascii', 'ignore'))}")
|
|
return sorted(set(found))[:20]
|
|
|
|
|
|
def hunt(path: str) -> dict:
|
|
data = Path(path).read_bytes()
|
|
urls = sorted({_defang(m.decode("ascii", "ignore")) for m in RE_URL.findall(data)})
|
|
ipports = sorted({_defang(m.decode("ascii", "ignore")) for m in RE_IPPORT.findall(data)})
|
|
mutexes = sorted({m.decode("ascii", "ignore") for m in RE_MUTEX.findall(data)})
|
|
decoded_hits = []
|
|
for blob in RE_B64.findall(data)[:2000]:
|
|
try:
|
|
raw = base64.b64decode(blob, validate=True)
|
|
except (binascii.Error, ValueError):
|
|
continue
|
|
for m in RE_URL.findall(raw) + RE_IPPORT.findall(raw):
|
|
decoded_hits.append("b64 " + _defang(m.decode("ascii", "ignore")))
|
|
return {
|
|
"urls": urls[:50],
|
|
"ip_port_pairs": ipports[:50],
|
|
"mutexes": mutexes[:50],
|
|
"base64_decoded_endpoints": sorted(set(decoded_hits))[:50],
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Hunt config indicators in a memory dump")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
p = sub.add_parser("hunt")
|
|
p.add_argument("dump")
|
|
p2 = sub.add_parser("xor")
|
|
p2.add_argument("dump")
|
|
args = parser.parse_args(argv)
|
|
if args.cmd == "xor":
|
|
print(json.dumps({"xor_endpoints": _xor_scan(Path(args.dump).read_bytes())}, indent=2))
|
|
else:
|
|
print(json.dumps(hunt(args.dump), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|