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.hhelpers.
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.hfrom Cobalt Strike. This provides APIs like:- BeaconPrintffor output.
- Argument parsing helpers.
 
- Compiler: Use MSVC (Windows) or MinGW-w64 (Linux). Both can generate .o/.objfiles 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-executecommand 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 gofunction, 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 
 Use- LoadLibraryA+- GetProcAddressto 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. Use- HeapAllocif needed.
- Test in a harness 
 Write a simple harness program that loads your- .ovia- goso 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-w64on 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.
