blacksandbeacon

BOF Authoring Guide

A BOF (Beacon Object File) is a position-independent ELF object the beacon loads into its own process at runtime. There is no process boundary, no shell, no interpreter. The BOF’s go runs in the beacon’s address space with the beacon’s privileges.

This document covers the contract, the build, the loader’s limitations, and the things that will silently break if you get them wrong.

Contract

Every BOF must export exactly one symbol:

void go(char *args, int alen);

args is a raw byte buffer the operator passed when scheduling the BOF. alen is its length. There is no automatic serialisation: if you want structured args, parse them yourself (use the helpers in beacon_api.h if your buffer is Cobalt Strike-style).

The function must return. It must not call exit() (libc is not linked). To stop early, just return.

The two APIs the beacon provides

void BeaconPrintf(int type, const char *fmt, ...);
void BeaconOutput(int type, const char *data, int len);

BeaconPrintf works like printf but writes into the beacon’s result buffer. BeaconOutput takes a raw byte buffer and an explicit length - useful for binary data.

Both type values are callback types from the header. In practice you want CALLBACK_OUTPUT (0). Other values are reserved for future use.

The headers

bof/include/beacon_api.h

bof/include/syscalls.h

Include both. Do not redeclare BeaconPrintf/BeaconOutput yourself - it will fail to compile with a conflicting-types error against the header.

Build

The Makefile wraps the gcc invocation:

make bof-<name>

Which expands to:

gcc -c -fPIC -nostdlib -m64 -O2 -I bof/include \
    bof/<name>/bof.c -o build/bof/<name>.x64.o

Flags you must keep:

Flag Why
-c Produce an object, not an executable
-fPIC Position-independent code, required for runtime relocation
-nostdlib No glibc, no startup files, no libc headers
-m64 x86_64 ABI

If you break the loader by removing any of these, the failure will be visible at load time, not compile time.

Symbol resolution

The beacon’s loader resolves these symbols from the BOF:

Anything else you call must be defined in the BOF itself. There is no implicit libc, no printf, no malloc. If you need memory, use brk/mmap via the syscall wrappers.

The tests/test_bof_compile.py suite catches the most common mistake: leaking a libc reference. The list of forbidden undefined symbols includes printf, puts, malloc, strlen, strcmp, memcpy, exit, and fprintf.

Pitfalls

Testing your BOF locally

Compile with the Makefile and then inspect the symbol table:

make bof-mine
nm build/bof/mine.x64.o | grep -E '^[Tt]|^[Uu]'

You should see:

If go is missing, the loader will not be able to call it. If BeaconPrintf shows up as defined, your BOF is wrong: the beacon would call your copy instead of its real one.

The test suite (make test) automates these checks for the shipped BOFs; you can extend it to cover your own by adding your BOF name to the BOFS list in tests/test_bof_compile.py.

Deploying

Drop the .x64.o into sessions/uploads/ on the C2 host. The beacon will fetch it the first time a bof:<name> command is scheduled. Files are served verbatim; there is no signing or integrity check in the protocol.