Module 7: The Beacon Compatibility Layer
Implementing Cobalt Strike's Beacon API from scratch so BOFs run outside of CS.
Why This Module?
BOFs are written against Cobalt Strike's Beacon API. Functions like BeaconPrintf, BeaconDataParse, and BeaconOutput are provided by the Beacon process at runtime. When running BOFs outside of Cobalt Strike (which is the entire point of COFFLoader), someone must implement these functions. This module examines COFFLoader's beacon_compatibility.c -- the standalone implementations that make BOFs work without Cobalt Strike.
Architecture of the Compatibility Layer
The compatibility layer consists of three components:
| Component | File | Purpose |
|---|---|---|
| Function declarations | beacon_compatibility.h | Struct typedefs, callback constants, function prototypes, InternalFunctions extern |
| Function implementations | beacon_compatibility.c | Actual C implementations of each Beacon API function |
| Function table | InternalFunctions[30][2] | Name-to-pointer mapping used by process_symbol() for resolution |
How BOF Calls Reach the Compatibility Layer
BeaconPrintf()indirect via functionMapping
BeaconPrintf()Output Buffering: The Global Output Buffer
In Cobalt Strike, BeaconPrintf and BeaconOutput send data back to the team server over the C2 channel. In COFFLoader, output is collected in a global buffer that can be retrieved after the BOF finishes executing:
C// Global output buffer (beacon_compatibility.c)
char* beacon_compatibility_output = NULL;
int beacon_compatibility_size = 0;
int beacon_compatibility_offset = 0;
// The output buffer grows dynamically via realloc as data is appended.
// After RunCOFF() completes, the caller retrieves output with:
char* BeaconGetOutputData(int* outsize) {
char* output = beacon_compatibility_output;
*outsize = beacon_compatibility_offset;
// Reset for next BOF execution
beacon_compatibility_output = NULL;
beacon_compatibility_size = 0;
beacon_compatibility_offset = 0;
return output;
}
Implementing BeaconPrintf
The most-used function in any BOF. COFFLoader's implementation does two things: prints to the console (for immediate visibility) and appends to the output buffer (for programmatic retrieval):
Cvoid BeaconPrintf(int type, char* fmt, ...) {
va_list args;
va_start(args, fmt);
// 1. Print to console (COFFLoader runs as a CLI tool)
vprintf(fmt, args);
va_end(args);
va_start(args, fmt);
// 2. Calculate required buffer size
int len = vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (len <= 0) return;
// 3. Allocate/grow the output buffer
char* newbuf = (char*)realloc(
beacon_compatibility_output,
beacon_compatibility_offset + len + 1
);
if (newbuf == NULL) return;
beacon_compatibility_output = newbuf;
// 4. Format the string into the buffer
va_start(args, fmt);
vsnprintf(
beacon_compatibility_output + beacon_compatibility_offset,
len + 1,
fmt,
args
);
va_end(args);
beacon_compatibility_offset += len;
}
No CRT in the BOF, CRT in the Loader
The compatibility layer itself (beacon_compatibility.c) is compiled as part of COFFLoader, which is a normal C program with full CRT access. It freely uses vprintf, vsnprintf, realloc, calloc, and free. The restriction on CRT usage applies only to the BOF code, not to the loader. The BOF calls Beacon API functions (which are in the loader's address space) through resolved function pointers, and those functions use the CRT internally.
Implementing BeaconOutput
Unlike BeaconPrintf, BeaconOutput takes raw bytes (not a format string). It is used for binary data or pre-formatted output:
Cvoid BeaconOutput(int type, char* data, int len) {
// Grow the output buffer
char* newbuf = (char*)realloc(
beacon_compatibility_output,
beacon_compatibility_offset + len + 1
);
if (newbuf == NULL) return;
beacon_compatibility_output = newbuf;
// Copy raw bytes
memcpy(
beacon_compatibility_output + beacon_compatibility_offset,
data,
len
);
beacon_compatibility_offset += len;
beacon_compatibility_output[beacon_compatibility_offset] = '\0';
}
Implementing the Data Parsing Functions
BeaconDataParse
Cvoid BeaconDataParse(datap* parser, char* buffer, int size) {
// Sanity check
if (parser == NULL) return;
parser->original = buffer;
parser->buffer = buffer + 4; // skip 4-byte size prefix
parser->length = size - 4; // remaining data after prefix
parser->size = size - 4;
}
BeaconDataInt
Cint BeaconDataInt(datap* parser) {
if (parser == NULL || parser->length < 4) return 0;
int32_t value;
memcpy(&value, parser->buffer, sizeof(int32_t));
parser->buffer += 4;
parser->length -= 4;
return value;
}
BeaconDataShort
Cshort BeaconDataShort(datap* parser) {
if (parser == NULL || parser->length < 2) return 0;
short value;
memcpy(&value, parser->buffer, sizeof(short));
parser->buffer += 2;
parser->length -= 2;
return value;
}
BeaconDataExtract
Cchar* BeaconDataExtract(datap* parser, int* size) {
if (parser == NULL || parser->length < 4) {
if (size) *size = 0;
return NULL;
}
// Read the 4-byte length prefix
int32_t length;
memcpy(&length, parser->buffer, sizeof(int32_t));
parser->buffer += 4;
parser->length -= 4;
// Return pointer to the data
char* data = parser->buffer;
if (size) *size = length;
// Advance past the data
parser->buffer += length;
parser->length -= length;
return data;
}
Implementing the Format Functions
The format functions build output buffers piece by piece. They are analogous to a string builder:
Cvoid BeaconFormatAlloc(formatp* format, int maxsz) {
if (format == NULL) return;
format->original = (char*)calloc(1, maxsz);
format->buffer = format->original;
format->length = 0;
format->size = maxsz;
}
void BeaconFormatReset(formatp* format) {
if (format == NULL) return;
memset(format->original, 0, format->size);
format->buffer = format->original;
format->length = 0;
}
void BeaconFormatFree(formatp* format) {
if (format == NULL) return;
free(format->original);
format->original = NULL;
format->buffer = NULL;
format->length = 0;
format->size = 0;
}
void BeaconFormatAppend(formatp* format, char* text, int len) {
if (format == NULL || format->length + len > format->size) return;
memcpy(format->buffer, text, len);
format->buffer += len;
format->length += len;
}
void BeaconFormatPrintf(formatp* format, char* fmt, ...) {
va_list args;
va_start(args, fmt);
int remaining = format->size - format->length;
int len = vsnprintf(format->buffer, remaining, fmt, args);
va_end(args);
if (len > 0 && len < remaining) {
format->buffer += len;
format->length += len;
}
}
char* BeaconFormatToString(formatp* format, int* size) {
if (size) *size = format->length;
return format->original;
}
BeaconFormatInt: The Endian Swap
Cvoid BeaconFormatInt(formatp* format, int value) {
// Swap from little-endian (native) to big-endian (network byte order)
// This matches Cobalt Strike's internal data format
int swapped = swap_endianess(value);
BeaconFormatAppend(format, (char*)&swapped, sizeof(int));
}
int swap_endianess(int value) {
return ((value >> 24) & 0x000000FF) |
((value >> 8) & 0x0000FF00) |
((value << 8) & 0x00FF0000) |
((value << 24) & 0xFF000000);
}
Token and Process Stubs
COFFLoader provides minimal implementations for the token/process management functions. Since COFFLoader runs as a standalone CLI tool (not an implant), some of these are stubs:
C// Token management
void BeaconUseToken(HANDLE token) {
// In a real implant, this would call ImpersonateLoggedOnUser(token)
// COFFLoader's implementation may use SetThreadToken or be a no-op
}
void BeaconRevertToken(void) {
// Revert to the process token
// In COFFLoader: RevertToSelf() or no-op
}
BOOL BeaconIsAdmin(void) {
// Check if running elevated
// COFFLoader implements this with CheckTokenMembership
// or OpenProcessToken + GetTokenInformation
return FALSE; // simplified stub
}
// Utility: convert ANSI to wide string
BOOL toWideChar(char* src, wchar_t* dst, int max) {
return MultiByteToWideChar(CP_ACP, 0, src, -1, dst, max);
}
Extending the Compatibility Layer
The 30-slot InternalFunctions table has room for additional entries. Custom C2 frameworks that integrate COFFLoader can add their own internal functions beyond the standard Beacon API. For example, a custom output function that sends data over a different channel, or a custom token management function that integrates with the framework's credential store. The BOF just needs to call a function with a matching name, and the loader will resolve it from the table.
The Complete InternalFunctions Mapping
Here is the full mapping of all Beacon API functions to their compatibility layer implementations:
| Index | Function Name | Category |
|---|---|---|
| 0 | BeaconDataParse | Data Parsing |
| 1 | BeaconDataInt | Data Parsing |
| 2 | BeaconDataShort | Data Parsing |
| 3 | BeaconDataLength | Data Parsing |
| 4 | BeaconDataExtract | Data Parsing |
| 5 | BeaconFormatAlloc | Formatting |
| 6 | BeaconFormatReset | Formatting |
| 7 | BeaconFormatFree | Formatting |
| 8 | BeaconFormatAppend | Formatting |
| 9 | BeaconFormatPrintf | Formatting |
| 10 | BeaconFormatToString | Formatting |
| 11 | BeaconFormatInt | Formatting |
| 12 | BeaconPrintf | Output |
| 13 | BeaconOutput | Output |
| 14 | BeaconUseToken | Token |
| 15 | BeaconRevertToken | Token |
| 16 | BeaconIsAdmin | Token |
| 17 | BeaconGetSpawnTo | Process |
| 18 | BeaconSpawnTemporaryProcess | Process |
| 19 | BeaconInjectProcess | Process |
| 20 | BeaconInjectTemporaryProcess | Process |
| 21 | BeaconCleanupProcess | Process |
| 22 | toWideChar | Utility |
| 23 | BeaconGetOutputData | Output |
| 24-29 | (reserved) | Available for extensions |
Pop Quiz: Beacon Compatibility Layer
Q1: How does COFFLoader's BeaconPrintf differ from Cobalt Strike's?
Q2: Why does BeaconDataParse set parser->buffer to buffer+4?
Q3: What does BeaconFormatInt do differently than simply appending 4 bytes?