In the previous post of this series we showed why Brute Ratel C4 (BRC4) isn’t able to execute most BOFs that use the de-facto BOF API standard by Cobalt Strike (CS): BRC4 implements their own BOF API which isn’t compatible with the CS BOF API. Then we also outlined an approach to solve this issue: by injecting a custom compatibility layer that implements the CS BOF API using the BRC4 API, we can enable BRC4 to support any BOF.
I’m proud to finally introduce you to our tool CS2BR (“Cobalt Strike to Brute Ratel [BOF]”) in this blog post. We’ll cover its concept and implementation, briefly discuss its usage, show some examples of CS2BR in use and draw our conclusions.
The tool is open-source and published on GitHub. It consists of three components: the compatibility layer (based on TrustedSec’s COFFLoader), a source-code patching script implemented in Python and an argument encoder script (also based on COFFLoader). Let’s take a closer look at each of those individually:
As outlined in the first blog post, the compatibility layer provides implementations of the CS BOF API for the original beacons and also comes with a new coffee
entrypoint that is invoked by BRC4, pre-processes BOF input parameters and calls the original BOF’s go
entrypoint.
For practical reasons that become apparent further down this post, the layer is split into two files: one for the BOF API implementation (beacon_wrapper.h
) and entrypoint (badger_stub.c
), respectively.
The BOF API implementation borrows heavily from COFFLoader and adds some bits and pieces, such as the Win32 APIs imported by default by CS (GetProcAddress, GetModuleHandle, LoadLibrary and FreeLibrary) and a global variable for the __dispatch
variable used by BRC4 BOFs for output. Note that as of this writing, CS2BR doesn’t implement the complete CS BOF API and lacks functions related to process tokens and injection, as those weren’t considered worthwhile pursuing yet.
The entrypoint itself, on the other hand, was built from scratch. Since BRC4’s coffee
entrypoint can only be supplied with string-based parameters (whereas CS’ go
takes arbitrary bytes), this custom one optionally base64-decodes an input string and forwards it to the CS go
entrypoint. To generate the base64-encoded input argument, CS2BR comes with a Python script (encode_args.py
, based on COFFLoader’s implementation) that assembles a binary blob of data to be passed to BOFs (such as integers, strings and files).
The compatibility layer alone only gets you so far though – it needs to be patched into a BOF somehow. That’s where the patcher comes in. It’s a Python script that injects the compatibility layer’s source code into any BOF’s source code. Its approach to this is simple and only consists of two steps:
beacon.h
) and replace their contents with CS2BR’s compatibility layer implementation beacon_wrapper.h
.go
entrypoint and append CS2BR’s custom coffee
entrypoint from badger_stub.c
.When I started working on the patcher’s implementation, I wasn’t sure just how tricky these two steps would be to implement: Would I need to come up with tons of RegEx’s to CS BOF API identify imports? Would I maybe need to parse the actual source code using the actual C grammar to find go
entrypoints? Or would I need to compile individual object files and extract line-number information from their metadata?
Luckily, I didn’t have to deal with most of the above. The CS BOF API imports are consistently included as a separate header file called beacon.h
, thus they can be found by name in most cases. To find the entrypoint, I wrote a single RegEx: \s+(go)\s*\(([^,]+?),([^\)]+?)\)\s*\{
. Let’s briefly break it down using Cyril’s Regex Tester:
The patterns matches:
char*
argument (which is any character but “,”),int
argument (matching any character but the closing parenthesis),This pattern allows CS2BR to identify the entrypoint, optionally rename it and reuse the exact parameter names and types. Once it identified the go
entrypoint in a file, it simply appends the contents of badger_stub.c
to the file. This stub contains forward-declarations of base64-decoding functions used in the custom coffee
entrypoint, the new entrypoint itself, and the accompanying definitions of the base64-decoding functions. And that’s it – BOFs patched this way can now be recompiled and are ready to use in BRC4. If a BOF takes input from CNA scripts, one might need to use the argument encoder.
CS BOFs can be supplied with arbitrary binary data, and the first blog post showed that BRC4 BOFs can’t since their entrypoints are designed and invoked differently. To remedy this, CS2BR borrows a utility from COFFLoader and comes with a Python script that allows operators to encode input parameters for their BOFs in a way that can be passed via BRC4 into CSBR’s custom coffee
entrypoint:
One drawback of using base64-encoding is the considerable overhead: base64 encodes 3 bytes of input into 4 bytes of ASCII, resulting in 33% overhead. As can be seen in the above screenshot, the raw data of about 6kB is encoded into about 8kB. The script also implements GZIP compression of input data, reducing the raw buffer to about 2.5kB and base64 data to about 3.5kB. As of this writing, however, CS2BR’s entrypoint doesn’t support decompression yet.
Using CS2BR is pretty straight-forward. You’ll need to patch & compile your BOFs only once and can then execute them via BRC4. If your BOFs accept input arguments, you’ll need to generate them via CS2BR’s argument encoder. Let’s have a look at the complete workflow.
Again, we’ll use CS-Situational-Awareness (SA) as an example. First, clone SA and CS2BR:
git clone https://github.com/trustedsec/CS-Situational-Awareness-BOF git clone https://github.com/NVISO-ARES/cs2br-bof/
Then, invoke the patcher from the cs2br-bof repo and specify the “CS-Situational-Awareness-BOF” directory you just cloned as the source directory (--src
) to patch:
Finally, compile the BOFs as you would usually do:
cd CS-Situational-Awareness-BOF ./make_all.sh
That’s it, simple BOFs (such as whoami
, uptime
, …) that don’t require any input arguments can be executed directly through BRC4 now:
In order to supply BOFs compiled with CS2BR with input arguments, we’ll use the encode_args
script.
Let’s use nslookup as an exemplary BOF for this workflow. It expects up to three input parameters, lookup value
, lookup server
and type
, as defined in CS-Situational-Awareness’ aggressor script:
alias nslookup { ... $lookup = $2; $server = iff(-istrue $3, $3, ""); $type = iff(-istrue $4, # ... ... $args = bof_pack($1, "zzs", $lookup, $server, $type); beacon_inline_execute($1, readbof($1, "nslookup", "Attempting to resolve $lookup", "T1018"), "go", $args); }
The bof_pack call above assembles these variables into a binary blob according to the format “zzs” ($lookup
and $server
as null-terminated strings with their length prepended and $type
as a 2-byte integer). This binary blob is disassembled by the BOF using the BeaconData*
APIs.
BRC4 doesn’t support aggressor scripts, though, so CS2BR’s argument encoder serves as a workaround. As an example, let’s encode blog.nviso.eu
for $lookup
, 8.8.8.8
for $server
and 1
for $type
(to query A records, ref. MS documentation):
The resulting base64 encoded argument buffer, DgAAAGJsb2cubnZpc28uZXUACAAAADguOC44LjgAAQA=
, can then be passed to BRC4’s coffexec
command and will be processed by CS2BR’s custom entrypoint and forwarded to the original BOF’s logic:
Working on CS2BR has been a lot of fun and, frankly, also quite frustrating at times. After all, BRC4 isn’t an easy target system to develop for due to its black-box nature. This project has come a fairly long way nonetheless!
This blog post showed how CS2BR works and how it can be used. At this point, the tool allows you to run all your favorite open-source CS BOFs via BRC4. So in case you are used to a BOF-heavy workflow in CS and intend to switch to BRC4, now you got the tools to keep using the same BOFs.
Using CS2BR is straight-forward and doesn’t require special skills or knowledge for the most part. There are some caveats to it that should be considered before using it “in production” though:
I’m convinced that most of those points don’t constitute actual practical problems, but rather academic challenges to tackle in the future. Overall, I think the benefit of being able to run CS BOFs in BRC4 outweighs CS2BR’s drawbacks.
While I’m happy with the current implementation, I’m convinced it can be improved upon. Expect a third, final blog post about the next iteration of CS2BR. What is it going to be about, I hear you ask? Well, let me use a meme to tease you:
Moritz Thomas
Moritz is a senior IT security consultant and red teamer at NVISO.
When he isn’t infiltrating networks or exfiltrating data, he is usually knees deep in research and development, working on new techniques and tools in red teaming.