102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Brute-force common embedded-config encryption schemes and score config-like output.
|
|
|
|
Supports single-byte XOR brute force, multi-byte repeating-key XOR, and RC4. Scores each candidate
|
|
plaintext by printable ratio and presence of config tokens, and defangs recovered endpoints. Reads
|
|
the ciphertext blob statically; executes nothing.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
CONFIG_TOKENS = [b"http", b".php", b"id=", b"botnet", b"campaign", b".onion", b"User-Agent",
|
|
b"POST", b"Mozilla"]
|
|
RE_IP = re.compile(rb"\b(?:\d{1,3}\.){3}\d{1,3}\b")
|
|
RE_URL = re.compile(rb"https?://[\x21-\x7e]{4,}")
|
|
|
|
|
|
def _score(data: bytes) -> int:
|
|
printable = sum(1 for b in data if 0x20 <= b <= 0x7e or b in (9, 10, 13))
|
|
ratio = printable / max(len(data), 1)
|
|
score = int(ratio * 100)
|
|
score += 10 * sum(1 for t in CONFIG_TOKENS if t in data)
|
|
score += 5 * len(RE_IP.findall(data))
|
|
return score
|
|
|
|
|
|
def _defang(s: str) -> str:
|
|
return s.replace("http", "hxxp").replace(".", "[.]")
|
|
|
|
|
|
def _endpoints(data: bytes) -> list[str]:
|
|
out = {_defang(m.decode("ascii", "ignore")) for m in RE_URL.findall(data)}
|
|
out |= {_defang(m.decode("ascii", "ignore")) for m in RE_IP.findall(data)}
|
|
return sorted(out)
|
|
|
|
|
|
def rc4(data: bytes, key: bytes) -> bytes:
|
|
s = list(range(256))
|
|
j = 0
|
|
for i in range(256):
|
|
j = (j + s[i] + key[i % len(key)]) & 0xFF
|
|
s[i], s[j] = s[j], s[i]
|
|
out = bytearray()
|
|
i = j = 0
|
|
for b in data:
|
|
i = (i + 1) & 0xFF
|
|
j = (j + s[i]) & 0xFF
|
|
s[i], s[j] = s[j], s[i]
|
|
out.append(b ^ s[(s[i] + s[j]) & 0xFF])
|
|
return bytes(out)
|
|
|
|
|
|
def xor_brute(data: bytes) -> dict:
|
|
best = []
|
|
for key in range(256):
|
|
dec = bytes(b ^ key for b in data)
|
|
best.append((_score(dec), key, dec))
|
|
best.sort(reverse=True, key=lambda t: t[0])
|
|
top = best[:3]
|
|
return {"candidates": [
|
|
{"key": hex(k), "score": s, "endpoints": _endpoints(d),
|
|
"preview": d[:80].decode("ascii", "replace")} for s, k, d in top]}
|
|
|
|
|
|
def xor_key(data: bytes, key: bytes) -> dict:
|
|
dec = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
|
return {"score": _score(dec), "endpoints": _endpoints(dec),
|
|
"preview": dec[:120].decode("ascii", "replace")}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Decrypt embedded config blobs")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
px = sub.add_parser("xor")
|
|
px.add_argument("file")
|
|
pr = sub.add_parser("rc4")
|
|
pr.add_argument("file")
|
|
pr.add_argument("--key", required=True)
|
|
pk = sub.add_parser("xorkey")
|
|
pk.add_argument("file")
|
|
pk.add_argument("--key", required=True, help="comma list e.g. 0x11,0x22")
|
|
args = parser.parse_args(argv)
|
|
data = Path(args.file).read_bytes()
|
|
if args.cmd == "xor":
|
|
print(json.dumps(xor_brute(data), indent=2))
|
|
elif args.cmd == "rc4":
|
|
dec = rc4(data, args.key.encode())
|
|
print(json.dumps({"score": _score(dec), "endpoints": _endpoints(dec),
|
|
"preview": dec[:120].decode("ascii", "replace")}, indent=2))
|
|
else:
|
|
key = bytes(int(x, 0) for x in args.key.split(","))
|
|
print(json.dumps(xor_key(data, key), indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|