BOF (Beacon Object File) loader

← runtime index · docs/index

TL;DR

Load + execute a Cobalt Strike-style Beacon Object File (BOF) — a compiled COFF object — entirely in process memory. Parses COFF, applies relocations, resolves entry-point, jumps into RWX memory. x64-only; no Beacon-API helpers (BOFs that call BeaconOutput etc. crash).

Primer

A BOF is a relocatable COFF (.o) object compiled by MSVC / MinGW. The format is the same as Linux's .o but for Windows PE-style relocations. BOFs were popularised by Cobalt Strike's inline-execute command — a tactical execution primitive that runs a small piece of native code inside the implant's process without spawning a fresh process or writing a PE to disk.

Use cases:

  • Run small Windows-API-heavy snippets (token enum, share enum, share scan) that don't need a full PE infrastructure.
  • Distribute compiled techniques as a .o artefact rather than a full implant.
  • Compose with the implant's runtime — the BOF runs in the caller's address space, so it can interact with implant state directly.

How It Works

flowchart LR
    INPUT[BOF .o bytes] --> PARSE[parse COFF<br>header + sections]
    PARSE --> ALLOC[VirtualAlloc RWX<br>copy .text + .data]
    ALLOC --> RELOC[apply relocations<br>ADDR64 / ADDR32NB / REL32]
    RELOC --> SYM[resolve entry symbol<br>from COFF symtab]
    SYM --> EXEC[jump to entry<br>via function ptr]
    EXEC --> OUT[capture output<br>via stdout redirect]

API Reference

SymbolDescription
type BOFLoaded BOF instance
Load(data []byte) (*BOF, error)Parse + relocate + ready to execute
(*BOF).Execute(args []byte) ([]byte, error)Run the entry point; return captured stdout

Examples

Simple — load + execute

import (
    "os"

    "github.com/oioio-space/maldev/runtime/bof"
)

data, _ := os.ReadFile("whoami.o")
b, err := bof.Load(data)
if err != nil {
    return
}
output, _ := b.Execute(nil)
fmt.Println(string(output))

Composed — chain multiple BOFs

for _, path := range []string{"whoami.o", "netstat.o", "tasklist.o"} {
    data, _ := os.ReadFile(path)
    b, err := bof.Load(data)
    if err != nil {
        continue
    }
    out, _ := b.Execute(nil)
    fmt.Printf("=== %s ===\n%s\n", path, out)
}

OPSEC & Detection

ArtefactWhere defenders look
VirtualAlloc(RWX) followed by EXECUTE from the allocBehavioural EDR — high-fidelity reflective-loader signal
Module-load events for non-stack .text regionsETW Microsoft-Windows-Threat-Intelligence
BOF entry-point execution from non-image memoryDefender for Endpoint MsSense

D3FEND counters:

  • D3-PA — RWX execute-from-allocation telemetry.
  • D3-FCA — YARA on the loaded bytes.

Hardening for the operator:

  • Allocate RW then RX via VirtualProtect instead of RWX — defeats the simplest RWX-watcher rules.
  • Encrypt the BOF at rest via crypto; decrypt + load + immediately re-encrypt the source buffer.
  • Pair with evasion/sleepmask for cleartext-at-rest mitigation.

MITRE ATT&CK

T-IDNameSub-coverageD3FEND counter
T1059Command and Scripting Interpreterpartial — in-memory native code executionD3-PA
T1620Reflective Code Loadingfull — COFF reflective loadD3-FCA, D3-PA

Limitations

  • No Beacon-API resolution. BOFs that call BeaconOutput, BeaconFormatAlloc, BeaconErrorD etc. crash. Use BOFs built without the Beacon-API contract or implement a stub resolver (out of scope here).
  • x64 only. Machine == 0x8664 required.
  • Limited relocation types. ADDR64 / ADDR32NB / REL32 only; exotic relocations (TLS, GOT) not supported.
  • No symbol resolution beyond the entry point. External imports are not resolved — pure in-process code only.
  • RWX allocation is loud. Hardened EDRs flag RWX from any source; pair with sleep-mask + RW→RX flip.

See also