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:
| Category | Functions | Purpose |
|---|---|---|
| Data Parsing | BeaconDataParse, BeaconDataInt, BeaconDataShort, BeaconDataLength, BeaconDataExtract | Parse the argument buffer passed to go() |
| Output | BeaconPrintf, BeaconOutput | Send text/data back to the operator |
| Formatting | BeaconFormatAlloc, BeaconFormatFree, BeaconFormatAppend, BeaconFormatPrintf, BeaconFormatToString, BeaconFormatInt, BeaconFormatReset | Build structured output buffers |
| Process/Token | BeaconUseToken, BeaconRevertToken, BeaconIsAdmin, BeaconGetSpawnTo, BeaconSpawnTemporaryProcess, BeaconInjectProcess, BeaconCleanupProcess | Token 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
start of buffer
current cursor
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:
| Function | Signature | Purpose |
|---|---|---|
| BeaconUseToken | void BeaconUseToken(HANDLE token) | Impersonate using the given token |
| BeaconRevertToken | void BeaconRevertToken(void) | Revert to original token |
| BeaconIsAdmin | BOOL BeaconIsAdmin(void) | Check if current context is elevated |
| BeaconGetSpawnTo | void BeaconGetSpawnTo(BOOL x86, char* buf, int len) | Get the spawnto path for process creation |
| BeaconSpawnTemporaryProcess | BOOL BeaconSpawnTemporaryProcess(...) | Create a sacrificial process for post-ex |
| BeaconInjectProcess | void BeaconInjectProcess(HANDLE hProc, int pid, char* pay, int pay_len, int offset, char* arg, int arg_len) | Inject payload into a process |
| BeaconCleanupProcess | void 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?
Q2: How does a BOF call GetCurrentProcessId from KERNEL32.dll?
Q3: What byte order does BeaconFormatInt use when appending an integer?