BOF Execution

Execute Beacon Object Files (BOF) in-memory on Windows agents.

Overview

NexusC2 supports execution of Beacon Object Files (BOFs), which are position-independent COFF (Common Object File Format) objects that run directly in the agent’s process memory. BOFs provide a lightweight mechanism for executing post-exploitation capabilities without dropping executables to disk.

Key Features:

  • In-memory COFF loading and execution
  • Cobalt Strike-compatible Beacon API implementation
  • Synchronous and asynchronous execution modes
  • Token impersonation support for network operations
  • Large output chunking and streaming

Platform: Windows only (x86_64)


Architecture

flowchart TB
    subgraph Client["GUI Client"]
        SELECT[Select BOF + Arguments]
    end

    subgraph Server["NexusC2 Server"]
        WS[WebSocket Service
Base64 encode, chunk if >512KB] RECV[Receive Output] end subgraph Agent["Windows Agent"] GET[Receive via GET poll] PARSE[Parse COFF headers] ALLOC[Allocate RWX memory] RESOLVE[Resolve imports
Beacon API + Windows API] EXEC[Execute entry point] CAPTURE[Capture output] end SELECT --> WS WS -->|Queue command| GET GET --> PARSE --> ALLOC --> RESOLVE --> EXEC --> CAPTURE CAPTURE -->|POST results| RECV RECV -->|Display| Client

COFF Loader

Parsing COFF Files

The loader uses the pecoff library to parse COFF object files:

// Load function signature
func Load(coffBytes []byte, argBytes []byte) (string, error)
func LoadWithTimeout(coffBytes []byte, argBytes []byte, timeout time.Duration) (string, error)

Memory Allocation

StageMemory Protection
Initial allocationPAGE_READWRITE
Section loadingPAGE_READWRITE
After relocationPAGE_EXECUTE_READ (executable sections)
CleanupVirtualFree

Section Handling

The loader processes standard COFF sections:

SectionPurpose
.textExecutable code
.dataInitialized data
.rdataRead-only data
.bssUninitialized data (zero-filled)

Symbol Resolution

Dynamic Function Resolution

BOFs can import Windows APIs using the LIBRARY$Function naming convention:

// BOF code example
DECLSPEC_IMPORT HANDLE WINAPI KERNEL32$CreateFileA(...);

The loader resolves these to actual function addresses via GetProcAddress.

Supported Libraries

LibraryCommon Functions
kernel32.dllLoadLibraryA, GetProcAddress, VirtualAlloc, CreateFileA, etc.
ntdll.dllRtlCopyMemory
user32.dllMessageBoxA, FindWindowA, GetWindowTextA
ws2_32.dllWSAStartup, socket, connect, send, recv
advapi32.dllRegOpenKeyExA, OpenProcessToken, DuplicateTokenEx

Internal Function Implementations

The loader provides custom implementations for standard C library functions:

FunctionImplementation
strlenMemory-safe length calculation
strcmp/strncmpByte-by-byte comparison
strcpy/strncpySafe string copy
memcpy/memset/memmoveMemory operations
malloc/calloc/free/reallocHeap management
vsnprintf/sprintfFormatted output

Beacon API

Output Functions

FunctionDescription
BeaconOutput(type, data, len)Write output to buffer
BeaconPrintf(type, fmt, ...)Formatted output

Output Types:

  • CALLBACK_OUTPUT (0x00) - Standard output
  • CALLBACK_OUTPUT_OEM (0x1e) - OEM character set
  • CALLBACK_OUTPUT_UTF8 (0x20) - UTF-8 encoded

Data Parsing

FunctionDescription
BeaconDataParse(parser, buffer, size)Initialize data parser
BeaconDataInt(parser)Extract 4-byte integer
BeaconDataShort(parser)Extract 2-byte integer
BeaconDataLength(parser)Get remaining data length
BeaconDataExtract(parser, size)Extract raw bytes

Format Functions

FunctionDescription
BeaconFormatAlloc(format, maxsz)Allocate format buffer
BeaconFormatFree(format)Free format buffer
BeaconFormatAppend(format, data, len)Append data
BeaconFormatPrintf(format, fmt, ...)Formatted append
BeaconFormatToString(format, size)Get formatted string
BeaconFormatInt(format, value)Append integer

Argument Packing

Format Specification

BOF arguments use a type-prefixed format:

PrefixTypeDescription
bBinaryRaw binary data (base64)
iInt324-byte signed integer
sInt162-byte signed integer
zStringNull-terminated ANSI string
ZWStringNull-terminated wide string (UTF-16)

Example Usage

# String argument
bof dir.x64.o z"C:\Windows\System32"

# Integer argument
bof enumerate.o i1234

# Wide string argument
bof search.o Z"C:\Users\*"

# Multiple arguments
bof netuser.o z"administrator" z"DOMAIN"

Synchronous Execution

Command Format

{
  "command_type": 13,
  "command": "bof z\"argument\"",
  "data": "<base64 BOF bytes>",
  "filename": "dir.x64.o"
}

Execution Flow

  1. Base64 decode BOF bytes
  2. Parse arguments (if provided)
  3. Apply token context (if impersonating)
  4. Load and execute COFF
  5. Capture output via global buffer
  6. Return result immediately

Output Chunking

Large outputs are automatically split:

SettingValue
Max chunk size100 KB
Max single response500 KB
Chunks per batch10

Asynchronous Execution

Command Format

{
  "command_type": 17,
  "command": "bof-async z\"argument\"",
  "data": "<base64 BOF bytes>",
  "filename": "longrunning.x64.o"
}

Job Management

Async BOFs run as managed jobs with streaming output:

type BOFJob struct {
    ID              string
    Name            string
    Status          string  // running, completed, crashed, killed, timeout
    StartTime       time.Time
    EndTime         *time.Time
    Output          strings.Builder
    ChunkIndex      int
    TotalBytesSent  int
    OutputTruncated bool
    TokenContext    *TokenContext
}

Job Commands

CommandDescription
bof-async-listList all BOF jobs
bof-async-output <job_id>Get job output
bof-async-kill <job_id>Kill running job

Streaming Configuration

SettingValue
Output check interval1 second
Flush interval30 seconds
Min send interval10 seconds
Min output before send50 KB
Max total output10 MB
Timeout30 minutes

Output Format

Async output uses a structured format:

BOF_ASYNC_STARTED|<job_id>|<bof_name>
BOF_ASYNC_OUTPUT|<job_id>|CHUNK_<n>|<output_data>
BOF_ASYNC_COMPLETED|<job_id>|CHUNK_<n>|<final_output>

Status markers:

  • COMPLETED - BOF finished successfully
  • CRASHED - BOF threw exception
  • KILLED - Job was cancelled
  • TIMEOUT - Exceeded 30-minute limit

Token Context

Impersonation Support

BOFs execute under the current token context:

Token TypeBehavior
No impersonationRuns as agent process identity
Regular impersonationUses active stolen/created token
Network-only tokenUses token for network operations

Token Application

// Token is duplicated for each BOF execution
func ensureTokenContextForBOF() func() {
    // Duplicate token to avoid invalidating global handle
    err := DuplicateTokenEx(sourceToken, TOKEN_ALL_ACCESS, ...)

    // Apply to current thread
    err = ImpersonateLoggedOnUser(dupToken)

    // Return cleanup function
    return func() {
        RevertToSelf()
        CloseHandle(dupToken)
    }
}

Network Share Access

For BOFs accessing network shares with netonly tokens:

  1. Token context is captured before job starts
  2. Token is duplicated for the BOF thread
  3. Impersonation applied immediately before execution
  4. Network resources tracked for cleanup
  5. Impersonation reverted after completion

Multi-Chunk BOF Transfer

Chunked Upload

Large BOF files are split for transfer:

type BOFChunkInfo struct {
    Filename    string
    TotalChunks int
    Chunks      map[int]string  // chunk number -> base64 data
    ReceivedAt  time.Time
}

Reassembly

  1. First chunk creates tracking entry
  2. Subsequent chunks stored in map
  3. When all chunks received:
    • Concatenate base64 strings
    • Decode combined data
    • Execute BOF normally

Thread Safety

Execution Mutex

BOF execution is serialized to prevent conflicts:

var bofExecutionMutex sync.Mutex

func executeBOFPlatform(bofBytes []byte, args []byte) (string, error) {
    bofExecutionMutex.Lock()
    defer bofExecutionMutex.Unlock()

    // ... execution logic
}

Output Buffer

Global output buffer protected by mutex:

var bofOutputBuffer []byte
var bofOutputMutex sync.Mutex

Error Handling

Error Codes

CodeDescription
E2Invalid COFF data (too small)
E18Base64 decode failure
E25BOF execution error
E47Job not found
E51BOF crashed/panic

Recovery

  • Panics are caught via recover()
  • Crashed jobs marked with status
  • Final output includes crash message
  • Token context properly cleaned up

Limitations

LimitationDescription
PlatformWindows x64 only
ConcurrencyOne BOF at a time (mutex)
Output size10 MB max for async
Timeout30 minutes max for async
DLL hooksMay be detected by EDR
MemoryRWX memory allocation visible

ComponentFile Path
COFF Loaderserver/docker/payloads/Windows/coff_loader.go
BOF Commandserver/docker/payloads/Windows/action_bof.go
BOF Platformserver/docker/payloads/Windows/action_bof_windows.go
BOF Asyncserver/docker/payloads/Windows/action_bof_async_windows.go
BOF Argumentsserver/docker/payloads/Windows/action_bof_args_windows.go
WebSocket Handlerserver/internal/websocket/handlers/bof_handler.go
to navigate to select ESC to close
Powered by Pagefind