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.[1] Instead, BOFs communicate with the loader through the Beacon API -- a set of functions declared in beacon.h.[2] COFFLoader implements a compatibility layer for these functions so BOFs written for Cobalt Strike work unchanged.[3] This module covers the API contract from the BOF author's perspective.
Lab Files: step03-beacon-api
Full beacon.h with all API declarations, plus a BOF that demonstrates argument parsing with datap and formatted output with formatp.
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.[4] 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.[5] This matches Cobalt Strike's internal data format for structured output. The COFFLoader compatibility layer implements this with a byte-swap before writing.[3]
Token and Process Functions
These functions are available but have limited or stub implementations in COFFLoader's compatibility layer.[3] They are primarily useful in the context of a real Cobalt Strike Beacon:[6]
| 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.[7] The loader parses the symbol name, splits on $, calls LoadLibraryA("LIBRARY") to load the DLL, and then GetProcAddress(hLib, "Function") to resolve the function.[3] 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.[8] 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.[3]
Why the API Is Designed This Way
The Beacon API may seem unusual compared to conventional C programming, but every design decision traces back to the constraints of in-process execution. A BOF runs inside the Beacon process itself -- there is no separate address space, no loader-initialized runtime, and no process startup sequence.[1] Understanding these constraints explains why the API takes its particular shape.
No C Runtime Library. When a normal C program starts, the OS loader calls the CRT entry point (mainCRTStartup or similar), which initializes the heap manager, sets up stdio file descriptors, processes environment variables, and parses command-line arguments before calling main(). A BOF never goes through this initialization. The loader maps the COFF sections into memory, resolves relocations, and jumps straight to go().[9] This means printf, malloc, fopen, and every other CRT function is unavailable. The BOF author cannot even call strlen unless they declare the MSVCRT import explicitly using the LIBRARY$Function convention (e.g., MSVCRT$strlen).[7]
No stdout or stderr. Even if the CRT were available, a Beacon process typically has no console attached. It runs as a background process (often injected into another process), so there is no terminal to write to. The BeaconPrintf and BeaconOutput functions exist to buffer output internally and transmit it back to the operator over the C2 channel. This output buffering is fundamental -- without it, any text the BOF produces would simply vanish.[8]
No heap guarantees. Because the CRT heap is not initialized, the BOF cannot call malloc or free directly. The format API (BeaconFormatAlloc, BeaconFormatFree) provides a controlled allocation mechanism. In Cobalt Strike's real implementation, these allocations are managed by the Beacon runtime. In COFFLoader's compatibility layer, they fall through to the Win32 heap functions (HeapAlloc/HeapFree) or calloc/free from the host process CRT.[3]
Cursor-based parsing over serialized buffers. Arguments cannot be passed as function parameters in the usual sense because the BOF entry point has a fixed signature: void go(char* args, int len). All arguments must be serialized into a single byte buffer by the operator's client, transmitted over the C2 channel, and deserialized by the BOF using the datap cursor. This design avoids the need for complex marshalling infrastructure and keeps the BOF entry point uniform across all BOFs regardless of their argument types.[4]
Limitations and Edge Cases
The Beacon API is intentionally minimal, and this minimalism comes with sharp edges that BOF authors must be aware of. The API provides little to no safety checking, and misuse leads to silent corruption rather than clean error messages.
Buffer Overreads in datap
When you call BeaconDataInt or BeaconDataShort and fewer bytes remain than the function expects to read, the function returns 0 without extracting data.[2] However, the behavior of BeaconDataExtract is more nuanced. It first reads a 4-byte length prefix. If the length prefix itself can be read but the claimed length exceeds the remaining buffer, the pointer returned will reference memory past the end of the argument buffer. In COFFLoader's implementation, this does not trigger an access violation immediately -- it simply returns a pointer into whatever memory happens to follow the buffer.[3] The BOF will then read garbage data or, worse, leak adjacent memory contents.
NULL datap Handling
If a BOF is invoked with no arguments (a NULL or zero-length argument buffer), calling BeaconDataParse with a NULL pointer for the buffer parameter produces a datap structure where original is NULL and buffer points to address 0x4 (NULL + 4 due to the size prefix skip). Any subsequent call to BeaconDataInt or BeaconDataExtract on this parser will dereference this near-NULL pointer and crash. Robust BOFs should check args and len before parsing:[10]
Cvoid go(char* args, int len) {
if (args == NULL || len < 4) {
BeaconPrintf(CALLBACK_ERROR, "No arguments provided\n");
return;
}
datap parser;
BeaconDataParse(&parser, args, len);
// ... safe to proceed ...
}
formatp Buffer Overflow
The BeaconFormatAlloc function allocates a fixed-size buffer (specified by the caller). If subsequent calls to BeaconFormatPrintf or BeaconFormatAppend write more data than the allocated capacity, the behavior depends on the implementation. In COFFLoader's compatibility layer, BeaconFormatPrintf uses vsnprintf internally and will silently truncate output that exceeds the remaining capacity.[3] In contrast, BeaconFormatAppend performs a raw memcpy with no bounds checking, which means writing past the end of the format buffer causes heap corruption. BOF authors should allocate generously and track their output size, especially when formatting output from enumeration loops where the total output size is unpredictable.
Thread Safety
The Beacon API functions are not thread-safe. The datap and formatp structures maintain internal cursor state, and the global output buffer (beacon_compatibility_output in COFFLoader) is a shared resource. If a BOF spawns threads that call Beacon API functions concurrently, output will be interleaved or corrupted. In practice, this is rarely an issue because BOFs are designed to run synchronously and return quickly, but it is worth noting for advanced BOF development scenarios.[11]
COFFLoader vs. Cobalt Strike: Implementation Differences
COFFLoader's compatibility layer aims to let BOFs written for Cobalt Strike run without modification, but the two implementations diverge in several important ways.[3]
| Aspect | Cobalt Strike Beacon | COFFLoader Compatibility Layer |
|---|---|---|
| Output delivery | Buffers output and transmits it back to the team server over the C2 channel on the next callback[8] | Prints to the host process stdout immediately and appends to an internal string buffer |
| Token functions | Full implementation: BeaconUseToken applies impersonation, BeaconRevertToken reverts, integrated with Beacon's token store[6] | Stub implementations that call the underlying Win32 token APIs directly, without a token store or session tracking |
| Process spawning | BeaconSpawnTemporaryProcess creates a sacrificial process using the configured spawnto binary, with PPID spoofing and blockdll options[12] | Basic CreateProcessA wrapper without OPSEC features like PPID spoofing or blockdll policy |
| Format buffer allocation | Managed by Beacon's internal memory allocator with lifetime tied to the BOF execution context | Uses standard calloc/free from the host process CRT |
| Error handling | Errors in API calls are logged and reported back to the operator through the C2 channel | Errors may print to stderr or silently fail depending on the function |
The most significant practical difference is in the process and token functions. A BOF that calls BeaconSpawnTemporaryProcess to inject shellcode into a sacrificial process will work in Cobalt Strike with all the OPSEC features the operator has configured (spawnto path, PPID spoofing, command-line spoofing, blockdll). In COFFLoader, the same call creates a plain process with none of those protections. BOF authors testing with COFFLoader should be aware that these functions will behave correctly in a functional sense but will not provide the same operational security characteristics.[3]
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?
References
- Cobalt Strike, "Beacon Object Files," official documentation. Describes the BOF execution model, CRT unavailability, and the go() entry point convention.
- Cobalt Strike,
beacon.hheader file. Defines the datap and formatp structures and declares all Beacon API function prototypes. - TrustedSec, "COFFLoader," GitHub repository (
trustedsec/COFFLoader). Source code for the open-source COFF loader and its beacon_compatibility.c/.h compatibility layer. - Cobalt Strike, "Beacon Object Files – Argument Packing," documentation. Describes the 4-byte size prefix, length-prefixed strings, and the binary argument buffer format.
- Cobalt Strike,
beacon.h:BeaconFormatIntimplementation note. The function stores integers in big-endian (network byte order) for consistency with the team server's data parsing. - Cobalt Strike, "BOF Developer Reference," blog post. Covers token manipulation functions, process injection APIs, and their integration with Beacon's internal state.
- Cobalt Strike, "Beacon Object Files – Dynamic Function Resolution," documentation. Explains the LIBRARY$Function naming convention and how the loader resolves DLL imports at load time.
- Cobalt Strike, "Beacon Object Files – Output Functions," documentation. Describes CALLBACK_OUTPUT, CALLBACK_ERROR, and other output type constants and their rendering in the operator console.
- TrustedSec, COFFLoader source:
COFFLoader.c,RunCOFF()function. Shows how the loader maps sections, resolves symbols, applies relocations, and calls the go() entry point. - Raphael Mudge, "Cobalt Strike 4.1 – The Mark of Injection," Cobalt Strike blog, 2020. Introduces BOF development best practices including argument validation.
- TrustedSec, "Beacon Object File Loader," blog post, 2021. Discusses the design goals and limitations of running BOFs outside of Cobalt Strike.
- Cobalt Strike, "BOF Process Injection and Token APIs," documentation. Details BeaconSpawnTemporaryProcess, spawnto configuration, PPID spoofing, and blockdll integration.
Further Reading
- Cobalt Strike BOF Documentation — Official reference for Beacon Object File development, covering the API contract, argument packing, and output functions.
- TrustedSec COFFLoader — The open-source COFF loader this course is built around. Study the beacon_compatibility.c source to see how each Beacon API function is implemented outside of Cobalt Strike.
- CS-Situational-Awareness-BOF — A collection of production BOFs demonstrating real-world usage of the datap parsing API, output formatting, and DLL import conventions.
- bof-launcher — An alternative BOF runtime written in Zig that provides a different compatibility layer. Comparing its API surface with COFFLoader highlights the design choices each project makes.
- A Developer's Introduction to Beacon Object Files (TrustedSec Blog) — Walkthrough of writing your first BOF, with emphasis on argument parsing pitfalls and output formatting best practices.
- BOF.NET — A .NET-based BOF runtime that enables BOF development in .NET, useful for understanding the API contract from an alternative implementation perspective.
- Cobalt Strike Community Kit — Curated collection of community-built BOFs and aggressor scripts. Excellent source of real-world BOF examples showing diverse API usage patterns.