diff --git a/dissect/cobaltstrike/beacon.py b/dissect/cobaltstrike/beacon.py index 0ac690c..94a1973 100644 --- a/dissect/cobaltstrike/beacon.py +++ b/dissect/cobaltstrike/beacon.py @@ -170,6 +170,12 @@ // CobaltStrike version >= 4.7 (Aug 17, 2022) SETTING_MASKED_WATERMARK = 74, + + // CobaltStrike version >= 4.9 (Sep 19, 2023) + SETTING_DATA_STORE_SIZE = 76, + + // CobaltStrike version >= 4.10 (Jul 16, 2024) + SETTING_BEACON_GATE = 78, }; enum DeprecatedBeaconSetting: uint16 { @@ -253,6 +259,33 @@ MapViewOfFile = 1, HeapAlloc = 2, }; + +// https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/beacon-gate.htm +struct BeaconGateOptions { + uint8 InternetOpenA; // commms + uint8 InternetConnectA; + uint8 VirtualAlloc; // core + uint8 VirtualAllocEx; + uint8 VirtualProtect; + uint8 VirtualProtectEx; + uint8 VirtualFree; + uint8 GetThreadContext; + uint8 SetThreadContext; + uint8 ResumeThread; + uint8 CreateThread; + uint8 CreateRemoteThread; + uint8 OpenProcess; + uint8 OpenThread; + uint8 CloseHandle; + uint8 CreateFileMappingA; + uint8 MapViewOfFile; + uint8 UnmapViewOfFile; + uint8 VirtualQuery; + uint8 DuplicateHandle; + uint8 ReadProcessMemory; + uint8 WriteProcessMemory; + uint8 ExitThread; // cleanup +}; """ cs_struct = cstruct.cstruct(endian=">") @@ -269,6 +302,7 @@ InjectAllocator = cs_struct.InjectAllocator InjectExecutor = cs_struct.InjectExecutor BofAllocator = cs_struct.BofAllocator +BeaconGateOptions = cs_struct.BeaconGateOptions DEFAULT_XOR_KEYS: List[bytes] = [b"\x69", b"\x2e", b"\x00"] """ Default XOR keys used by Cobalt Strike for obfuscating Beacon config bytes """ @@ -581,6 +615,61 @@ def parse_pivot_frame(data: bytes) -> bytes: return p.read(length - 4) +def parse_beacon_gate(data: bytes) -> BeaconGateOptions: + """Parse ``SETTING_BEACON_GATE`` (`.stage.beacon_gate`) data""" + return BeaconGateOptions(data) + + +def beacon_gate_options_string(bgo: BeaconGateOptions) -> list[str]: + """Return the enabled BeaconGate WinAPI's as a list of strings""" + options = {k for k, v in bgo._values.items() if v} + + comms = {"InternetOpenA", "InternetConnectA"} + core = { + "VirtualAlloc", + "VirtualAllocEx", + "VirtualProtect", + "VirtualProtectEx", + "VirtualFree", + "GetThreadContext", + "SetThreadContext", + "ResumeThread", + "CreateThread", + "CreateRemoteThread", + "OpenProcess", + "OpenThread", + "CloseHandle", + "CreateFileMappingA", + "MapViewOfFile", + "UnmapViewOfFile", + "VirtualQuery", + "DuplicateHandle", + "ReadProcessMemory", + "WriteProcessMemory", + } + cleanup = {"ExitThread"} + + ret = [] + if options.issuperset(comms | core | cleanup): + ret.append("All") + options -= comms | core | cleanup + + if options.issuperset(comms): + ret.append("Comms") + options -= comms + + if options.issuperset(core): + ret.append("Core") + options -= core + + if options.issuperset(cleanup): + ret.append("Cleanup") + options -= cleanup + + ret.extend(options) + return ret + + def sha256sum_pubkey(der_data: bytes) -> str: """Return the SHA-256 digest of `der_data`""" return hashlib.sha256(der_data.rstrip(b"\x00")).hexdigest() @@ -643,6 +732,7 @@ def null_terminated_str(data: bytes) -> str: BeaconSetting.SETTING_WATERMARKHASH: lambda x: null_terminated_bytes(x) if isinstance(x, bytes) else x, BeaconSetting.SETTING_MASKED_WATERMARK: lambda x: x.hex(), BeaconSetting.SETTING_BOF_ALLOCATOR: lambda x: BofAllocator(x).name, + BeaconSetting.SETTING_BEACON_GATE: lambda x: beacon_gate_options_string(parse_beacon_gate(x)), # BeaconSetting.SETTING_PROTOCOL: lambda x: BeaconProtocol(x).name, # BeaconSetting.SETTING_CRYPTO_SCHEME: lambda x: CryptoScheme(x).name, # BeaconSetting.SETTING_PROXY_BEHAVIOR: lambda x: ProxyServer(x).name, diff --git a/dissect/cobaltstrike/c2profile.lark b/dissect/cobaltstrike/c2profile.lark index 71c3adf..1d80a2c 100644 --- a/dissect/cobaltstrike/c2profile.lark +++ b/dissect/cobaltstrike/c2profile.lark @@ -137,6 +137,8 @@ stage_options: "string" string ";" -> string | "set" "rich_header" string ";" -> rich_header | "set" "checksum" string ";" -> checksum | "set" "syscall_method" string ";" -> syscall_method // introduced in Cobalt Strike 4.8 + | "set" "data_store_size" string ";" -> data_store_size // introduced in Cobalt Strike 4.9 + | "beacon_gate" "{" beacon_gate_options* "}" -> beacon_gate // introduced in Cobalt Strike 4.10 process_inject_options: "set" "allocator" string ";" -> allocator | "set" "min_alloc" string ";" -> min_alloc @@ -158,6 +160,35 @@ execute_options: "CreateThread" string ";" -> createthread_special | "RtlCreateUserThread" ";" -> rtlcreateuserthread | "SetThreadContext" ";" -> setthreadcontext +beacon_gate_options: "None" ";" -> none + | "Comms" ";" -> comms + | "Core" ";" -> core + | "Cleanup" ";" -> cleanup + | "All" ";" -> all + | "InternetOpenA" ";" -> internetopena + | "InternetConnectA" ";" -> internetconnecta + | "VirtualAlloc" ";" -> virtualalloc + | "VirtualAllocEx" ";" -> virtualallocex + | "VirtualProtect" ";" -> virtualprotect + | "VirtualProtectEx" ";" -> virtualprotextex + | "VirtualFree" ";" -> virtualfree + | "GetThreadContext" ";" -> getthreadcontext + | "SetThreadContext" ";" -> setthreadcontext + | "ResumeThread" ";" -> resumethread + | "CreateThread" ";" -> createthread + | "CreateRemoteThread" ";" -> createremotethread + | "OpenProcess" ";" -> openprocess + | "OpenThread" ";" -> openthread + | "CloseHandle" ";" -> closehandle + | "CreateFileMappingA" ";" -> createfilemappinga + | "MapViewOfFile" ";" -> mapviewoffile + | "UnmapViewOfFile" ";" -> unmapviewoffile + | "VirtualQuery" ";" -> virtualquery + | "DuplicateHandle" ";" -> duplicatehandle + | "ReadProcessMemory" ";" -> readprocessmemory + | "WriteProcessMemory" ";" -> writeprocessmemory + | "ExitThread" ";" -> exitthread + postex_options: "set" "spawnto_x86" string ";" -> spawnto_x86 | "set" "spawnto_x64" string ";" -> spawnto_x64 | "set" "obfuscate" string ";" -> obfuscate diff --git a/dissect/cobaltstrike/c2profile.py b/dissect/cobaltstrike/c2profile.py index d1b7d77..3745946 100644 --- a/dissect/cobaltstrike/c2profile.py +++ b/dissect/cobaltstrike/c2profile.py @@ -350,6 +350,50 @@ def from_execute_list(cls, execute_list=None): return block +class BeaconGateBlock(ConfigBlock): + """`.stage.beacon_gate` block""" + + __name__ = "BeaconGateBlock" + + none = ConfigBlock._enable + comms = ConfigBlock._enable + core = ConfigBlock._enable + cleanup = ConfigBlock._enable + all = ConfigBlock._enable + internetopena = ConfigBlock._enable + internetconnecta = ConfigBlock._enable + virtualalloc = ConfigBlock._enable + virtualallocex = ConfigBlock._enable + virtualprotect = ConfigBlock._enable + virtualprotextex = ConfigBlock._enable + virtualfree = ConfigBlock._enable + getthreadcontext = ConfigBlock._enable + setthreadcontext = ConfigBlock._enable + resumethread = ConfigBlock._enable + createthread = ConfigBlock._enable + createremotethread = ConfigBlock._enable + openprocess = ConfigBlock._enable + openthread = ConfigBlock._enable + closehandle = ConfigBlock._enable + createfilemappinga = ConfigBlock._enable + mapviewoffile = ConfigBlock._enable + unmapviewoffile = ConfigBlock._enable + virtualquery = ConfigBlock._enable + duplicatehandle = ConfigBlock._enable + readprocessmemory = ConfigBlock._enable + writeprocessmemory = ConfigBlock._enable + exitthread = ConfigBlock._enable + + @classmethod + def from_beacon_gate_option_strings(cls, options: list[str]): + block = cls() + + for option in options: + block._enable(option.lower(), True) + + return block + + class C2Profile(ConfigBlock): """A :class:`C2Profile` object represents a parsed Malleable C2 Profile @@ -622,6 +666,11 @@ def from_beacon_config(cls, config: BeaconConfig) -> "C2Profile": proc_inj.set_option("bof_reuse_memory", "true") elif setting == BeaconSetting.SETTING_BOF_ALLOCATOR: proc_inj.set_option("bof_allocator", value) + elif setting == BeaconSetting.SETTING_DATA_STORE_SIZE: + stage.set_option("data_store_size", value) + elif setting == BeaconSetting.SETTING_BEACON_GATE and value: + block = BeaconGateBlock.from_beacon_gate_option_strings(value) + stage.set_config_block("beacon_gate", block) if c2_recover: http_get.set_non_empty_config_block("server", HttpOptionsBlock(output=DataTransformBlock(steps=c2_recover))) diff --git a/tests/test_c2profile.py b/tests/test_c2profile.py index 29e4615..89243e6 100644 --- a/tests/test_c2profile.py +++ b/tests/test_c2profile.py @@ -298,6 +298,39 @@ def test_c2profile_v48(): assert profile.properties["stage.syscall_method"] == ["direct"] +def test_c2profile_v49(): + c2profile_fragment = """ + stage { + set data_store_size "1024"; + } + """ + profile = c2profile.C2Profile.from_text(c2profile_fragment) + assert profile.properties["stage.data_store_size"] == ["1024"] + + +def test_c2profile_v410(): + c2profile_fragment = """ + stage { + beacon_gate { + All; + } + } + """ + profile = c2profile.C2Profile.from_text(c2profile_fragment) + assert profile.properties["stage.beacon_gate"] == ["All"] + + c2profile_fragment = """ + stage { + beacon_gate { + VirtualAlloc; + WriteProcessMemory; + } + } + """ + profile = c2profile.C2Profile.from_text(c2profile_fragment) + assert profile.properties["stage.beacon_gate"] == ["VirtualAlloc", "WriteProcessMemory"] + + @pytest.mark.parametrize( ("allocator_enum", "bof_allocator"), [ @@ -338,3 +371,27 @@ def test_c2profile_bof_reuse_memory(bof_reuse_memory): assert profile.properties["process-inject.bof_reuse_memory"] == ["true"] else: assert "project.inject.bof_reuse_memory" not in profile.properties + + +def test_c2profile_beacon_gate(): + # beacon gate with all options + data = beacon.Setting( + index=beacon.BeaconSetting.SETTING_BEACON_GATE, + type=beacon.SettingsType.TYPE_PTR, + length=0x40, + value=b"\x01" * 0x40, + ).dumps() + bconfig = beacon.BeaconConfig(data) + profile = c2profile.C2Profile.from_beacon_config(bconfig) + assert profile.properties["stage.beacon_gate"] == ["All"] + + # empty beacon gate should not result into any profile setting + data = beacon.Setting( + index=beacon.BeaconSetting.SETTING_BEACON_GATE, + type=beacon.SettingsType.TYPE_PTR, + length=0x40, + value=b"\x00" * 0x40, + ).dumps() + bconfig = beacon.BeaconConfig(data) + profile = c2profile.C2Profile.from_beacon_config(bconfig) + assert "stage.beacon_gate" not in profile.properties