dissect.cobaltstrike 1.2.1.dev9__tar.gz → 1.2.1.dev11__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.dev9 → dissect_cobaltstrike-1.2.1.dev11}/.readthedocs.yml +3 -0
  2. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/PKG-INFO +2 -2
  3. dissect_cobaltstrike-1.2.1.dev11/dissect/cobaltstrike/_version.py +21 -0
  4. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/beacon.py +79 -60
  5. dissect_cobaltstrike-1.2.1.dev11/dissect/cobaltstrike/guardrails.py +195 -0
  6. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/utils.py +8 -0
  7. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/version.py +2 -0
  8. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/conftest.py +2 -0
  9. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_beacon.py +42 -0
  10. dissect_cobaltstrike-1.2.1.dev11/tests/test_guardrails.py +60 -0
  11. dissect_cobaltstrike-1.2.1.dev9/dissect/cobaltstrike/_version.py +0 -16
  12. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/.github/workflows/dissect-ci.yml +0 -0
  13. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/.gitignore +0 -0
  14. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/.lfs-assets-id +0 -0
  15. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/.pre-commit-config.yaml +0 -0
  16. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/LICENSE +0 -0
  17. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/README.rst +0 -0
  18. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/codecov.yml +0 -0
  19. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/__init__.py +0 -0
  20. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/artifact.py +0 -0
  21. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/c2.py +0 -0
  22. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/c2profile.lark +0 -0
  23. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/c2profile.py +0 -0
  24. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/c_c2.py +0 -0
  25. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/client.py +0 -0
  26. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/pcap.py +0 -0
  27. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/pe.py +0 -0
  28. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/dissect/cobaltstrike/xordecode.py +0 -0
  29. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/Makefile +0 -0
  30. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/c2profile-grammar.rst +0 -0
  31. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/codespell-ignore-words.txt +0 -0
  32. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/conf.py +0 -0
  33. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/examples.rst +0 -0
  34. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/images/beacon-client.png +0 -0
  35. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/images/teamserver-echoclient.png +0 -0
  36. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/images/teamserver_ls.png +0 -0
  37. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/images/vt-cobaltstrike-43-zip.png +0 -0
  38. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/index.rst +0 -0
  39. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/installation.rst +0 -0
  40. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/license.rst +0 -0
  41. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/scripts/checksum8-accesslogs.rst +0 -0
  42. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/scripts/dump_beacon_keys.rst +0 -0
  43. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/scripts/example_client.rst +0 -0
  44. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/scripts/index.rst +0 -0
  45. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/structures/c2_def.rst +0 -0
  46. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/structures/cs_def.rst +0 -0
  47. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/structures/index.rst +0 -0
  48. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/structures/pe_def.rst +0 -0
  49. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tools/beacon-artifact.rst +0 -0
  50. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tools/beacon-client.rst +0 -0
  51. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tools/beacon-dump.rst +0 -0
  52. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tools/beacon-pcap.rst +0 -0
  53. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tools/beacon-xordecode.rst +0 -0
  54. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tools/c2profile-dump.rst +0 -0
  55. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tutorials/decrypt_cobaltstrike_pcaps.rst +0 -0
  56. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tutorials/index.rst +0 -0
  57. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/docs/tutorials/minimal_beacon_client.rst +0 -0
  58. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/pyproject.toml +0 -0
  59. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/scripts/checksum8-accesslogs.py +0 -0
  60. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/scripts/dump_beacon_keys.py +0 -0
  61. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/scripts/example_client.py +0 -0
  62. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_c2.py +0 -0
  63. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_c2profile.py +0 -0
  64. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_client.py +0 -0
  65. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_pe.py +0 -0
  66. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_utils.py +0 -0
  67. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_version.py +0 -0
  68. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tests/test_xordecode.py +0 -0
  69. {dissect_cobaltstrike-1.2.1.dev9 → dissect_cobaltstrike-1.2.1.dev11}/tox.ini +0 -0
@@ -1,5 +1,8 @@
1
1
  version: 2
2
2
 
3
+ sphinx:
4
+ configuration: docs/conf.py
5
+
3
6
  build:
4
7
  os: ubuntu-lts-latest
5
8
  tools:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: dissect.cobaltstrike
3
- Version: 1.2.1.dev9
3
+ Version: 1.2.1.dev11
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.dev11'
21
+ __version_tuple__ = version_tuple = (1, 2, 1, 'dev11')
@@ -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,
@@ -464,13 +465,6 @@ def iter_settings(fobj: Union[bytes, BinaryIO]) -> Iterator["Setting"]:
464
465
  yield setting
465
466
 
466
467
 
467
- def grouper(iterable, n, fillvalue=None):
468
- "Collect data into fixed-length chunks or blocks"
469
- # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
470
- args = [iter(iterable)] * n
471
- return itertools.zip_longest(*args, fillvalue=fillvalue)
472
-
473
-
474
468
  def parse_recover_binary(program: bytes) -> List[Tuple[str, Union[int, bool]]]:
475
469
  """Parse ``SETTING_C2_RECOVER`` (`.http-get.server.output`) data"""
476
470
  rsteps: List[Tuple[str, Union[int, bool]]] = []
@@ -773,6 +767,8 @@ class BeaconConfig:
773
767
  """ PE compile timestamp, ``None`` if unknown. """
774
768
  self.architecture: Optional[str] = None
775
769
  """ PE architecture, ``"x86"`` or ``"x64"`` and ``None`` if unknown. """
770
+ self.guardrails: Optional[GuardrailMetadata] = None
771
+ """ Guardrails metadata, ``None`` if not available. """
776
772
 
777
773
  # Used for caching
778
774
  self._settings: Optional[Mapping[str, Any]] = None
@@ -809,6 +805,22 @@ class BeaconConfig:
809
805
  bconfig.architecture = pe.find_architecture(fh)
810
806
  # Return the first found beacon config.
811
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
+
812
824
  raise ValueError("No valid Beacon configuration found")
813
825
 
814
826
  @classmethod
@@ -1108,7 +1120,7 @@ def build_parser():
1108
1120
  import argparse
1109
1121
 
1110
1122
  parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
1111
- parser.add_argument("input", metavar="FILE", help="Beacon to dump")
1123
+ parser.add_argument("input", metavar="FILE", nargs="+", help="Beacon to dump")
1112
1124
  parser.add_argument(
1113
1125
  "-x",
1114
1126
  "--xorkey",
@@ -1116,10 +1128,9 @@ def build_parser():
1116
1128
  help="override default xor key(s) (default: -x 0x69 -x 0x2e -x 0x00)",
1117
1129
  )
1118
1130
  parser.add_argument(
1119
- "-a",
1120
- "--all",
1131
+ "--default-xor-keys-only",
1121
1132
  action="store_true",
1122
- 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",
1123
1134
  )
1124
1135
  parser.add_argument(
1125
1136
  "-t",
@@ -1150,63 +1161,71 @@ def main():
1150
1161
  level = levels[min(len(levels) - 1, args.verbose)]
1151
1162
  logging.basicConfig(
1152
1163
  level=level,
1153
- datefmt="[%X]",
1154
- format="%(asctime)s %(name)s %(message)s",
1164
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
1155
1165
  )
1156
1166
 
1157
1167
  xor_keys = None
1158
1168
  if args.xorkey:
1159
1169
  xor_keys = tuple(utils.pack_be(int(x, 0)) for x in args.xorkey)
1160
1170
 
1161
- try:
1162
- if args.input in ("-", "/dev/stdin"):
1163
- with io.BytesIO(sys.stdin.buffer.read()) as fin:
1164
- config = BeaconConfig.from_file(fin, xor_keys=xor_keys, all_xor_keys=args.all)
1165
- else:
1166
- config = BeaconConfig.from_path(args.input, xor_keys=xor_keys, all_xor_keys=args.all)
1167
- except ValueError:
1168
- print(f"{args.input}: No beacon configuration found.", file=sys.stderr)
1169
- return 1
1170
-
1171
- if args.type == "raw":
1172
- for setting in config.settings_tuple:
1173
- print(setting)
1174
- elif args.type == "dumpstruct":
1175
- cstruct.hexdump(config.config_block)
1176
- print("-----")
1177
- for setting in config.settings_tuple:
1178
- cstruct.dumpstruct(setting)
1179
- print("-" * 10)
1180
- elif args.type == "normal":
1181
- settings = config.settings
1182
- for setting, value in settings.items():
1183
- print(f"{setting} = {value!r}")
1184
- if args.verbose >= 1:
1185
- print("-" * 50)
1186
- print(
1187
- "pe_export_stamp = {}, {}, {}".format(
1188
- config.pe_export_stamp,
1189
- hex(config.pe_export_stamp),
1190
- 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
+ )
1191
1206
  )
1192
- )
1193
- print(
1194
- "pe_compile_stamp = {}, {}, {}".format(
1195
- config.pe_compile_stamp,
1196
- hex(config.pe_compile_stamp),
1197
- 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
+ )
1198
1213
  )
1199
- )
1200
- print(
1201
- "max_setting_enum = {} - {}".format(
1202
- config.max_setting_enum,
1203
- BeaconSetting(config.max_setting_enum),
1214
+ print(
1215
+ "max_setting_enum = {} - {}".format(
1216
+ config.max_setting_enum,
1217
+ BeaconSetting(config.max_setting_enum),
1218
+ )
1204
1219
  )
1205
- )
1206
- print("beacon_version =", config.version)
1207
- elif args.type == "c2profile":
1208
- profile = c2profile.C2Profile.from_beacon_config(config)
1209
- 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
1210
1229
 
1211
1230
 
1212
1231
  if __name__ == "__main__":
@@ -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()
@@ -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.dev9'
16
- __version_tuple__ = version_tuple = (1, 2, 1, 'dev9')