reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a Cobalt Strike Beacon configuration by brute-forcing the XOR key.
|
||||
|
||||
Cobalt Strike Beacon stores its settings as a single-byte XOR-encoded table of TLV entries.
|
||||
This tool scans candidate keys, locates the decoded table, and parses well-known setting
|
||||
indices into a readable config. It reads bytes only and never executes the beacon.
|
||||
|
||||
This is original code based on the publicly documented config-block layout. It decodes a
|
||||
limited, well-known subset of settings for analyst triage.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Subset of well-known Beacon setting indices (type catalog is public).
|
||||
SETTINGS = {
|
||||
1: "BeaconType",
|
||||
2: "Port",
|
||||
3: "SleepTime",
|
||||
4: "MaxGetSize",
|
||||
5: "Jitter",
|
||||
7: "PublicKey",
|
||||
8: "C2Server",
|
||||
9: "UserAgent",
|
||||
10: "HttpPostUri",
|
||||
12: "HttpGet_Metadata",
|
||||
13: "HttpPost_Metadata",
|
||||
19: "SpawnTo",
|
||||
26: "PipeName",
|
||||
37: "Watermark",
|
||||
43: "DNS_Idle",
|
||||
}
|
||||
|
||||
TYPE_SHORT = 1
|
||||
TYPE_INT = 2
|
||||
TYPE_STR = 3
|
||||
|
||||
|
||||
def _parse_table(buf: bytes) -> dict | None:
|
||||
"""Parse a decoded settings table. Returns dict or None if it does not look valid."""
|
||||
out: dict = {}
|
||||
off = 0
|
||||
parsed = 0
|
||||
while off + 6 <= len(buf):
|
||||
index, vtype, length = struct.unpack_from(">HHH", buf, off)
|
||||
off += 6
|
||||
if index == 0 and vtype == 0 and length == 0:
|
||||
break
|
||||
if vtype == TYPE_SHORT and length == 2:
|
||||
(val,) = struct.unpack_from(">H", buf, off)
|
||||
elif vtype == TYPE_INT and length == 4:
|
||||
(val,) = struct.unpack_from(">I", buf, off)
|
||||
elif vtype == TYPE_STR and 0 < length <= 4096:
|
||||
val = buf[off:off + length].rstrip(b"\x00").decode("latin-1", "replace")
|
||||
else:
|
||||
return None # malformed -> wrong key
|
||||
off += length
|
||||
parsed += 1
|
||||
name = SETTINGS.get(index, f"Setting_{index}")
|
||||
out[name] = val
|
||||
if parsed > 128:
|
||||
break
|
||||
return out if parsed >= 5 else None
|
||||
|
||||
|
||||
def extract_config(data: bytes) -> dict | None:
|
||||
"""Brute-force the single-byte XOR key and parse the first valid-looking table."""
|
||||
for key in range(1, 256):
|
||||
decoded = bytes(b ^ key for b in data)
|
||||
# the table begins with index=1 (BeaconType), type=1 (short), len=2
|
||||
marker = struct.pack(">HHH", 1, TYPE_SHORT, 2)
|
||||
pos = decoded.find(marker)
|
||||
if pos == -1:
|
||||
continue
|
||||
cfg = _parse_table(decoded[pos:])
|
||||
if cfg:
|
||||
cfg["_xor_key"] = key
|
||||
return cfg
|
||||
return None
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Extract Cobalt Strike beacon config")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
p = sub.add_parser("parse")
|
||||
p.add_argument("file")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
data = Path(args.file).read_bytes()
|
||||
cfg = extract_config(data)
|
||||
if cfg is None:
|
||||
print(json.dumps({"found": False,
|
||||
"note": "no XOR-decoded config table located"}, indent=2))
|
||||
return 1
|
||||
print(json.dumps({"found": True, "config": cfg}, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user