Files
2026-08-13 10:39:51 -04:00

13 KiB

+++ title = "BOF Loading System" chapter = false weight = 40 +++

BOF Loading System

This document explains how the load command works in the Dark Agent and details the BOF (Beacon Object Files) loading workflow.

Overview

The Dark Agent provides a robust system for dynamically loading and executing BOF (Beacon Object Files) commands. This enables operators to extend the agent's functionality without rebuilding it, making it highly adaptable for various operational needs.

BOF Loading Workflow

The following diagram illustrates the BOF loading process:

┌─────────────────┐     ┌───────────────┐     ┌─────────────────┐     ┌─────────────────┐
│                 │     │               │     │                 │     │                 │
│   Mythic UI     │────▶│  Mythic Server│────▶│   Dark Agent    │────▶│   BOF Registry  │
│                 │     │               │     │                 │     │                 │
└─────────────────┘     └───────────────┘     └─────────────────┘     └─────────────────┘
       │                       │                      │                        │
       │                       │                      │                        │
       ▼                       ▼                      ▼                        ▼
┌─────────────────┐     ┌───────────────┐     ┌─────────────────┐     ┌─────────────────┐
│                 │     │               │     │                 │     │                 │
│ User selects    │     │ Server finds  │     │ Agent downloads │     │ BOF stored in   │
│ command to load │     │ BOF file and  │     │ BOF file via    │     │ memory registry │
│                 │     │ provides ID   │     │ chunked transfer│     │ for execution   │
└─────────────────┘     └───────────────┘     └─────────────────┘     └─────────────────┘

Detailed Process

The BOF loading system follows these steps:

  1. Command Selection: The operator selects a command to load from the Mythic UI.

    • The command options are dynamically generated by the agent by comparing available commands with currently loaded commands.
    • This list is provided through the get_commands function in LoadArguments class.
  2. Server File Preparation:

    • The Mythic server locates the corresponding BOF file with a name pattern of {payload_uuid}_{command_name}.o.
    • The server assigns a unique file ID to this BOF file for secure transfer.
  3. File Transfer Initialization:

    • The agent receives the task with the file ID and command name.
    • It initializes a chunked download process through the download_bof_file method.
  4. Chunked File Download:

    • The agent requests chunks of the BOF file from the Mythic server.
    • Each chunk is Base64-encoded for safe transfer.
    • The agent reassembles these chunks into the complete BOF binary.
  5. BOF Registration:

    • The BOF binary is loaded into the BofRegistry with a unique name.
    • The registry stores the raw bytes and metadata (like load time) without executing the BOF.
    • This minimizes memory usage and reduces the risk of memory corruption.
  6. Command Registration:

    • The command name is added to the agent's command registry through MessageHandler.add_command.
    • This makes the command available for execution in future tasks.

Implementation Details

Server-Side (Python)

The load.py file implements the server-side logic:

class LoadCommand(CommandBase):
  # Command definition
  cmd = "load"
  needs_admin = False
  help_cmd = "load [command]"
  description = "Load a BOF'd command into the agent."
  
  # File location logic
  async def create_go_tasking(self, taskData):
    # Find BOF file based on command name
    bof_search_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage(
      TaskID=taskData.Task.ID,
      Filename=f"{taskData.Payload.UUID}_{bof_name}.o"
    ))
    
    # Add file ID to task parameters
    taskData.args.add_arg("file_id", bof_search_resp.Files[0].AgentFileId)

Agent-Side (Crystal)

The agent implements BOF loading in command_handler.cr:

# Load a BOF file and register it as a Mythic command
register_command("load") do |args|
  # Extract parameters
  file_id = args["parameters"]["file_id"]?.try(&.as_s) || ""
  command_name = args["parameters"]["command"]?.try(&.as_s) || ""
  
  # Download BOF from file_id
  bof_data = download_bof_file(file_id, task_id)
  
  # Load the BOF into the registry
  bof_name = @bof_registry.load(command_name, bof_data)
  
  # Add the new command to the registry
  MessageHandler.add_command(bof_name)
end

BOF Registry (Crystal)

The BofRegistry class in registry.cr manages loaded BOFs:

class BofRegistry
  # BofEntry stores metadata for a loaded BOF
  record BofEntry,
    raw_bytes : Bytes,  # The raw binary content
    load_time : Time    # When it was loaded
    
  # Map of BOF name to loaded BOF entry
  getter bofs = {} of String => BofEntry
  
  # Load a BOF from content and register it
  def load(name : String, bof_content : Bytes | String) : String
    # Convert content to bytes if necessary
    bytes = case bof_content
    when Bytes
      bof_content
    when String
      bof_content.to_slice
    else
      raise "Invalid BOF content type"
    end
    
    # Store the raw bytes in the registry
    bofs[name] = BofEntry.new(
      raw_bytes: bytes,
      load_time: Time.utc
    )
    
    return name
  end

BOF Execution Process

When a loaded BOF is executed:

  1. The agent looks up the BOF by name in the registry.
  2. It creates a new ObjectFile instance with the raw bytes.
  3. The BOF is initialized in memory with the provided arguments.
  4. The entry point (coffee function) is executed.
  5. The memory is automatically cleaned up when execution completes.
def execute(name : String, args : Array(String)? = nil) : Int32
  # Get the BOF entry from registry
  entry = bofs[name]?
  
  # Create an ObjectFile instance and execute it
  obj_data = IO::Memory.new(entry.raw_bytes)
  ObjectFile.new(obj_data, args_to_use)
  
  return 0 # Success
end

BOF Build and Packaging Process

The Dark Agent includes a sophisticated system for building, packaging, and deploying BOFs. This process happens automatically during agent generation and ensures that all necessary BOF files are available for the agent to load at runtime.

BOF Build Process Overview

┌─────────────────┐     ┌───────────────┐     ┌─────────────────┐     ┌─────────────────┐
│                 │     │               │     │                 │     │                 │
│  Source Code    │────▶│  Build Script │────▶│ Compiled BOFs   │────▶│  Mythic Storage │
│  (C Files)      │     │  (build.sh)   │     │ (.o Files)      │     │                 │
│                 │     │               │     │                 │     │                 │
└─────────────────┘     └───────────────┘     └─────────────────┘     └─────────────────┘

Build Steps

  1. BOF Source Code Compilation

    • BOFs are written in C and stored in the src/bofs/c/ directory
    • Each BOF must include a coffee() function as its entry point
    • The build script compiles each BOF using GCC with the command:
    gcc -fPIC -c src/bofs/c/file.c -o output/bofs/file.o -I src/bofs/c/includes
    
  2. BOF Storage in Mythic

    • During the agent build process, all compiled BOFs are uploaded to Mythic's file storage
    • Each BOF is stored with a filename pattern of {payload_uuid}_{bof_name}.o
    • These stored BOFs become available to the agent when it's deployed
  3. BOF Loading at Runtime

    • When an operator issues a load command, the agent:
      • Identifies the BOF by name
      • Retrieves the BOF file from Mythic using the file ID
      • Downloads and loads the BOF into memory
      • Registers the BOF as a command in the agent

Inside the Build Script

The build process is handled by the build.sh script, which performs the following tasks:

# Find and compile all BOF files
build_bofs() {
    # Find all .c files in src/bofs/c directory
    for c_file in src/bofs/c/*.c; do
        base_name=$(basename "$c_file")
        output_name="${base_name%.c}.o"
        gcc -fPIC -c "$c_file" -o "output/bofs/$output_name" -I src/bofs/c/includes
    done
}

Mythic Integration

The builder.py file in the agent's Mythic implementation handles the integration with Mythic:

# Build BOFs and store in Mythic
build_cmd = ["./build.sh", "-b"]
proc = subprocess.Popen(build_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(agent_build_path))
stdout, stderr = proc.communicate()

# Get all BOF object files
bofs_dir = agent_build_path / "output" / "bofs"
bof_files = [f for f in os.listdir(bofs_dir) if f.endswith('.o')]

# Store each BOF in Mythic's file system
for bof_file in bof_files:
    bof_path = bofs_dir / bof_file
    bof_name = os.path.splitext(bof_file)[0]
    bof_content = open(bof_path, 'rb').read()
    
    # Store BOF in Mythic with payload UUID as reference
    file_resp = await SendMythicRPCFileCreate(MythicRPCFileCreateMessage(
        PayloadUUID=self.uuid,
        FileContents=bof_content,
        Filename=f"{self.uuid}_{bof_file}",
        Comment=f"Available BOF: {bof_name}"
    ))

This integration ensures that:

  1. Every BOF is compiled during the agent build process
  2. Each BOF is uploaded to Mythic and associated with the specific payload
  3. The BOFs are available for download by the agent when needed

Adding Custom BOFs to the Build Process

To add a new BOF to the build process:

  1. Create a new C file in the src/bofs/c/ directory with a coffee() function:

    #include "beacon.h"
    
    void coffee() {
        // Your BOF code here
        BeaconPrintf(CALLBACK_OUTPUT, "BOF output");
    }
    
  2. Add a corresponding Mythic command wrapper if desired (not required, but provides UI integration)

  3. The build process will automatically:

    • Compile your BOF
    • Upload it to Mythic
    • Make it available for loading by the agent

Security Considerations

  • BOFs are stored as raw bytes and only initialized when executed, minimizing memory usage.
  • Each BOF runs in isolation, reducing the chance of memory corruption.
  • The chunked file transfer ensures integrity during download.
  • BOFs can be unloaded when no longer needed, reducing the agent's memory footprint.
  • BOFs are compiled separately from the agent, allowing for updates without redeploying the agent.

Symbol Resolution Order (OPSEC Enhancement)

The Dark Agent uses an optimized symbol resolution order when loading BOFs that improves operational security:

  1. Crystal Runtime (Current Process) - Checks the Crystal standard library and statically linked libraries first
  2. System Libraries (RTLD_DEFAULT) - Falls back to dynamically loaded system libraries only if needed

This approach provides several OPSEC benefits:

  • Reduced System Calls: By checking the Crystal stdlib first (which is already loaded in the current process), the agent avoids unnecessary dlsym calls to system libraries for common functions.
  • Lower Detection Surface: Fewer interactions with the dynamic linker means less observable behavior for EDR/monitoring solutions.
  • Performance: Symbol resolution from the current process is faster than searching through all loaded libraries.
  • Reliability: Common functions like malloc, free, strlen, etc. are resolved from Crystal's runtime, ensuring consistent behavior.

The implementation prioritizes stealth by minimizing system interactions while maintaining full BOF compatibility.

Summary

The load command and BOF build system provide a powerful mechanism for dynamically extending agent capabilities at runtime through secure BOF loading. This system enables operators to add functionality to a running agent without needing to rebuild or redeploy it, making it highly flexible for various operational scenarios.