dissect.cobaltstrike 1.2.1.dev8__tar.gz → 1.2.1.dev10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/PKG-INFO +2 -2
  2. dissect_cobaltstrike-1.2.1.dev10/dissect/cobaltstrike/_version.py +21 -0
  3. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/beacon.py +80 -60
  4. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/c2profile.lark +5 -0
  5. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/c2profile.py +10 -0
  6. dissect_cobaltstrike-1.2.1.dev10/dissect/cobaltstrike/guardrails.py +195 -0
  7. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/utils.py +8 -0
  8. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/version.py +2 -0
  9. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/conftest.py +2 -0
  10. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_beacon.py +42 -0
  11. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_c2profile.py +22 -0
  12. dissect_cobaltstrike-1.2.1.dev10/tests/test_guardrails.py +60 -0
  13. dissect_cobaltstrike-1.2.1.dev8/dissect/cobaltstrike/_version.py +0 -16
  14. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/.github/workflows/dissect-ci.yml +0 -0
  15. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/.gitignore +0 -0
  16. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/.lfs-assets-id +0 -0
  17. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/.pre-commit-config.yaml +0 -0
  18. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/.readthedocs.yml +0 -0
  19. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/LICENSE +0 -0
  20. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/README.rst +0 -0
  21. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/codecov.yml +0 -0
  22. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/__init__.py +0 -0
  23. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/artifact.py +0 -0
  24. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/c2.py +0 -0
  25. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/c_c2.py +0 -0
  26. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/client.py +0 -0
  27. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/pcap.py +0 -0
  28. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/pe.py +0 -0
  29. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/dissect/cobaltstrike/xordecode.py +0 -0
  30. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/Makefile +0 -0
  31. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/c2profile-grammar.rst +0 -0
  32. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/codespell-ignore-words.txt +0 -0
  33. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/conf.py +0 -0
  34. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/examples.rst +0 -0
  35. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/images/beacon-client.png +0 -0
  36. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/images/teamserver-echoclient.png +0 -0
  37. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/images/teamserver_ls.png +0 -0
  38. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/images/vt-cobaltstrike-43-zip.png +0 -0
  39. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/index.rst +0 -0
  40. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/installation.rst +0 -0
  41. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/license.rst +0 -0
  42. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/scripts/checksum8-accesslogs.rst +0 -0
  43. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/scripts/dump_beacon_keys.rst +0 -0
  44. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/scripts/example_client.rst +0 -0
  45. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/scripts/index.rst +0 -0
  46. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/structures/c2_def.rst +0 -0
  47. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/structures/cs_def.rst +0 -0
  48. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/structures/index.rst +0 -0
  49. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/structures/pe_def.rst +0 -0
  50. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tools/beacon-artifact.rst +0 -0
  51. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tools/beacon-client.rst +0 -0
  52. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tools/beacon-dump.rst +0 -0
  53. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tools/beacon-pcap.rst +0 -0
  54. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tools/beacon-xordecode.rst +0 -0
  55. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tools/c2profile-dump.rst +0 -0
  56. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tutorials/decrypt_cobaltstrike_pcaps.rst +0 -0
  57. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tutorials/index.rst +0 -0
  58. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/docs/tutorials/minimal_beacon_client.rst +0 -0
  59. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/pyproject.toml +0 -0
  60. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/scripts/checksum8-accesslogs.py +0 -0
  61. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/scripts/dump_beacon_keys.py +0 -0
  62. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/scripts/example_client.py +0 -0
  63. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_c2.py +0 -0
  64. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_client.py +0 -0
  65. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_pe.py +0 -0
  66. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_utils.py +0 -0
  67. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_version.py +0 -0
  68. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tests/test_xordecode.py +0 -0
  69. {dissect_cobaltstrike-1.2.1.dev8 → dissect_cobaltstrike-1.2.1.dev10}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: dissect.cobaltstrike
3
- Version: 1.2.1.dev8
3
+ Version: 1.2.1.dev10
4
4
  Summary: a Python library for dissecting Cobalt Strike related data
5
5
  Project-URL: Homepage, https://github.com/fox-it/dissect.cobaltstrike
6
6
  Project-URL: Documentation, https://dissect-cobaltstrike.readthedocs.io/
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '1.2.1.dev10'
21
+ __version_tuple__ = version_tuple = (1, 2, 1, 'dev10')
@@ -9,7 +9,6 @@ import functools
9
9
  import hashlib
10
10
  import io
11
11
  import ipaddress
12
- import itertools
13
12
  import logging
14
13
  import sys
15
14
  import time
@@ -36,8 +35,10 @@ from typing import (
36
35
 
37
36
  from dissect import cstruct
38
37
  from dissect.cobaltstrike import pe
38
+ from dissect.cobaltstrike.guardrails import GuardrailMetadata, iter_guardrail_configs_with_beacon
39
39
  from dissect.cobaltstrike.utils import (
40
40
  catch_sigpipe,
41
+ grouper,
41
42
  iter_find_needle,
42
43
  p8,
43
44
  u16be,
@@ -175,6 +176,7 @@ enum BeaconSetting: uint16 {
175
176
  SETTING_DATA_STORE_SIZE = 76,
176
177
 
177
178
  // CobaltStrike version >= 4.10 (Jul 16, 2024)
179
+ SETTING_HTTP_DATA_REQUIRED = 77,
178
180
  SETTING_BEACON_GATE = 78,
179
181
  };
180
182
 
@@ -463,13 +465,6 @@ def iter_settings(fobj: Union[bytes, BinaryIO]) -> Iterator["Setting"]:
463
465
  yield setting
464
466
 
465
467
 
466
- def grouper(iterable, n, fillvalue=None):
467
- "Collect data into fixed-length chunks or blocks"
468
- # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
469
- args = [iter(iterable)] * n
470
- return itertools.zip_longest(*args, fillvalue=fillvalue)
471
-
472
-
473
468
  def parse_recover_binary(program: bytes) -> List[Tuple[str, Union[int, bool]]]:
474
469
  """Parse ``SETTING_C2_RECOVER`` (`.http-get.server.output`) data"""
475
470
  rsteps: List[Tuple[str, Union[int, bool]]] = []
@@ -772,6 +767,8 @@ class BeaconConfig:
772
767
  """ PE compile timestamp, ``None`` if unknown. """
773
768
  self.architecture: Optional[str] = None
774
769
  """ PE architecture, ``"x86"`` or ``"x64"`` and ``None`` if unknown. """
770
+ self.guardrails: Optional[GuardrailMetadata] = None
771
+ """ Guardrails metadata, ``None`` if not available. """
775
772
 
776
773
  # Used for caching
777
774
  self._settings: Optional[Mapping[str, Any]] = None
@@ -808,6 +805,22 @@ class BeaconConfig:
808
805
  bconfig.architecture = pe.find_architecture(fh)
809
806
  # Return the first found beacon config.
810
807
  return bconfig
808
+
809
+ # Try finding Beacon config protected with Guardrails
810
+ try:
811
+ fxor = XorEncodedFile.from_file(fobj)
812
+ except ValueError:
813
+ fxor = fobj
814
+ for grconfig in iter_guardrail_configs_with_beacon(fxor):
815
+ if not grconfig.unmasked_beacon_config:
816
+ continue
817
+ bconfig = cls(grconfig.unmasked_beacon_config)
818
+ bconfig.guardrails = grconfig
819
+ bconfig.xorkey = grconfig.beacon_xor_key
820
+ bconfig.pe_compile_stamp, bconfig.pe_export_stamp = pe.find_compile_stamps(fxor)
821
+ bconfig.architecture = pe.find_architecture(fxor)
822
+ return bconfig
823
+
811
824
  raise ValueError("No valid Beacon configuration found")
812
825
 
813
826
  @classmethod
@@ -1107,7 +1120,7 @@ def build_parser():
1107
1120
  import argparse
1108
1121
 
1109
1122
  parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
1110
- parser.add_argument("input", metavar="FILE", help="Beacon to dump")
1123
+ parser.add_argument("input", metavar="FILE", nargs="+", help="Beacon to dump")
1111
1124
  parser.add_argument(
1112
1125
  "-x",
1113
1126
  "--xorkey",
@@ -1115,10 +1128,9 @@ def build_parser():
1115
1128
  help="override default xor key(s) (default: -x 0x69 -x 0x2e -x 0x00)",
1116
1129
  )
1117
1130
  parser.add_argument(
1118
- "-a",
1119
- "--all",
1131
+ "--default-xor-keys-only",
1120
1132
  action="store_true",
1121
- help="try all other single byte xor keys when default ones fail",
1133
+ help="only try the default xor keys instead of all possible ones",
1122
1134
  )
1123
1135
  parser.add_argument(
1124
1136
  "-t",
@@ -1149,63 +1161,71 @@ def main():
1149
1161
  level = levels[min(len(levels) - 1, args.verbose)]
1150
1162
  logging.basicConfig(
1151
1163
  level=level,
1152
- datefmt="[%X]",
1153
- format="%(asctime)s %(name)s %(message)s",
1164
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
1154
1165
  )
1155
1166
 
1156
1167
  xor_keys = None
1157
1168
  if args.xorkey:
1158
1169
  xor_keys = tuple(utils.pack_be(int(x, 0)) for x in args.xorkey)
1159
1170
 
1160
- try:
1161
- if args.input in ("-", "/dev/stdin"):
1162
- with io.BytesIO(sys.stdin.buffer.read()) as fin:
1163
- config = BeaconConfig.from_file(fin, xor_keys=xor_keys, all_xor_keys=args.all)
1164
- else:
1165
- config = BeaconConfig.from_path(args.input, xor_keys=xor_keys, all_xor_keys=args.all)
1166
- except ValueError:
1167
- print(f"{args.input}: No beacon configuration found.", file=sys.stderr)
1168
- return 1
1169
-
1170
- if args.type == "raw":
1171
- for setting in config.settings_tuple:
1172
- print(setting)
1173
- elif args.type == "dumpstruct":
1174
- cstruct.hexdump(config.config_block)
1175
- print("-----")
1176
- for setting in config.settings_tuple:
1177
- cstruct.dumpstruct(setting)
1178
- print("-" * 10)
1179
- elif args.type == "normal":
1180
- settings = config.settings
1181
- for setting, value in settings.items():
1182
- print(f"{setting} = {value!r}")
1183
- if args.verbose >= 1:
1184
- print("-" * 50)
1185
- print(
1186
- "pe_export_stamp = {}, {}, {}".format(
1187
- config.pe_export_stamp,
1188
- hex(config.pe_export_stamp),
1189
- time.ctime(config.pe_export_stamp),
1171
+ dumped = False
1172
+ for fname in args.input:
1173
+ logging.info("Processing: %r", fname)
1174
+ try:
1175
+ if fname in ("-", "/dev/stdin"):
1176
+ with io.BytesIO(sys.stdin.buffer.read()) as fin:
1177
+ config = BeaconConfig.from_file(fin, xor_keys=xor_keys, all_xor_keys=not args.default_xor_keys_only)
1178
+ else:
1179
+ config = BeaconConfig.from_path(fname, xor_keys=xor_keys, all_xor_keys=not args.default_xor_keys_only)
1180
+ except ValueError:
1181
+ print(f"{fname}: No beacon configuration found.", file=sys.stderr)
1182
+ continue
1183
+
1184
+ dumped = True
1185
+ if args.type == "raw":
1186
+ for setting in config.settings_tuple:
1187
+ print(setting)
1188
+ elif args.type == "dumpstruct":
1189
+ cstruct.hexdump(config.config_block)
1190
+ print("-----")
1191
+ for setting in config.settings_tuple:
1192
+ cstruct.dumpstruct(setting)
1193
+ print("-" * 10)
1194
+ elif args.type == "normal":
1195
+ settings = config.settings
1196
+ for setting, value in settings.items():
1197
+ print(f"{setting} = {value!r}")
1198
+ if args.verbose >= 1:
1199
+ print("-" * 50)
1200
+ print(
1201
+ "pe_export_stamp = {}, {}, {}".format(
1202
+ config.pe_export_stamp,
1203
+ hex(config.pe_export_stamp),
1204
+ time.ctime(config.pe_export_stamp),
1205
+ )
1190
1206
  )
1191
- )
1192
- print(
1193
- "pe_compile_stamp = {}, {}, {}".format(
1194
- config.pe_compile_stamp,
1195
- hex(config.pe_compile_stamp),
1196
- time.ctime(config.pe_compile_stamp),
1207
+ print(
1208
+ "pe_compile_stamp = {}, {}, {}".format(
1209
+ config.pe_compile_stamp,
1210
+ hex(config.pe_compile_stamp),
1211
+ time.ctime(config.pe_compile_stamp),
1212
+ )
1197
1213
  )
1198
- )
1199
- print(
1200
- "max_setting_enum = {} - {}".format(
1201
- config.max_setting_enum,
1202
- BeaconSetting(config.max_setting_enum),
1214
+ print(
1215
+ "max_setting_enum = {} - {}".format(
1216
+ config.max_setting_enum,
1217
+ BeaconSetting(config.max_setting_enum),
1218
+ )
1203
1219
  )
1204
- )
1205
- print("beacon_version =", config.version)
1206
- elif args.type == "c2profile":
1207
- profile = c2profile.C2Profile.from_beacon_config(config)
1208
- print(profile.as_text())
1220
+ print("beacon_version =", config.version)
1221
+ if config.guardrails:
1222
+ print("guardrail payload xor key =", config.guardrails.payload_xor_key)
1223
+ print("guardrail options =", [s.option for s in config.guardrails.settings])
1224
+ elif args.type == "c2profile":
1225
+ profile = c2profile.C2Profile.from_beacon_config(config)
1226
+ print(profile.as_text())
1227
+
1228
+ return 0 if dumped else 1
1209
1229
 
1210
1230
 
1211
1231
  if __name__ == "__main__":
@@ -11,6 +11,7 @@ start: value*
11
11
  | "process-inject" "{" process_inject_options* "}" -> process_inject
12
12
  | "post-ex" "{" postex_options* "}" -> post_ex
13
13
  | "dns-beacon" "{" dns_beacon_options* "}" -> dns_beacon
14
+ | "http-beacon" "{" http_beacon_options* "}" -> http_beacon
14
15
 
15
16
  OPTION: "sample_name"
16
17
  | "data_jitter"
@@ -214,6 +215,10 @@ dns_beacon_options: "set" "dns_idle" string ";" -> dns_idle
214
215
  | "set" "ns_response" string ";" -> ns_response
215
216
  | "#" "dns_resolver" string ";" -> comment_dns_resolver
216
217
 
218
+ http_beacon_options: "set" "library" string ";" -> library // introduced in Cobalt Strike 4.9
219
+ | "set" "data_required" string ";" -> data_required // introduced in Cobalt Strike 4.10
220
+ | "set" "data_required_length" string ";" -> data_required_length // introduced in Cobalt Strike 4.10
221
+
217
222
  header: string
218
223
  string: STRING
219
224
  variant: string
@@ -309,6 +309,12 @@ class DnsBeaconBlock(ConfigBlock):
309
309
  __name__ = "dns_beacon"
310
310
 
311
311
 
312
+ class HttpBeaconBlock(ConfigBlock):
313
+ """`.http-beacon` block"""
314
+
315
+ __name__ = "http_beacon"
316
+
317
+
312
318
  class ExecuteOptionsBlock(ConfigBlock):
313
319
  """`.process-inject.execute` block"""
314
320
 
@@ -446,6 +452,7 @@ class C2Profile(ConfigBlock):
446
452
  http_post_client = HttpOptionsBlock()
447
453
  proc_inj = ProcessInjectBlock()
448
454
  dns_beacon = DnsBeaconBlock()
455
+ http_beacon = HttpBeaconBlock()
449
456
  # http_get_server = HttpOptionsBlock()
450
457
 
451
458
  for setting, value in config.settings_by_index.items():
@@ -668,6 +675,8 @@ class C2Profile(ConfigBlock):
668
675
  proc_inj.set_option("bof_allocator", value)
669
676
  elif setting == BeaconSetting.SETTING_DATA_STORE_SIZE:
670
677
  stage.set_option("data_store_size", value)
678
+ elif setting == BeaconSetting.SETTING_HTTP_DATA_REQUIRED and value:
679
+ http_beacon.set_option("data_required", "true")
671
680
  elif setting == BeaconSetting.SETTING_BEACON_GATE and value:
672
681
  block = BeaconGateBlock.from_beacon_gate_option_strings(value)
673
682
  stage.set_config_block("beacon_gate", block)
@@ -681,6 +690,7 @@ class C2Profile(ConfigBlock):
681
690
  profile.set_non_empty_config_block("stage", stage)
682
691
  profile.set_non_empty_config_block("process_inject", proc_inj)
683
692
  profile.set_non_empty_config_block("dns_beacon", dns_beacon)
693
+ profile.set_non_empty_config_block("http_beacon", http_beacon)
684
694
  return profile
685
695
 
686
696
  def __str__(self) -> str:
@@ -0,0 +1,195 @@
1
+ """
2
+ This module is responsible for finding and recovering Beacon Guardrails configuration from Cobalt Strike payloads.
3
+ Guardrails is an additional layer of protection to the beacon config by using environmental keying (`T1480`_).
4
+
5
+ Beacon Guardrails was introduced in Cobalt Strike 4.8:
6
+
7
+ - https://www.cobaltstrike.com/blog/cobalt-strike-4-8-system-call-me-maybe
8
+
9
+ Other research on Beacon Guardrails:
10
+
11
+ - https://itea.org/journals/volume-45-3/cobalt-strike-cyber-assessment-challenge/
12
+
13
+ .. _T1480: https://attack.mitre.org/techniques/T1480/
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import collections
19
+ import functools
20
+ import io
21
+ import logging
22
+ from dataclasses import dataclass
23
+ from typing import TYPE_CHECKING, BinaryIO
24
+
25
+ from dissect.cobaltstrike.utils import grouper, u32be, xor
26
+ from dissect.cstruct import cstruct
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Iterator
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+ C_GUARDRAILS_DEF = """
34
+ enum GuardOption: uint16 {
35
+ GUARD_USER = 5,
36
+ GUARD_COMPUTER = 6,
37
+ GUARD_DOMAIN = 7,
38
+ GUARD_LOCAL_IP = 8,
39
+ GUARD_PAYLOAD_CHECKSUM = 9,
40
+ };
41
+
42
+ enum SettingsType: uint16 {
43
+ TYPE_NONE = 0,
44
+ TYPE_SHORT = 1,
45
+ TYPE_INT = 2,
46
+ TYPE_PTR = 3,
47
+ };
48
+
49
+ struct GuardrailSetting {
50
+ GuardOption option; // uint16
51
+ SettingsType type; // uint16
52
+ uint16 length; // uint16
53
+ char value[length];
54
+ };
55
+ """
56
+
57
+ BEACON_CONFIG_PATCH_SIZE = 6144
58
+ GUARD_PATCH_SIZE = 2048
59
+
60
+ GUARD_CONFIG_STARTS = [
61
+ b"\x00\x05\x00\x01\x00\x02", # GUARD_USER
62
+ b"\x00\x06\x00\x01\x00\x02", # GUARD_COMPUTER
63
+ b"\x00\x07\x00\x01\x00\x02", # GUARD_DOMAIN
64
+ b"\x00\x08\x00\x02\x00\x04", # GUARD_LOCAL_IP
65
+ ]
66
+
67
+ c_guardrails = cstruct(endian=">").load(C_GUARDRAILS_DEF)
68
+ GuardrailSetting = c_guardrails.GuardrailSetting
69
+ GuardOption = c_guardrails.GuardOption
70
+
71
+
72
+ @dataclass
73
+ class GuardrailMetadata:
74
+ """Class for holding Guardrail related data"""
75
+
76
+ beacon_config_offset: int
77
+ """ Offset of the beacon configuration in the payload """
78
+ guard_config_offset: int
79
+ """ Offset of the guardrail configuration in the payload """
80
+ masked_beacon_config: bytes
81
+ """ Masked raw beacon configuration """
82
+ masked_guard_config: bytes
83
+ """ Masked raw guardrail configuration """
84
+ beacon_xor_key: bytes
85
+ """ Single byte XOR key used to mask the beacon configuration. (0x2e by default unless modified beacon) """
86
+ guardrail_xor_key: bytes
87
+ """ Single byte XOR key used to unmask the guardrail configuration (0x8a by default unless modified beacon) """
88
+ unmasked_guard_config: bytes
89
+ """ Unmasked guardrail configuration """
90
+ checksum: int
91
+ """ Extracted payload checksum from guardrail configuration. This is used to validate the beacon configuration """
92
+ payload_xor_key: bytes | None
93
+ """ XOR key used to unmask the guarded beacon configuration. This is the environmental key """
94
+ unmasked_beacon_config: bytes
95
+ """ Unmasked beacon configuration """
96
+ settings: list[GuardrailSetting]
97
+ """ List of guardrail settings """
98
+
99
+
100
+ def iter_guardrail_configs(fh: BinaryIO, xorkey: bytes = b"\x8a") -> Iterator[GuardrailMetadata]:
101
+ xorred_guardconfig_starts = [xor(x, xorkey) for x in GUARD_CONFIG_STARTS]
102
+ size = len(xorred_guardconfig_starts[0])
103
+ offset = 0
104
+ while True:
105
+ fh.seek(offset)
106
+ block = fh.read(size * 2)
107
+ if not block:
108
+ break
109
+ a, b = block[:size], block[size:]
110
+ if xor(a[::-1], b) in xorred_guardconfig_starts:
111
+ log.info("Found guardrail config at offset: %u in %r", offset, fh)
112
+ guard_config_offset = offset + 6
113
+ beacon_config_offset = guard_config_offset - BEACON_CONFIG_PATCH_SIZE
114
+ fh.seek(beacon_config_offset)
115
+ masked_beacon_config = fh.read(BEACON_CONFIG_PATCH_SIZE)
116
+ masked_guard_config = fh.read(GUARD_PATCH_SIZE)
117
+ unmasked_guard_config = xor(xor(masked_guard_config, masked_beacon_config[::-1]), xorkey)
118
+
119
+ fh_guard = io.BufferedReader(io.BytesIO(unmasked_guard_config))
120
+ checksum = 0
121
+ settings: list[GuardrailSetting] = []
122
+ while True:
123
+ if fh_guard.peek(2)[:2] == b"\x00\x00":
124
+ break
125
+ setting = GuardrailSetting(fh_guard)
126
+ settings.append(setting)
127
+ log.debug(setting)
128
+ if setting.option == GuardOption.GUARD_PAYLOAD_CHECKSUM:
129
+ checksum = u32be(setting.value)
130
+ log.debug("%s = 0x%08x", setting.option.name, checksum)
131
+
132
+ yield GuardrailMetadata(
133
+ beacon_config_offset=beacon_config_offset,
134
+ guard_config_offset=guard_config_offset,
135
+ checksum=checksum,
136
+ masked_guard_config=masked_guard_config,
137
+ masked_beacon_config=masked_beacon_config,
138
+ unmasked_guard_config=unmasked_guard_config,
139
+ guardrail_xor_key=xorkey,
140
+ beacon_xor_key=b"\x2e", # we currently only support the XOR default key
141
+ payload_xor_key=None,
142
+ unmasked_beacon_config=None,
143
+ settings=settings,
144
+ )
145
+ offset += 1
146
+
147
+
148
+ def find_xor_key_candidates(fh: BinaryIO) -> Iterator[bytes]:
149
+ for keylen in range(2, 257):
150
+ fh.seek(0)
151
+ counter = collections.Counter()
152
+ for chunk in iter(functools.partial(fh.read, io.DEFAULT_BUFFER_SIZE), b""):
153
+ grams = grouper(chunk, n=keylen, fillvalue=0)
154
+ counter.update(bytes(gram) for gram in grams)
155
+
156
+ first_count = 0
157
+ for key, count in counter.most_common(2):
158
+ if count >= first_count:
159
+ first_count = count
160
+ yield key
161
+ else:
162
+ break
163
+
164
+
165
+ def payload_checksum(data: bytes) -> int:
166
+ n = 0
167
+ for i in range(len(data)):
168
+ n = (n + (data[i] & 0xFF) * (i % 3 + 1)) % 99999999
169
+ return n
170
+
171
+
172
+ def iter_guardrail_configs_with_beacon(fh: BinaryIO) -> Iterator[GuardrailMetadata]:
173
+ for grconfig in iter_guardrail_configs(fh):
174
+
175
+ # Unmask the beacon config, static single byte xor key should be 0x2E unless modified beacon
176
+ # The beacon config is still masked with the environmental key
177
+ grconfig.beacon_xor_key = b"\x2e" # we currently only support the XOR default key
178
+ guarded_config = xor(grconfig.masked_beacon_config, grconfig.beacon_xor_key)
179
+
180
+ for xorkey in find_xor_key_candidates(io.BytesIO(guarded_config)):
181
+ unguarded = xor(guarded_config, xorkey)
182
+
183
+ checksum = payload_checksum(unguarded) + 1
184
+ log.debug("payload checksum: 0x%08x for xorkey: %r", checksum, xorkey)
185
+
186
+ if grconfig.checksum == checksum:
187
+ log.info("Found guardrail payload xorkey: %r", xorkey)
188
+ grconfig.payload_xor_key = xorkey
189
+ grconfig.unmasked_beacon_config = unguarded
190
+ yield grconfig
191
+ break
192
+ else:
193
+ # No valid xor key found, so not able to unmask the beacon config
194
+ # but we can still return the guardrail config
195
+ yield grconfig
@@ -4,6 +4,7 @@ This module contains generic helper functions used by ``dissect.cobaltstrike``.
4
4
 
5
5
  import errno
6
6
  import io
7
+ import itertools
7
8
  import os
8
9
  import random
9
10
  import re
@@ -280,3 +281,10 @@ class LRUDict(OrderedDict):
280
281
  if len(self) > self.maxsize:
281
282
  oldest = next(iter(self))
282
283
  del self[oldest]
284
+
285
+
286
+ def grouper(iterable, n, fillvalue=None):
287
+ "Collect data into fixed-length chunks or blocks"
288
+ # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
289
+ args = [iter(iterable)] * n
290
+ return itertools.zip_longest(*args, fillvalue=fillvalue)
@@ -80,11 +80,13 @@ PE_EXPORT_STAMP_TO_VERSION: Dict[int, str] = {
80
80
  0x63EE056C: "Cobalt Strike 4.8 (Feb 28, 2023)",
81
81
  0x63EE0587: "Cobalt Strike 4.8 (Feb 28, 2023)",
82
82
  0x64F88C5E: "Cobalt Strike 4.9 (Sep 19, 2023)",
83
+ 0x64F88C7E: "Cobalt Strike 4.9 (Sep 19, 2023)",
83
84
  0x64F88C9E: "Cobalt Strike 4.9 (Sep 19, 2023)",
84
85
  0x64F88CDE: "Cobalt Strike 4.9 (Sep 19, 2023)",
85
86
  0x6691500F: "Cobalt Strike 4.10 (Jul 16, 2024)",
86
87
  0x66915020: "Cobalt Strike 4.10 (Jul 16, 2024)",
87
88
  0x66915022: "Cobalt Strike 4.10 (Jul 16, 2024)",
89
+ 0x66915024: "Cobalt Strike 4.10 (Jul 16, 2024)",
88
90
  }
89
91
  """ PE export timestamp to Cobalt Strike version mapping """
90
92
 
@@ -19,6 +19,8 @@ beacons = {
19
19
  "c2test_beacon": "37882262c9b5e971067fd989b26afe28.bin.zip",
20
20
  # beacon with unicode in domain
21
21
  "punycode_beacon": "5a197a8bb628a2555f5a86c51b85abd7.bin.zip",
22
+ # guardrails protected beacon
23
+ "guardrails_beacon": "124552cf674b362e0c916ab79b9e7a56.bin.zip",
22
24
  }
23
25
 
24
26
 
@@ -323,3 +323,45 @@ def test_beacon_setting_unknown_enum():
323
323
  config = beacon.BeaconConfig(data)
324
324
  assert None not in config.settings
325
325
  assert dict(config.settings) == {"BeaconSetting_6969": b"foo"}
326
+
327
+
328
+ def test_beacon_dump_multiple_files(beacon_x86_path, beacon_x64_path):
329
+ proc = subprocess.run(
330
+ [sys.executable, "-m", "dissect.cobaltstrike.beacon", str(beacon_x86_path), str(beacon_x64_path)],
331
+ capture_output=True,
332
+ )
333
+ proc.check_returncode()
334
+ assert "9b9e85b111d9bef8d599905a06be0d207c388c4acaab8e74a01c04406fe26309" in proc.stdout.decode()
335
+ assert "71fab2149cbdce552f00e6d75372494d3f7755d366fd6849a6d5c9e0f73bc40f" in proc.stdout.decode()
336
+
337
+
338
+ def test_beacon_dump_default_xor_keys_only(beacon_custom_xorkey_path):
339
+ # default behavior is to try all xor keys, and we should find a valid beacon
340
+ proc = subprocess.run(
341
+ [
342
+ sys.executable,
343
+ "-m",
344
+ "dissect.cobaltstrike.beacon",
345
+ str(beacon_custom_xorkey_path),
346
+ ],
347
+ capture_output=True,
348
+ )
349
+ assert proc.returncode == 0
350
+ assert b"SETTING_PUBKEY = '36aff0b273cb7aa704e4219ad3be78defcc8c1d7ecb779d55f438e82c7138673'" in proc.stdout
351
+ proc.check_returncode()
352
+
353
+ # when we enable --default-xor-keys-only, we should not find a valid beacon
354
+ proc = subprocess.run(
355
+ [
356
+ sys.executable,
357
+ "-m",
358
+ "dissect.cobaltstrike.beacon",
359
+ str(beacon_custom_xorkey_path),
360
+ "--default-xor-keys-only",
361
+ ],
362
+ capture_output=True,
363
+ )
364
+ assert proc.returncode == 1
365
+ assert b"No beacon configuration found" in proc.stderr
366
+ with pytest.raises(subprocess.CalledProcessError):
367
+ proc.check_returncode()
@@ -395,3 +395,25 @@ def test_c2profile_beacon_gate():
395
395
  bconfig = beacon.BeaconConfig(data)
396
396
  profile = c2profile.C2Profile.from_beacon_config(bconfig)
397
397
  assert "stage.beacon_gate" not in profile.properties
398
+
399
+
400
+ @pytest.mark.parametrize(
401
+ ("http_data_required",),
402
+ [
403
+ (True,),
404
+ (False,),
405
+ ],
406
+ )
407
+ def test_c2profile_http_data_required(http_data_required: bool):
408
+ data = beacon.Setting(
409
+ index=beacon.BeaconSetting.SETTING_HTTP_DATA_REQUIRED,
410
+ type=beacon.SettingsType.TYPE_SHORT,
411
+ length=0x2,
412
+ value=beacon.cs_struct.uint16(http_data_required).dumps(),
413
+ ).dumps()
414
+ bconfig = beacon.BeaconConfig(data)
415
+ profile = c2profile.C2Profile.from_beacon_config(bconfig)
416
+ if not http_data_required:
417
+ assert "http-beacon.data_required" not in profile.properties
418
+ else:
419
+ assert profile.properties["http-beacon.data_required"] == ["true"]
@@ -0,0 +1,60 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ import pytest
5
+
6
+ from dissect.cobaltstrike.beacon import BeaconConfig
7
+ from dissect.cobaltstrike.guardrails import GUARD_CONFIG_STARTS, c_guardrails
8
+
9
+
10
+ def test_guard_config_start_size() -> None:
11
+ """Test that all GUARD_CONFIG_STARTS have the same length."""
12
+ sizes = map(len, GUARD_CONFIG_STARTS)
13
+ assert len(set(sizes)) == 1
14
+
15
+
16
+ @pytest.mark.parametrize(
17
+ "guard_option,settings_type,length",
18
+ [
19
+ (c_guardrails.GuardOption.GUARD_USER, c_guardrails.SettingsType.TYPE_SHORT, 2),
20
+ (c_guardrails.GuardOption.GUARD_COMPUTER, c_guardrails.SettingsType.TYPE_SHORT, 2),
21
+ (c_guardrails.GuardOption.GUARD_DOMAIN, c_guardrails.SettingsType.TYPE_SHORT, 2),
22
+ (c_guardrails.GuardOption.GUARD_LOCAL_IP, c_guardrails.SettingsType.TYPE_INT, 4),
23
+ ],
24
+ )
25
+ def test_guard_config_start_settings(guard_option, settings_type, length) -> None:
26
+ """Test correctness of the known Guardrail start bytes by constructing the GuardrailSetting."""
27
+ guard_option_user = c_guardrails.GuardrailSetting(
28
+ option=guard_option,
29
+ type=settings_type,
30
+ length=length,
31
+ value=b"", # ignored
32
+ ).dumps()
33
+ assert guard_option_user in GUARD_CONFIG_STARTS
34
+
35
+
36
+ def test_beacon_dump_guardrails(guardrails_beacon_path):
37
+ proc = subprocess.run(
38
+ [sys.executable, "-m", "dissect.cobaltstrike.beacon", "-v", "-t", "normal", str(guardrails_beacon_path)],
39
+ capture_output=True,
40
+ )
41
+ proc.check_returncode()
42
+ stdout = proc.stdout
43
+ stderr = proc.stderr
44
+
45
+ assert b"Found guardrail payload xorkey: b'desktop-r4vgq8o'" in stderr
46
+ assert b"guardrail payload xor key = b'desktop-r4vgq8o'" in stdout
47
+ assert b"guardrail options = [<GuardOption.GUARD_COMPUTER: 6>, <GuardOption.GUARD_PAYLOAD_CHECKSUM: 9>]" in stdout
48
+
49
+
50
+ def test_beacon_guardrails_metadata(guardrails_beacon_file, beacon_x64_file):
51
+ beacon = BeaconConfig.from_file(guardrails_beacon_file)
52
+ assert beacon.guardrails is not None
53
+ assert beacon.guardrails.payload_xor_key == b"desktop-r4vgq8o"
54
+ assert beacon.guardrails.settings[0].option == c_guardrails.GuardOption.GUARD_COMPUTER
55
+ assert beacon.guardrails.settings[1].option == c_guardrails.GuardOption.GUARD_PAYLOAD_CHECKSUM
56
+ assert beacon.guardrails.checksum == 0xA5AD1
57
+ assert beacon.guardrails.guardrail_xor_key == b"\x8a"
58
+
59
+ beacon = BeaconConfig.from_file(beacon_x64_file)
60
+ assert beacon.guardrails is None
@@ -1,16 +0,0 @@
1
- # file generated by setuptools_scm
2
- # don't change, don't track in version control
3
- TYPE_CHECKING = False
4
- if TYPE_CHECKING:
5
- from typing import Tuple, Union
6
- VERSION_TUPLE = Tuple[Union[int, str], ...]
7
- else:
8
- VERSION_TUPLE = object
9
-
10
- version: str
11
- __version__: str
12
- __version_tuple__: VERSION_TUPLE
13
- version_tuple: VERSION_TUPLE
14
-
15
- __version__ = version = '1.2.1.dev8'
16
- __version_tuple__ = version_tuple = (1, 2, 1, 'dev8')