Difficulty: Beginner

Module 3: The BOF API Contract

The API that every BOF author must know: data parsing, output, and the contract between BOF and loader.

Why This Module?

BOFs cannot use the C runtime library (printf, malloc, etc.) because the CRT is never initialized. Instead, BOFs communicate with the loader through the Beacon API -- a set of functions declared in beacon.h. COFFLoader implements a compatibility layer for these functions so BOFs written for Cobalt Strike work unchanged. This module covers the API contract from the BOF author's perspective.

The beacon.h Header

Every BOF includes beacon.h, which declares the Beacon API functions. These functions are not defined in the BOF -- they are external symbols that the loader resolves at load time. The BOF calls them through function pointers that the loader patches via relocations.

The API falls into four categories:

CategoryFunctionsPurpose
Data ParsingBeaconDataParse, BeaconDataInt, BeaconDataShort, BeaconDataLength, BeaconDataExtractParse the argument buffer passed to go()
OutputBeaconPrintf, BeaconOutputSend text/data back to the operator
FormattingBeaconFormatAlloc, BeaconFormatFree, BeaconFormatAppend, BeaconFormatPrintf, BeaconFormatToString, BeaconFormatInt, BeaconFormatResetBuild structured output buffers
Process/TokenBeaconUseToken, BeaconRevertToken, BeaconIsAdmin, BeaconGetSpawnTo, BeaconSpawnTemporaryProcess, BeaconInjectProcess, BeaconCleanupProcessToken manipulation and process spawning

The datap Structure

Arguments are passed to a BOF as a raw byte buffer. The datap structure is a cursor-based parser that walks through this buffer extracting typed values:

Ctypedef struct {
    char* original;   // pointer to the start of the buffer
    char* buffer;     // current read position (advances as you extract)
    int   length;     // remaining bytes from buffer to end
    int   size;       // total size of the parseable region
} datap;

datap Parsing Model

original
start of buffer
buffer
current cursor
remaining
length bytes left

Data Parsing Functions

BeaconDataParse

Cvoid BeaconDataParse(datap* parser, char* buffer, int size);

// Initializes the parser with a buffer.
// IMPORTANT: Skips the first 4 bytes (size prefix in CS argument format).
// After this call:
//   parser->original = buffer
//   parser->buffer   = buffer + 4
//   parser->length   = size - 4
//   parser->size     = size - 4

The 4-Byte Skip

Cobalt Strike's argument packing format prepends a 4-byte little-endian length prefix to the argument buffer. BeaconDataParse skips these 4 bytes automatically. If you are building argument buffers manually for COFFLoader, you must include this 4-byte prefix or your data will be misaligned.

BeaconDataInt

Cint BeaconDataInt(datap* parser);

// Extracts a 4-byte (32-bit) integer from the current cursor position.
// Advances parser->buffer by 4 and decrements parser->length by 4.
// Returns 0 if insufficient data remains.

BeaconDataShort

Cshort BeaconDataShort(datap* parser);

// Extracts a 2-byte (16-bit) short from the current cursor position.
// Advances parser->buffer by 2 and decrements parser->length by 2.
// Returns 0 if insufficient data remains.

BeaconDataLength

Cint BeaconDataLength(datap* parser);

// Returns the number of bytes remaining in the buffer (parser->length).
// Does not modify the cursor position.

BeaconDataExtract

Cchar* BeaconDataExtract(datap* parser, int* size);

// Extracts a length-prefixed binary blob:
//   1. Reads a 4-byte length prefix from the current position
//   2. Returns a pointer to the data immediately after the prefix
//   3. Advances the cursor past the data
//   4. Sets *size to the extracted length
// This is how strings and byte arrays are packed in CS argument format.

Argument Buffer Format

When Cobalt Strike (or COFFLoader with hex arguments) passes data to a BOF, the buffer is packed in a specific format:

TEXTArgument Buffer Layout:

+---4 bytes---+---4 bytes---+---N bytes---+---4 bytes---+---M bytes---+
| total_size  | len_of_str1 |   string1   | len_of_str2 |   string2   |
+-------------+-------------+-------------+-------------+-------------+

Example: Two strings "hello" and "world"
  0x12000000   // total size (18 bytes of payload)
  0x06000000   // length 6 (including null terminator)
  68656C6C6F00 // "hello\0"
  0x06000000   // length 6
  776F726C6400 // "world\0"

The first 4 bytes (total_size) are skipped by BeaconDataParse.
Each subsequent value is extracted by BeaconDataInt, BeaconDataShort,
or BeaconDataExtract depending on the expected type.

A Complete Data Parsing Example

C#include <windows.h>
#include "beacon.h"

// BOF that takes a hostname (string) and port (int)
void go(char* args, int len) {
    datap parser;
    BeaconDataParse(&parser, args, len);

    // Extract a length-prefixed string
    int hostname_len;
    char* hostname = BeaconDataExtract(&parser, &hostname_len);

    // Extract a 4-byte integer
    int port = BeaconDataInt(&parser);

    BeaconPrintf(CALLBACK_OUTPUT, "Connecting to %s:%d\n", hostname, port);

    // ... do work with hostname and port ...
}

Output Functions

BeaconPrintf

Cvoid BeaconPrintf(int type, char* fmt, ...);

// Printf-style output. In Cobalt Strike, this sends output back to
// the operator console. In COFFLoader, it prints to stdout and
// appends to an internal buffer (beacon_compatibility_output).
//
// type values:
//   CALLBACK_OUTPUT      = 0x00   // normal output
//   CALLBACK_OUTPUT_OEM  = 0x1e   // OEM-encoded output
//   CALLBACK_ERROR       = 0x0d   // error output
//   CALLBACK_OUTPUT_UTF8 = 0x20   // UTF-8 output

BeaconOutput

Cvoid BeaconOutput(int type, char* data, int len);

// Sends raw bytes as output (not printf-formatted).
// Useful for binary data or pre-formatted strings.
// Same type constants as BeaconPrintf.

The formatp Structure & Format Functions

The formatp structure is identical to datap but used for building output buffers incrementally:

Ctypedef struct {
    char* original;   // allocated buffer base
    char* buffer;     // current write position
    int   length;     // bytes written so far
    int   size;       // total allocated capacity
} formatp;

Format API Usage Pattern

Cvoid go(char* args, int len) {
    formatp buffer;

    // Allocate a format buffer (512 bytes capacity)
    BeaconFormatAlloc(&buffer, 512);

    // Append formatted text
    BeaconFormatPrintf(&buffer, "User: %s\n", "SYSTEM");
    BeaconFormatPrintf(&buffer, "PID:  %d\n", GetCurrentProcessId());

    // Append a big-endian integer (for structured data)
    BeaconFormatInt(&buffer, 42);

    // Append raw bytes
    BeaconFormatAppend(&buffer, "raw", 3);

    // Extract the built buffer and send it
    int output_size;
    char* output = BeaconFormatToString(&buffer, &output_size);
    BeaconOutput(CALLBACK_OUTPUT, output, output_size);

    // Free the format buffer
    BeaconFormatFree(&buffer);
}

BeaconFormatInt: Endian Swap

BeaconFormatInt appends a 4-byte integer in big-endian (network byte order), not native little-endian. This matches Cobalt Strike's internal data format for structured output. The COFFLoader compatibility layer implements this with a byte-swap before writing.

Token and Process Functions

These functions are available but have limited or stub implementations in COFFLoader's compatibility layer. They are primarily useful in the context of a real Cobalt Strike Beacon:

FunctionSignaturePurpose
BeaconUseTokenvoid BeaconUseToken(HANDLE token)Impersonate using the given token
BeaconRevertTokenvoid BeaconRevertToken(void)Revert to original token
BeaconIsAdminBOOL BeaconIsAdmin(void)Check if current context is elevated
BeaconGetSpawnTovoid BeaconGetSpawnTo(BOOL x86, char* buf, int len)Get the spawnto path for process creation
BeaconSpawnTemporaryProcessBOOL BeaconSpawnTemporaryProcess(...)Create a sacrificial process for post-ex
BeaconInjectProcessvoid BeaconInjectProcess(HANDLE hProc, int pid, char* pay, int pay_len, int offset, char* arg, int arg_len)Inject payload into a process
BeaconCleanupProcessvoid BeaconCleanupProcess(PROCESS_INFORMATION* pi)Clean up a spawned process

The DLL Import Convention

BOFs cannot use standard C library imports. To call a Windows API function, a BOF must declare it using a special naming convention that tells the loader which DLL to load and which function to resolve:

C// In beacon.h or the BOF source:
// DECLSPEC_IMPORT tells the compiler this is a DLL import
// The function is declared as a regular prototype

// For x64, the symbol name becomes: __imp_KERNEL32$GetCurrentProcessId
// For x86, the symbol name becomes: __imp__KERNEL32$GetCurrentProcessId
DECLSPEC_IMPORT DWORD WINAPI KERNEL32$GetCurrentProcessId(void);
DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$OpenProcess(DWORD, BOOL, DWORD);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$OpenProcessToken(HANDLE, DWORD, PHANDLE);
DECLSPEC_IMPORT NTSTATUS NTAPI NTDLL$NtQuerySystemInformation(ULONG, PVOID, ULONG, PULONG);

// Usage in the BOF:
void go(char* args, int len) {
    DWORD pid = KERNEL32$GetCurrentProcessId();
    BeaconPrintf(CALLBACK_OUTPUT, "My PID: %d\n", pid);
}

The LIBRARY$Function Convention

The LIBRARY$Function naming convention is not a Windows convention -- it is specific to BOFs. The loader parses the symbol name, splits on $, calls LoadLibraryA("LIBRARY") to load the DLL, and then GetProcAddress(hLib, "Function") to resolve the function. The __imp_ prefix (or __imp__ on x86) is added automatically by the compiler because of DECLSPEC_IMPORT (__declspec(dllimport)).

Callback Type Constants

C// Defined in beacon_compatibility.h
#define CALLBACK_OUTPUT      0x00   // standard output
#define CALLBACK_OUTPUT_OEM  0x1e   // OEM codepage output
#define CALLBACK_ERROR       0x0d   // error output (shown in red in CS)
#define CALLBACK_OUTPUT_UTF8 0x20   // UTF-8 encoded output

In Cobalt Strike, these control how the output is displayed in the operator console. In COFFLoader, CALLBACK_OUTPUT and CALLBACK_ERROR both go to stdout, but the type is preserved in the output buffer for frameworks that consume COFFLoader's output programmatically.

Pop Quiz: BOF API Contract

Q1: Why does BeaconDataParse skip the first 4 bytes of the argument buffer?

Cobalt Strike's argument packing format includes a 4-byte little-endian total size prefix at the start of the buffer. BeaconDataParse skips this prefix so subsequent calls to BeaconDataInt/BeaconDataExtract read the actual argument data.

Q2: How does a BOF call GetCurrentProcessId from KERNEL32.dll?

BOFs declare DLL imports using the LIBRARY$Function convention with DECLSPEC_IMPORT. The compiler generates a symbol like __imp_KERNEL32$GetCurrentProcessId. At load time, the COFF loader splits the symbol on $, loads the DLL with LoadLibraryA, and resolves the function with GetProcAddress.

Q3: What byte order does BeaconFormatInt use when appending an integer?

BeaconFormatInt swaps the byte order to big-endian before appending the 4-byte integer. This matches Cobalt Strike's internal structured data format (network byte order) for consistency in data exchange between Beacon and the team server.