Using Kaitai Struct to Parse Cobalt Strike Beacon Configs

Parsing Beacon Configs: Basics

To Start

When staged from a server, the Beacon DLL is encoded/packed with a stub loader. You might also find this DLL in memory dumps or in fully-staged Beacon payloads in common malware repositories. Both the NSE script and Sentinel One’s parser have the ability to unpack the actual Beacon code and XOR decode the config (0x69 or 0x2e depending on version). I will start from the point that you have decoded config data. We will be focusing on an in-the-wild sample I discovered and uploaded to MalwareBazaar for those who want to follow along. The sample is already unpacked and XOR decoded. The sample uses a popular Amazon Malleable C2 profile created by Will (Twitter). To get to the config data in the sample, open it in your favorite hex editor and Ctrl/Cmd-F for the hex string “00 01 00 01 00 02”.

Binary Config Structure

Configs are built up of multiple records in the format of ( Index# | Type | Length | Value ). This is common to the usual “TLV” structure used in binary structures. Within this structure, the type is either 0x1 for short, 0x2 for int, or 0x3 for byte array / blob. The blob (0x3 type) can vary in actual interpretation depending on index/field #, ranging from string to byte array to sub-structures. We can look at a couple of simple examples of this I-TLV in practice from a real world config.

Two I-TLV structures at the beginning of the sample config.
  • Index # = 0x01 (Beacon Type) | Type = 0x01 (Short) | Length = 0x02 | Value = 0x00 (HTTP Beacon)
  • Index # = 0x02 (Port) | Type = 0x01 (Short) | Length = 0x02 | Value = 0x50 (Port 80)
Config field 0x8 showing an example blob structure in the sample data
config_entry:
seq:
- id: index
type: u2
- id: fieldtype
type: u2
- id: fieldlength
type: u2
- id: fieldvalue
size: fieldlength
type:
switch-on: fieldtype
cases:
1: u2
2: u4
3: bytes
bytes:
seq:
- id: byte_val
type:
switch-on: _parent._parent.index
cases:
11: transform_blocks
12: req_malleablec2
13: req_malleablec2
42: gargle_section
46: procinj_transform
47: procinj_transform
....... SUB STRUCTURES CONTINUE

Beacon Configs: Malleable C2

It might seem simple so far, but things get complicated quick: the Malleable C2 block. These are actually sub-structures that get interpreted and used by Beacon to alter various aspects of the command and control protocols through its own transform language. To understand these, I highly highly recommend reading the Cobalt Strike docs on Malleable C2. Doing binary analysis on these structures actually made me appreciate the beauty of Cobalt Strike. For the sake of this blog, we will only decompose one of the types, the Malleable C2 Request block, which is used in the profile’s protocol sections, specifically in the “client” blocks (used in index #12 and #13 of the binary structure).

Malleable C2 Request Blocks

We will use the following images for our walkthrough. On the left, we have the binary structure that is representing the actual malleable config on the right, inside of the client section. The binary config starts off with the standard I-TLV (0xd | 0x3 | 0x200 | BLOB). Inside the blob is the malleable C2 request structure.

Comparison showing the binary structure for malleable c2 and the actual written c2 profile.
  • (0xa or _Header| 0xb | “Accept: */*”)
  • (0xa or _Header | 0x16 | “Content-Type: text/xml”)
  • (0xa or _Header | 0x20 | “X-Requested-With” “XMLHttpRequest”)
  • (0x10 or _HostHeader | 0x14 | “Host: www.amazon.com”)
  • (0x9 or _Parameter | 0xa | “sz=160x600”)
  • …. And so on
malleable_block:
seq:
- id: statement
type: u4
enum: transform_actions
- id: statement_value
type:
switch-on: statement
cases:
transform_actions::uheader: length_val_string
transform_actions::uparameter: length_val_string
transform_actions::build: data_transform
transform_actions::uhostheader: length_val_string
if: statement != transform_actions::stop

DataTransform Blocks

The DataTransform block is another substructure that is described well in the Cobalt Strike docs in the “Data Transform” section. Within the Malleable C2 profiles, these will be in the “id”, “output”, or “metadata” blocks and describe how to transform the data CS produces. Profile authors can prepend or append bytes, change encodings, xor mask, etc. For our example, we will focus on the “output” block of the http-post section in the Amazon config.

output {            
base64;
print;
}
Example DataTransform block within the binary structure.
  • (0x07 = DataTransform) (0x01 = Output Section) (0x03 = Base64) (0x04 = Terminate_Print)
 data_transform:
seq:
- id: type_code
type: u4
- id: transform_statement
type: transform_statement
repeat: until
repeat-until: _.action == <TERMINATION ACTION>
transform_statement:
seq:
- id: action
type: u4
enum: transform_actions
- id: action_args
type:
switch-on: action
cases:
transform_actions::append: length_val_bytes
transform_actions::prepend: length_val_bytes
transform_actions::termination_header: length_val_string
transform_actions::termination_parameter: length_val_string
if: <NOT A TERMINATION ACTION>

Results & Future Work

My full proof-of-concept KSY file for Beacon config data can be found here: https://gist.github.com/sixdub/a5361168ba7acecf7a7a214bf7e5d3d3.

GraphViz output of my parser structs. This is not very pretty but so easy to generate!
Screenshot of simple HTML/Javascript wrapper POC using my Kaitai Struct Beacon Config Parser. Some wrapping functionality is required to display and pretty print the structure information.
  • This parser will break if CS adds sub-structures to existing assumed formats such as Malleable C2 Req. For example, if there is a new Malleable Transform Action that has a (length | value) argument, this proof-of-concept will not know about it today. This should handle new “fields” at the higher level just fine, it will treat them as blobs.
  • The current parser assumes you start the input at the config start (it does not handle unpacking, xor decoding, or seeking the config).
  • I believe my structure definition is overly nested and complicated. I’d bet that it could be optimized many ways by someone with more time in the language. It could definitely be prettier.
  • I would love to extend the parser to handle Beacon dlls or stages entirely without the need to first parse the start of the config bytes. I didn’t implement this for the sake of time.
  • There are probably more blob structures that have meaning to the Beacon agent and could use sub-structures. I implemented the obvious ones.
  • Validate all of the bytes fields that should be strings are properly casted.

Would I do it again?

Overall, I found it simple and intuitive to use. I really appreciated the declarative methodology and the support for multiple languages. I could see this being much easier to maintain as a parser format with a wrapper that just loads the updates. If I were honest, this is potentially a bit of overkill to just hack together a Beacon parser but to build, support, and maintain a long term parsing project, I could totally see the value (or to teach others the structure).

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store