Post

Practical Guide to Writing Beacon Object Files (BOFs) for Havoc C2

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

  1. Install Havoc: Follow Havoc’s Installation in my previous blog
  2. Clone the BOF headers: Use beacon.h from Cobalt Strike. This provides APIs like:
    • BeaconPrintf for output.
    • Argument parsing helpers.
  3. 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:

  1. 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
  1. Havoc loads the object, locates the go function, and executes it.
  2. 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:

MessageBox

Writing Effective BOFs

  1. Resolve APIs dynamically
    Use LoadLibraryA + GetProcAddress to avoid static imports.

  2. Avoid CRT functions
    Implement small helpers or use Windows APIs (lstrlenA, RtlMoveMemory).

  3. Keep stack usage small
    Don’t declare large arrays on the stack. Use HeapAlloc if needed.

  4. Test in a harness
    Write a simple harness program that loads your .o via go 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.

This post is licensed under CC BY 4.0 by the author.