During last months we were busy working on our bof-launcher project. In essence it is an open-source library for loading, relocating and launching Cobalt Strike’s BOFs on Windows and UNIX/Linux systems. But it also contains other very interesting features and capabilities that are worth discussing.

I won’t write in length here what BOFs are and why they’re so useful as this was already discussed elsewhere. As a brief refresher though, I will just remaind that BOFs were introduced for the first time by Raphael Mudge in Cobalt Strike v4.1. They were defined as follows:

Beacon Object Files are a way to build small post-ex capabilities that execute in Beacon, parse arguments, call a few Win32 APIs, report output, and exit. A Beacon Object File is an object file, produced by a C compiler, that is linked and loaded by Cobalt Strike. Beacon Object Files can call Win32 APIs and have access to some internal Beacon APIs (e.g., for output, token impersonation, etc.).

When starting bof-launcher project we wanted to extend BOFs capabilities, versatility and usefulness even further. Specifically, we had following design goals in mind:

  1. Running BOFs on other CPU architectures - currently bof-launcher supports following platform/architectures: Windows x86/x86_64, Linux x86/x86_64/ARM/AArch64.

  2. Writing cross-platforms BOFs - writing cross-platform BOFs in bof-launcher is possible thanks to using Zig’s thin OS abstraction layer. For an example of such cross-platform BOF (can be compiled and run on follwoing CPU architectures: x86/x86_64/ARM/AArch64), see our udpScanner.

  3. Writing BOFs in more user-friendly language - using bof-launcher you can write BOFs in C and in Zig language. Zig is a low-level langauge with a goal of being a “better C”. All the features of the language and its rich standard library can be used in BOFs (hash maps and other data structures, cross-platform OS layer, http, networking, threading, crypto and more).

  4. Implement (almost) every C2 implant’s functionality as BOFs - thanks to its flexible API, with bof-launcher it is possible to:

    • run more time consuming BOFS (ex. UDP scanning of wide IP ranges) in a seperate thread,
    • injecting and running BOF’s code in a seperate process,
    • store multiple BOFs in memory and synchronize it’s execution,
    • create BOFs pipeline, i.e.: a sequence of two or more BOFs in which output of the current BOF in the pipeline is processed as an input by the next BOF in a pipeline,
    • dynamically changing certain C2 implant behaviour (ex. changing network protocol for C2) by implementing it as a BOF.

Building bof-launcher library

To start using bof-launcher project follow these straightforward steps:

  1. Download Zig package suitable for your operating system (Windows / macOS / Linux) available at https://ziglang.org/download/. Unpack it. For rest of this article I will assume that your Zig package is located in your home directory and its directory name is changed to zig.

  2. Add .\zig\ directory to your PATH environment variable. From now on Zig toolchain is deployed for the current user, and you’re ready to get and build our bof-launcher.

  3. Get bof-launcher sources:

git clone https://github.com/The-Z-Labs/bof-launcher
  1. Build project’s sources and tests:
cd bof-launcher
zig build
zig build test    
  1. Build artifacts for bof-launcher library will show up in zig-out/lib folder:
bof-launcher_win_x64.lib
bof-launcher_win_x64_shared.dll
bof-launcher_win_x64_shared.lib
bof-launcher_win_x86.lib
libbof-launcher_lin_aarch64.a
libbof-launcher_lin_aarch64_shared.so
libbof-launcher_lin_arm.a
libbof-launcher_lin_arm_shared.so
libbof-launcher_lin_x64.a
libbof-launcher_lin_x64_shared.so
libbof-launcher_lin_x86.a

Using bof-launcher library in C/C++ program

Now, to illustrate how to use bof-launcher in C-based program we will implement simple BOF and a basic BOF runner (the program that will take a path to the BOF and run it). For now we will stick to Windows-based environments as we will be working with Windows-only BOF at this time. The full source code of discussed sample is available for downloading at our repository here: minimal BOF win_x64.

Getting sources:

git clone https://github.com/The-Z-Labs/bof-minimal_win_x64

The repository contains following files:

beacon.h
bof_launcher_api.h

example_bof.c
example_bof_runner.c

bof_launcher_win_x64.lib
build.bat
  1. beacon.h - a header provided by Cobalt Strike authors. Provides definition of internal Beacon API available for BOFs. Latest version is available here.

  2. bof_launcher_api.h - Part of the bof-launcher project. Provides definition of bof-launcher API functions. Latest version is available here.

  3. example_bof.c - simple BOF that gets Windows version with RtlGetVersion function and prints it with BeaconPrintf.

  4. example_bof_runner.c - simple BOF runner illustrating basic usage of bof-launcher library API. We will discuss its content in more details later.

  5. bof_launcher_win_x64.lib - statically compiled version of bof-launcher for Windows x86_64 platform.

To build provided BOF, simply run (you could also build it with Visual Studio or mingw compilers):

zig build-obj -O ReleaseSmall -target x86_64-windows-gnu -lc example_bof.c

To build provided BOF runner, just invoke (we’re using zig as a drop-in C/C++ compiler here):

zig cc -lc -mcpu=x86_64 -o example_bof_runner.exe example_bof_runner.c bof_launcher_win_x64.lib -lole32 -lws2_32

Now, we will walk thru initialization and basic usage of bof-launcher library (for brevity error checking code will be skipped).

// ...
#include "bof_launcher_api.h"

// ...
FILE* fp = fopen("example_bof.obj", "rb");
// ...

fseek(fp, 0, SEEK_END);
long len = ftell(fp);
fseek(fp, 0, SEEK_SET);

void* buf = malloc(len);
// ...

fread(buf, 1, len, fp);

// ...
fclose(fp);

Initialization of bof-launcher library:

if (bofLauncherInit() < 0) {
    fprintf(stderr, "Failed to init bof-launcher library.\n");
}

Loading object file (COFF x86/x86_64 or ELF x86/x86_64/ARM/AArch64) and getting handle to it:

BofObjectHandle bof_handle;
if (bofObjectInitFromMemory((unsigned char*)buf, len, &bof_handle) < 0) {
    fprintf(stderr, "Failed to parse/init BOF.\n");
    return 1;
}
free(buf);

Creating BOF context that represents single execution of a particular BOF. Among other stuff, It stores BOF’s output and BOF’s exit code:

BofContext* bof_context = NULL;
if (bofObjectRun(bof_handle, NULL, 0, &bof_context) < 0) {
    fprintf(stderr, "Failed to run BOF.\n");
    return 1;
}

Getting and printing output of last BOF’s run:

const char* output = bofContextGetOutput(bof_context, NULL);
if (output) {
    printf("\n%s\n", output);
}

Cleanup functions:

bofContextRelease(bof_context);
bofObjectRelease(bof_handle);
bofLauncherRelease();