reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
Apache License 2.0 — see the repository root LICENSE file for the full text.
|
||||
|
||||
Copyright the AnalystAIPack contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: extracting-cobalt-strike-beacon-config
|
||||
description: 'Extracts and interprets a Cobalt Strike Beacon configuration: decoding the
|
||||
encoded config block, recovering C2 servers, malleable profile fields, sleep/jitter,
|
||||
watermark, and pipe names to build IOCs and attribution leads. Activates for requests to
|
||||
analyze a Cobalt Strike beacon, extract beacon config, or interpret malleable C2 settings.'
|
||||
domain: cybersecurity
|
||||
subdomain: malware-analysis
|
||||
tags:
|
||||
- malware
|
||||
- cobalt-strike
|
||||
- c2
|
||||
- beacon-config
|
||||
- malleable
|
||||
- threat-intel
|
||||
version: 1.0.0
|
||||
author: analyst-ai-pack
|
||||
license: Apache-2.0
|
||||
mitre_attack:
|
||||
- T1071.001
|
||||
- T1573
|
||||
- T1027
|
||||
- T1090
|
||||
d3fend:
|
||||
- D3-NTA
|
||||
- D3-FCR
|
||||
references:
|
||||
- 'Cobalt Strike documentation (malleable C2) — https://www.cobaltstrike.com/'
|
||||
- 'SentinelOne / community CS config parsers (concepts) — https://github.com/Sentinel-One/CobaltStrikeParser'
|
||||
---
|
||||
|
||||
# Extracting Cobalt Strike Beacon Config
|
||||
|
||||
## When to Use
|
||||
|
||||
- You have a suspected Cobalt Strike Beacon (PE, DLL, shellcode, or extracted blob) and need
|
||||
its configuration.
|
||||
- You want C2 servers, the malleable profile shape, sleep/jitter, watermark, and named pipes.
|
||||
- You are building network IOCs and attribution leads from a beacon.
|
||||
|
||||
**Do not use** an extracted config alone for hard attribution — watermarks can be shared or
|
||||
cracked. Treat it as one signal among several.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The beacon payload (or a memory dump containing it) in neutralized form inside the lab.
|
||||
- Familiarity with Cobalt Strike's encoded config block (commonly XOR-encoded with 0x2e in
|
||||
older versions, 0x69 in others; varies by version).
|
||||
|
||||
## Safety & Handling
|
||||
|
||||
- Static extraction only; do not run the beacon.
|
||||
- Defang recovered C2 hosts/URIs before sharing.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Locate the encoded config block
|
||||
|
||||
Beacon stores settings as a packed, XOR-encoded table of TLV-like entries. Brute the
|
||||
single-byte XOR key by scanning for the decoded table's known opening bytes (the first
|
||||
setting index/type/length pattern).
|
||||
|
||||
### Step 2: Decode and parse settings
|
||||
|
||||
Once the key is found, walk the entries. Each has an index (setting type), a type
|
||||
(short/int/string), and a length. The script decodes and maps known indices:
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py parse beacon.bin
|
||||
```
|
||||
|
||||
### Step 3: Recover key fields
|
||||
|
||||
Pull the high-value settings:
|
||||
|
||||
```text
|
||||
C2 servers / GET-URI / POST-URI -> network IOCs
|
||||
User-Agent -> detection signal
|
||||
Sleep time / jitter -> beacon cadence
|
||||
Watermark -> license/team grouping lead
|
||||
Pipe name / process-inject targets -> host IOCs
|
||||
Spawn-to / DNS settings -> behavior
|
||||
```
|
||||
|
||||
### Step 4: Interpret the malleable profile
|
||||
|
||||
The URIs, headers, and transform settings reveal which malleable C2 profile the operator used
|
||||
(e.g. masquerading as a known web service) — useful for network detection.
|
||||
|
||||
### Step 5: Build IOCs
|
||||
|
||||
Emit network IOCs (hosts, URIs, UA), host IOCs (pipe names), and the watermark for tracking.
|
||||
|
||||
## Validation
|
||||
|
||||
- The recovered C2 host(s) and URIs are well-formed and consistent with the profile.
|
||||
- Sleep/jitter values are plausible and match any observed beaconing intervals.
|
||||
- The same config reproduces from an independent extractor or memory copy.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Hardcoding one XOR key — it varies across versions; brute-force per sample.
|
||||
- Confusing a watermark match with attribution; cracked/leaked kits share watermarks.
|
||||
- Stopping at the first C2 when the config lists multiple/backup servers.
|
||||
|
||||
## References
|
||||
|
||||
- See [`references/api-reference.md`](references/api-reference.md) for the config parser.
|
||||
- Cobalt Strike malleable C2 docs and community parser concepts (linked in frontmatter).
|
||||
@@ -0,0 +1,49 @@
|
||||
# API Reference: Analyzing Cobalt Strike Beacon Config
|
||||
|
||||
## Overview
|
||||
|
||||
`scripts/analyst.py` brute-forces the single-byte XOR key protecting a Beacon configuration
|
||||
block, locates the TLV settings table, and parses a well-known subset of settings (C2 server,
|
||||
URIs, sleep/jitter, watermark, pipe name). It reads bytes only.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| (stdlib) | — | struct, JSON |
|
||||
|
||||
## External tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| CobaltStrikeParser (community) | Reference config parsing across versions |
|
||||
| 1768.py (Didier Stevens) | Alternative beacon config extractor |
|
||||
|
||||
## Core functions
|
||||
|
||||
### `extract_config(data)`
|
||||
Tries XOR keys 1–255, finds the decoded settings table (anchored on the BeaconType entry),
|
||||
and returns the parsed config plus the recovered key.
|
||||
|
||||
### `_parse_table(buf)`
|
||||
Walks TLV entries (index, type, length, value), mapping known indices to names. Returns
|
||||
`None` on malformed data so the brute-force can reject wrong keys.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py parse beacon.bin
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
The setting index catalog and XOR-encoded table layout are publicly documented. This parser
|
||||
intentionally decodes a limited subset for triage; use a full community parser for complete,
|
||||
version-specific extraction.
|
||||
|
||||
## Sources
|
||||
|
||||
- Cobalt Strike malleable C2 documentation — https://www.cobaltstrike.com/
|
||||
- CobaltStrikeParser (community reference for config layout) —
|
||||
https://github.com/Sentinel-One/CobaltStrikeParser
|
||||
- Publicly documented Beacon config TLV structure
|
||||
@@ -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