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:
-
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_commandsfunction inLoadArgumentsclass.
-
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.
- The Mythic server locates the corresponding BOF file with a name pattern of
-
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_filemethod.
-
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.
-
BOF Registration:
- The BOF binary is loaded into the
BofRegistrywith 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.
- The BOF binary is loaded into the
-
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.
- The command name is added to the agent's command registry through
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:
- The agent looks up the BOF by name in the registry.
- It creates a new
ObjectFileinstance with the raw bytes. - The BOF is initialized in memory with the provided arguments.
- The entry point (coffee function) is executed.
- 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
-
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 - BOFs are written in C and stored in the
-
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
-
BOF Loading at Runtime
- When an operator issues a
loadcommand, 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
- When an operator issues a
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:
- Every BOF is compiled during the agent build process
- Each BOF is uploaded to Mythic and associated with the specific payload
- 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:
-
Create a new C file in the
src/bofs/c/directory with acoffee()function:#include "beacon.h" void coffee() { // Your BOF code here BeaconPrintf(CALLBACK_OUTPUT, "BOF output"); } -
Add a corresponding Mythic command wrapper if desired (not required, but provides UI integration)
-
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:
- Crystal Runtime (Current Process) - Checks the Crystal standard library and statically linked libraries first
- 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
dlsymcalls 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.