Atomic BOFs
tl;dr
Inspired by Red Canary's Atomic Red Team, 'Atomic BOFs' is my attempt at an implementation pattern to ease detection engineering for Beacon Object Files.
This project leverages two design ideas.
BOF Inversions
A BOF is an object (COFF) file that's loaded by a C2 (such as Cobalt Strike) and linked to Win32 API, as well as some internal Beacon APIs. The implementation of those internal APIs live inside the Beacon agent, which makes a BOF dependent on it for execution. Practically every COFF/BOF loader project I've seen, such as TrustedSec's COFFLoader, includes a compatibility layer that implements these functions for the BOF to call instead. BOF Inversions flips this around by merging the API implementations into the BOF instead of having them in the loader or C2 agent.
BOF Cocktails
Cobalt Strike's BOF APIs include functions like BeaconVirtualAlloc. The idea behind these is to expose Beacon's evasion capabilities (such as syscalls) to BOFs. However, as above, this makes the BOF dependent on the C2 agent for evasion. BOF Cocktails flips this by merging evasion tradecraft directly into the BOF.
So what?
The outcome of these two ideas is that BOFs suddenly become 'self-contained', and no longer require a C2 agent to achieve equivalent functionality. By extension, self-contained units are far easier to use as test cases. The primary goal was to provide a means for detection engineers to execute BOFs within their test/analysis environments, without having the overhead of a whole C2 setup.
The Harness
The meat and potatoes of the project is a simple BOF loader, which I called the 'harness'. This is responsible for loading a COFF into memory, calling its entry point, and passing any packed arguments.
#include <windows.h>
#include "loader.h"
#include "tcg.h"
DECLSPEC_IMPORT LPVOID WINAPI KERNEL32$VirtualAlloc ( LPVOID, SIZE_T, DWORD, DWORD );
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$VirtualProtect ( LPVOID, SIZE_T, DWORD, PDWORD );
char _COFF_ [ 0 ] __attribute__ ( ( section ( "coff" ) ) );
char _ARGS_ [ 0 ] __attribute__ ( ( section ( "args" ) ) );
void go ( )
{
RESOURCE * src = GETRESOURCE ( _COFF_ );
char * code = KERNEL32$VirtualAlloc ( NULL, PicoCodeSize ( src->val ), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE );
char * data = KERNEL32$VirtualAlloc ( NULL, PicoDataSize ( src->val ), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE );
IMPORTFUNCS funcs;
funcs.GetProcAddress = GetProcAddress;
funcs.LoadLibraryA = LoadLibraryA;
PicoLoad ( ( IMPORTFUNCS * ) &funcs, src->val, code, data );
RESOURCE * args = GETRESOURCE ( _ARGS_ );
( ( BOFMAIN ) PicoEntryPoint ( src->val, code ) ) ( args->val, args->len );
}
loader.c
Its corresponding spec file expects to receive $COFF and $ARGS variables, where $COFF are the bytes of a COFF file and $ARGS are packed arguments. There's also a variable called %entrypoint that will contain the BOF's entry point (typically go).
x64:
load "bin/loader.x64.o"
make pic +gofirst +optimize
run "services.spec"
mergelib "libtcg.x64.zip"
push $COFF
make object +optimize
run "bof.spec" %entrypoint
export
preplen
link "coff"
push $ARGS
preplen
link "args"
export
The BOF Inversion magic happens in bof.spec. First, we have bofapi.c that implements the BOF APIs, such as BeaconDataParse.
void BeaconDataParse ( datap * parser, char * buffer, int size )
{
parser->original = buffer;
parser->buffer = buffer;
parser->length = size;
parser->size = size;
}
... etc ...bofapi.c
The spec file merges these functions into the BOF and redirects any calls to them.
x64:
entry %1
load "bin/bofapi.x64.o"
merge
attach "$BeaconDataExtract" "BeaconDataExtract"
attach "$BeaconDataLength" "BeaconDataLength"
attach "$BeaconDataParse" "BeaconDataParse"
attach "$BeaconDataPtr" "BeaconDataPtr"
attach "$BeaconDataInt" "BeaconDataInt"
attach "$BeaconDataShort" "BeaconDataShort"
attach "$BeaconFormatAlloc" "BeaconFormatAlloc"
attach "$BeaconFormatReset" "BeaconFormatReset"
attach "$BeaconFormatAppend" "BeaconFormatAppend"
attach "$BeaconFormatPrintf" "BeaconFormatPrintf"
attach "$BeaconFormatToString" "BeaconFormatToString"
attach "$BeaconFormatFree" "BeaconFormatFree"
attach "$BeaconFormatInt" "BeaconFormatInt"
attach "$BeaconPrintf" "BeaconPrintf"
attach "$BeaconOutput" "BeaconOutput"
bof.spec
My intention is that a consumer of the project will not need to change anything related to the harness, as any changes can be made in other spec files (as I'll describe below).
Test BOF
As a simple test, I've included a small piece of C that simply prints a message using BeaconPrintf.
#include <windows.h>
#include "beacon.h"
void go ( char * args, int len )
{
datap parser;
BeaconDataParse ( &parser, args, len );
char * message = BeaconDataExtract ( &parser, NULL );
BeaconPrintf ( CALLBACK_OUTPUT, "%s\n", message );
}test.c
A config.spec file is used to configure the harness. This is where the $COFF, $ARGS, and %entrypoint variables need to be set.
x64:
# load your coff
load $COFF "bin/test.x64.o"
# pack args
pack $ARGS "iz" "16" "Hello World x64"
# if none, pack 0
# pack $ARGS "b" "0x0"
# set desired entry point
setg "%entrypoint" "go"config.spec
To produce the final PIC, use Crystal Palace's piclink utility:
atomic-bofs$ ./piclink harness/loader.spec x64 test.x64.bin @test/config.specWe are providing the harness' loader.spec as the main spec file, but the 'test' config.spec file to add the configurations to it. The output will be a .bin file that you can execute with any shellcode runner.
atomic-bofs$ /mnt/c/Tools/cpl/demo/run.x64.exe test.x64.bin
Allocated 0x00000199df9f0000 (1611 bytes) for PIC
Read 1611 bytes from test.x64.bin. Press 'enter' to continue.
BeaconOutput[0]: Hello World x643rd Party BOFs
The other example in the project uses the whoami BOF from TrustedSec's CS-Situational-Awareness-BOF repo. This one demonstrates how BOF Cocktails can be used to change the default behaviour of a BOF.
hooks.c contains a single hook for KERNEL32$GetCurrentProcess:
#include <windows.h>
HANDLE _GetCurrentProcess ( )
{
/* return pseudo handle directly */
return ( HANDLE ) ( -1 );
}
hooks.c
The hooks.spec file merges this function into the COFF and hooks the Win32 API calls.
x64:
load "../bin/hooks.x64.o"
merge
attach "KERNEL32$GetCurrentProcess" "_GetCurrentProcess"hooks.spec
Finally, the config.spec file leverages Crystal Palace's setg, resolve, and before commands.
x64:
load $COFF "whoami.x64.o"
pack $ARGS "b" "0x0"
setg "%entrypoint" "go"
setg "%hooks" "hooks.spec"
resolve "%hooks"
before "export": run %hooksconfig.spec
What we're effectively doing is hooking Crystal Palace's export command and forcing it to process hooks.spec before the final export is done. This was a really nice solution for not requiring any changes to the harness' core spec files.
You can also stack before commands if you want to run multiple spec files.
setg "%spec1" "spec1.spec"
setg "%spec2" "spec2.spec"
resolve "%spec1"
resolve "%spec2"
before "export": run %spec1
before "export": run %spec2before with foreach, but it was a little convoluted and I felt this stacking approach would be easier for people to understand.Conclusion
This is my first stab at providing repeatable, atomic test units for BOF execution. The goal was to provide an easy means of running BOFs outside of a C2 framework, whilst maintaining functionality.
Detection engineers can run 'vanilla' BOFs as a control, collect telemetry to build detections; then merge evasion tradecraft to simulate advanced threats to verify & improve those detections.
Of course, I'm not a blue teamer, so this may all have been for nout 😄