Skip to content

Commit

Permalink
Add support for SETTING_DATA_STORE_SIZE and SETTING_BEACON_GATE optio…
Browse files Browse the repository at this point in the history
…ns (#68)

These settings were introduced in Cobalt Strike 4.9 and 4.10.
  • Loading branch information
yunzheng authored Oct 14, 2024
1 parent 56fdd6b commit 3fb5f79
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 0 deletions.
90 changes: 90 additions & 0 deletions dissect/cobaltstrike/beacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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=">")
Expand All @@ -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 """
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions dissect/cobaltstrike/c2profile.lark
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
49 changes: 49 additions & 0 deletions dissect/cobaltstrike/c2profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)))
Expand Down
57 changes: 57 additions & 0 deletions tests/test_c2profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
[
Expand Down Expand Up @@ -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

0 comments on commit 3fb5f79

Please sign in to comment.