Module 1: What Are Beacon Object Files?
In-process execution of compiled C, without a PE, without a new process, without fork-and-run.
Why This Module?
Beacon Object Files (BOFs) are the foundation of modern post-exploitation tooling. Before you can understand how COFFLoader (by TrustedSec) parses and executes them, you need to understand what they are, why they exist, and what problem they solve compared to older execute-assembly and fork-and-run techniques. This module covers the concept, the motivation, and the architecture of BOF-based execution.
The Problem: Post-Exploitation Tooling
After an implant (Beacon, Sliver, Havoc, etc.) gains execution on a target, the operator needs to run additional tools: enumerate users, dump credentials, query Active Directory, manipulate tokens. Historically, there were two approaches to running these tools, and both had serious OPSEC problems.
Approach 1: Fork-and-Run
The implant spawns a new sacrificial process (e.g., rundll32.exe), injects shellcode or a reflective DLL into it, executes the tool, captures output, and then kills the process. This was the default model in Cobalt Strike for years.
TEXTFork-and-Run Execution Flow:
Beacon Process (PID 1234)
|
+-- CreateProcess("rundll32.exe", SUSPENDED) --> New Process (PID 5678)
+-- VirtualAllocEx(PID 5678, RWX)
+-- WriteProcessMemory(PID 5678, payload)
+-- ResumeThread(PID 5678)
+-- ReadPipe(output) <-- Tool runs in PID 5678
+-- TerminateProcess(PID 5678) <-- Sacrificial process dies
Fork-and-Run OPSEC Failures
Every fork-and-run execution creates a new process, triggers kernel callbacks (PsSetCreateProcessNotifyRoutine), generates cross-process memory allocation and write events, and leaves a terminated process in ETW logs. EDRs correlate these events trivially: a process that spawns rundll32, writes RWX memory into it, and pipes output back is textbook injection behavior. Each command execution is a fresh detection opportunity.
Approach 2: Execute-Assembly
Cobalt Strike's execute-assembly loads the .NET CLR into a sacrificial process and runs a .NET assembly. While more flexible, it still spawns a new process, loads the CLR (observable via ETW's CLR loading events and clr.dll module loads), and the .NET assembly lands in memory where AMSI can scan it.
The BOF Solution: In-Process Execution
Beacon Object Files, introduced in Cobalt Strike 4.1 (June 2020), take a fundamentally different approach. Instead of spawning a new process, a BOF runs inside the Beacon process itself, in the same thread context. No new process. No cross-process injection. No CLR. No DLL on disk.
BOF vs Fork-and-Run
New process, injection, pipe
New process, CLR, AMSI
Same process, same thread
A BOF is a compiled C object file in COFF format -- the intermediate output of the compiler before linking. It is not a PE (no PE headers, no import table, no entry point in the traditional sense). The Beacon (or COFFLoader) acts as a miniature linker: it parses the COFF headers, loads sections into memory, resolves symbols, applies relocations, and calls the entry function.
C// A minimal BOF -- this is the ENTIRE source file
#include <windows.h>
#include "beacon.h"
void go(char* args, int len) {
BeaconPrintf(CALLBACK_OUTPUT, "Hello from BOF! PID: %d\n", GetCurrentProcessId());
}
The function go is the conventional entry point for a BOF (though COFFLoader allows specifying any function name). The BOF includes beacon.h which declares the Beacon API functions. When compiled, the BOF produces a .o (object) file -- raw COFF, no linking step.
What is a COFF Object File?
COFF (Common Object File Format) is the object file format used by Microsoft's toolchain (MSVC) and MinGW. When you compile a C source file with cl.exe /c or x86_64-w64-mingw32-gcc -c, the compiler produces a .obj or .o file in COFF format. This file contains:
| Component | Purpose |
|---|---|
| COFF File Header | Machine type (x64/x86), number of sections, pointer to symbol table |
| Section Table | Array of section headers (.text, .data, .rdata, .bss) with sizes, offsets, characteristics |
| Section Data | Raw bytes for each section (compiled code, initialized data, read-only data) |
| Relocation Table | Per-section list of addresses that need fixups (because absolute addresses are unknown until load time) |
| Symbol Table | Names and metadata for all defined and external symbols (functions, variables, imports) |
| String Table | Storage for symbol names longer than 8 characters |
Critically, a COFF object file is not directly executable. It contains unresolved external references (e.g., calls to BeaconPrintf, GetCurrentProcessId) and relocations that assume a base address of zero. A linker (or a COFF loader) must resolve these references and apply relocations before the code can run.
Why COFFLoader Exists
Cobalt Strike's Beacon has a built-in COFF loader that can execute BOFs. But what if you are not using Cobalt Strike? What if you are developing a custom C2, or you want to test BOFs from the command line, or you want to integrate BOF execution into another framework?
COFFLoader by TrustedSec is a standalone, open-source COFF loader written in C. It implements the same parsing, loading, linking, and execution pipeline that Cobalt Strike's Beacon performs internally, but as a standalone program. It provides a Beacon API compatibility layer so that BOFs written for Cobalt Strike work without modification.
TEXTCOFFLoader Usage:
COFFLoader.exe go path/to/bof.o [optional hex-encoded arguments]
- "go" = name of the entry function to call
- "bof.o" = the compiled COFF object file
- hex arguments = optional BeaconDataParse-compatible argument buffer
In-Process Execution: Why It Matters
The key advantage of BOFs (and by extension, COFFLoader) is that execution happens entirely within the calling process. This has profound implications for both capability and stealth.
Advantages of In-Process BOF Execution
| Property | Fork-and-Run | BOF / COFFLoader |
|---|---|---|
| Process creation | New process per command | None -- runs in current process |
| Cross-process APIs | VirtualAllocEx, WriteProcessMemory | None -- local memory only |
| Token/handle inheritance | Must duplicate or impersonate | Inherits caller's token and handles |
| Memory footprint | Full PE or DLL loaded | Small .o file, typically 2-20 KB |
| ETW visibility | Process creation, module loads, thread creation | Only VirtualAlloc for section memory |
| Cleanup | Must terminate sacrificial process | VirtualFree the loaded sections |
Because a BOF runs in the same process and thread, it automatically inherits the current access token, any impersonated tokens, open handles, and the process environment. A BOF that queries Active Directory can use the Beacon's existing Kerberos ticket. A BOF that accesses a file share uses the Beacon's current impersonation context. No token duplication or pass-through is needed.
The Tradeoff: Stability Risk
In-process execution is a double-edged sword. If a BOF crashes (null pointer dereference, buffer overflow, unhandled exception), it crashes the entire Beacon process. There is no sacrificial process to absorb the fault. This is why BOFs must be carefully written and tested -- a bug does not just lose output, it loses the implant.
BOF Stability Rules
BOFs must not call ExitProcess or exit(). They must not use C runtime functions that rely on CRT initialization (the CRT is not initialized for the BOF). They must not leak memory (no garbage collector, no cleanup after go() returns unless explicitly coded). They must handle errors gracefully because an unhandled exception means the Beacon dies.
COFFLoader Architecture Overview
At a high level, COFFLoader performs these steps to execute a BOF. Each step will be covered in detail in subsequent modules:
TEXTCOFFLoader Execution Pipeline:
1. Read COFF file into memory buffer
2. Parse COFF file header (validate machine type, get section/symbol counts)
3. Locate section table, symbol table, string table
4. Allocate RWX memory for each section (VirtualAlloc)
5. Copy section raw data into allocated memory
6. Build function pointer table for Beacon API (InternalFunctions[30])
7. For each section, process relocations:
a. Look up the target symbol
b. Resolve symbol to address (internal section, Beacon API, or DLL import)
c. Apply the relocation fixup based on type (ADDR64, REL32, ADDR32NB, etc.)
8. Find the entry function symbol (e.g., "go" or "_go")
9. Cast the entry address to a function pointer and call it
10. Capture output from BeaconPrintf/BeaconOutput
11. Free allocated memory (VirtualFree)
BOF Compilation
A BOF is compiled but not linked. The -c flag tells the compiler to produce an object file and stop before the linking stage:
BASH# MinGW (cross-compile from Linux for Windows x64)
x86_64-w64-mingw32-gcc -c bof.c -o bof.o
# MSVC (on Windows)
cl.exe /c /GS- bof.c /Fo bof.obj
# Key flags:
# -c = compile only, do not link
# /GS- = disable stack cookies (no CRT to handle them)
# -o / /Fo = output object file name
The /GS- flag is important for MSVC: it disables stack buffer security checks (__security_check_cookie) which require the CRT to be initialized. Since a BOF runs without CRT initialization, stack cookies would cause a crash.
Pop Quiz: BOF Fundamentals
Q1: What is the primary OPSEC advantage of BOFs over fork-and-run execution?
Q2: What file format is a compiled BOF?
Q3: Why is the /GS- flag important when compiling BOFs with MSVC?