Practical Guide to Writing Beacon Object Files (BOFs) for Havoc C2
Introduction
Havoc is a modern command-and-control (C2) framework with a modular architecture. In the last post, we explored how to install and run Havoc C2.
One of its most powerful features is support for Beacon Object Files (BOFs) — compiled COFF objects that can be dynamically loaded and executed inside an agent’s process memory.
BOFs were first introduced in Cobalt Strike as a way to execute small, position-independent C programs directly in memory. Havoc adopts the same approach, allowing operators to extend functionality without recompiling the framework. This enables low-level operations with minimal footprint, often bypassing endpoint defenses that focus on process injection, scripting engines, or binary artifacts.
What is a BOF?
A BOF is a COFF object file (.o
or .obj
) that exposes a single entrypoint:
void go(char *args, unsigned long length);
When loaded, Havoc passes arguments (args
) and their length to this function. The BOF executes inside the agent process with minimal runtime support:
- No C runtime (CRT): standard library functions are unavailable.
- No static initializers: global constructors and destructors won’t run.
- Minimal imports: only what you explicitly reference.
- Direct API interaction: all output, memory handling, and argument parsing must go through Havoc’s
beacon.h
helpers.
Writing BOFs requires a bare-metal mindset: no printf
, no malloc
, no C++ abstractions like std::string
. Every call must be intentional and lightweight to keep the object portable and stealthy.
Environment Setup
- Install Havoc: Follow Havoc’s Installation in my previous blog
- Clone the BOF headers: Use
beacon.h
from Cobalt Strike. This provides APIs like:BeaconPrintf
for output.- Argument parsing helpers.
- Compiler: Use MSVC (Windows) or MinGW-w64 (Linux). Both can generate
.o/.obj
files compatible with Havoc.
Minimal BOF Example
We’ll start with a harmless example: calling MessageBoxA
from user32.dll
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <windows.h>
#include "beacon.h"
#ifdef __cplusplus
extern "C" {
#endif
// int WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
typedef int (WINAPI *PFN_MessageBoxA)(HWND, LPCSTR, LPCSTR, UINT);
void go(char *args, unsigned long length) {
HMODULE hUser32 = LoadLibraryA("user32.dll");
if (!hUser32) { BeaconPrintf(CALLBACK_ERROR, "Failed to load user32.dll\n"); return; }
PFN_MessageBoxA pMessageBoxA = (PFN_MessageBoxA)GetProcAddress(hUser32, "MessageBoxA");
if (!pMessageBoxA) { BeaconPrintf(CALLBACK_ERROR, "Failed to resolve MessageBoxA\n"); return; }
pMessageBoxA(NULL, "Hello from BOF", "Dynamic BOF", MB_OK | MB_ICONINFORMATION);
BeaconPrintf(CALLBACK_OUTPUT, "MessageBoxA executed dynamically\n");
}
#ifdef __cplusplus
}
#endif
Compilation
Using MSVC (Windows)
1
cl.exe /c /GS- /O2 /Fo:messageBox.obj messageBox.c
NOTE
This didn’t worked for me! But when you compile it in Visual Studio.
Refer this
Using MinGW (Linux)
1
x86_64-w64-mingw32-gcc -c messageBox.c -o messageBox.o -w
Both commands produce an object file (.obj
or .o
) ready for Havoc.
Loading the BOF in Havoc
In Havoc:
- Use the
inline-execute
command to run BOFs.
Example:
1
2
# inline-execute /path/to/messageBox.o
/home/fury/Desktop/C2/havoc_bof_dev/messageBox.o
- Havoc loads the object, locates the
go
function, and executes it. - Output appears in the console via
BeaconPrintf
.
In Interact tab of Havoc GUI, you’ll see the following logs:
02/09/2025 15:30:14 [5pider] Demon » inline-execute /home/fury/Desktop/C2/havoc_bof_dev/messageBox.o
[*] [10DF3C7B] Tasked demon to execute an object file: /home/fury/Desktop/C2/havoc_bof_dev/messageBox.o
[+] Send Task to Agent [31 bytes]
And in the Target Windows OS, you’ll see a message box like this:
Writing Effective BOFs
Resolve APIs dynamically
UseLoadLibraryA
+GetProcAddress
to avoid static imports.Avoid CRT functions
Implement small helpers or use Windows APIs (lstrlenA
,RtlMoveMemory
).Keep stack usage small
Don’t declare large arrays on the stack. UseHeapAlloc
if needed.Test in a harness
Write a simple harness program that loads your.o
viago
so you can debug without Havoc.
By following this rule, let’s write a BOF that beeps via MessageBeep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// bof_messagebeep.c
#include <windows.h>
#include "beacon.h"
#ifdef __cplusplus
extern "C" {
#endif
// typedef from the SDK prototype: WINUSERAPI WINBOOL WINAPI MessageBeep(UINT uType);
typedef BOOL (WINAPI *PFN_MessageBeep)(UINT uType);
void go(char *args, unsigned long length) {
// optional: allow operator to pass a UINT uType; default to MB_ICONEXCLAMATION
UINT uType = MB_ICONEXCLAMATION;
if (args && length >= sizeof(int)) {
datap parser;
BeaconDataParse(&parser, args, length);
if (parser.buffer) {
// BeaconDataInt returns 32-bit; treat as UINT for MessageBeep
uType = (UINT)BeaconDataInt(&parser);
}
}
HMODULE hUser32 = LoadLibraryA("user32.dll");
if (!hUser32) {
BeaconPrintf(CALLBACK_ERROR, "user32.dll load failed\n");
return;
}
PFN_MessageBeep pMessageBeep =
(PFN_MessageBeep)GetProcAddress(hUser32, "MessageBeep");
if (!pMessageBeep) {
BeaconPrintf(CALLBACK_ERROR, "GetProcAddress(MessageBeep) failed\n");
return;
}
if (!pMessageBeep(uType)) {
BeaconPrintf(CALLBACK_ERROR, "MessageBeep call failed\n");
return;
}
BeaconPrintf(CALLBACK_OUTPUT, "MessageBeep OK (uType=0x%x)\n", uType);
}
#ifdef __cplusplus
}
#endif
Compilation
1
2
# Using MinGW (Linux)
x86_64-w64-mingw32-gcc -c bof_messagebeep.c -o bof_messagebeep.o -w
I’ll revise the general workflow.
General workflow
Install mingw-w64 headers (
sudo apt install mingw-w64
on Debian/Ubuntu).Search for the function you need:
1
grep -R "FunctionName" /usr/x86_64-w64-mingw32/include
- Take the prototype, replace the function name with
(*Name)
→ you now have a typedef.
Example: LoadLibraryA
1
grep -R "LoadLibraryA" /usr/x86_64-w64-mingw32/include
I find this way more fast than referring to msdn docs.
Yields:
1
/usr/x86_64-w64-mingw32/include/winbase.h: WINBASEAPI HMODULE WINAPI LoadLibraryA (LPCSTR lpLibFileName);
Declare typedef:
1
typedef HMODULE (WINAPI *PFN_LoadLibraryA)(LPCSTR lpLibFileName);
Just remember:
Given a function:
1
RETURNTYPE WINAPI FunctionName(TYPE1, TYPE2, ...);
make a typedef:
1
typedef RETURNTYPE (WINAPI *PFN_FunctionName)(TYPE1, TYPE2, ...);
Many people prefix with PFN_
or LPFN_
to avoid clashing with real symbols.
Every Win32 API exported from kernel32.dll
, user32.dll
, advapi32.dll
, ws2_32.dll
, etc. can be used the same way. You take the prototype from the SDK, turn it into a typedef, then resolve it with GetProcAddress
.