Beacon Object Files
A Beacon Object File (BOF) is a compiled C program (developed by CobaltStrike and widely used in the community and other similar tools), written to a convention that allows it to execute within an Agent process and use internal Agent APIs. BOFs are a way to rapidly extend the Agent with new post-exploitation features. Users can develop their own modules following this BOF standard and run them from CoreImpact now; they can also leverage all BOFs out there.
What are the advantages of BOFs?
One of the key roles of a command & control platform is to provide ways to use external post-exploitation functionality. BOFs have a light footprint and they run inside of an Agent process.
BOFs are small. This can make a significant difference when using bandwidth constrained channels, such as DNS.
Finally, BOFs are easy to develop. You just need a Win32 C compiler and a command line. Both MinGW and Microsoft's C compiler can produce BOF files. You do not have to fuss with project settings that are sometimes more effort than the code itself.
How do BOFs work?
To the Agent, a BOF is just a block of position-independent code that receives pointers to some Agent internal APIs.
To Core Impact, a BOF is an object file produced by a C compiler. Core Impact parses this file and acts as a linker and loader for its contents. This approach allows you to write position-independent code, for use in the Agent, without tedious gymnastics to manage strings and dynamically call Win32 APIs.
Some considerations:
-
If the BOF ends up exfiltrating a file from the target system, it is stored at %PROGRAMDATA%\BOFOutputFiles
-
Architecture of the BOF must match with the architecture of the agent (32bits or 64bits) on which it is running
-
BOF only work in Windows systems.
What are the disadvantages of BOFs?
BOFs are single-file C programs that call Win32 APIs and limited Beacon APIs. Do not expect to link in other functionality or build large projects with this mechanism.
Core Impact does not link your BOF to a libc. This means you are limited to compiler intrinsics (e.g., __stosb on Visual Studio for memset), the exposed Agent internal APIs, Win32 APIs, and the functions that you write. Expect that a lot of common functions (e.g., strlen, stcmp, etc.) are not available to you via a BOF.
BOFs execute inside of your Agent. If a BOF crashes, you, or a friend you value will lose access. Write your BOFs carefully.
How do I develop a BOF?
Easy. Open up a text editor and start writing a C program. Here's a Hello World BOF:
#include <windows.h> #include "beacon.h" void go(char * args, int alen) { BeaconPrintf(CALLBACK_OUTPUT, "Hello World: %s", args); }
Download beacon.h.
To compile this with Visual Studio:
cl.exe /c /GS- hello.c /Fohello.o
To compile this with x86 MinGW:
i686-w64-mingw32-gcc -c hello.c -o hello.o
To compile this with x64 MinGW:
x86_64-w64-mingw32-gcc -c hello.c -o hello.o
The commands above produce a hello.o file. Use inline-execute in Beacon to run the BOF.
beacon> inline-execute /path/to/hello.o these are arguments
beacon.h contains definitions for several internal Beacon APIs. The function go is similar to main in any other C program. It's the function that's called by inline-execute and arguments are passed to it. BeaconOutput is an internal Beacon API to send output to the operator. Not much to it.
Dynamic Function Resolution
GetProcAddress, LoadLibraryA, GetModuleHandle, and FreeLibrary are available within BOF files. You have the option to use these to resolve Win32 APIs you wish to call. Another option is to use Dynamic Function Resolution (DFR).
Dynamic Function Resolution is a convention to declare and call Win32 APIs as LIBRARY$Function. This convention provides Beacon the information it needs to explicitly resolve the specific function and make it available to your BOF file before it runs. When this process fails, Cobalt Strike will refuse to execute the BOF and tell you which function it couldn't resolve.
Here's an example BOF that uses DFR and looks up the current domain:
#include <windows.h> #include <stdio.h> #include <dsgetdc.h> #include "beacon.h" DECLSPEC_IMPORT DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID); DECLSPEC_IMPORT DWORD WINAPI NETAPI32$NetApiBufferFree(LPVOID); void go(char * args, int alen) { DWORD dwRet; PDOMAIN_CONTROLLER_INFO pdcInfo; dwRet = NETAPI32$DsGetDcNameA(NULL, NULL, NULL, NULL, 0, &pdcInfo); if (ERROR_SUCCESS == dwRet) { BeaconPrintf(CALLBACK_OUTPUT, "%s", pdcInfo->DomainName); } NETAPI32$NetApiBufferFree(pdcInfo); }
The above code makes DFR calls to DsGetDcNameA and NetApiBufferFree from NETAPI32. When you declare function prototypes for Dynamic Function Resolution, pay close attention to the decorators attached to the function declaration. Keywords, such as WINAPI and DECLSPEC_IMPORT are important. These decorations provide the compiler with the needed hints to pass arguments and generate the right call instruction.
When this process fails, Core Impact will refuse to execute the BOF and tell you which function it couldn't resolve.
Running a BOF from Core Impact
To run BOFs in the context of an Impact agent we created a new module Run BOF. Select Modules > Post Exploitation > Run BOF to open the Parameter dialog.
Parameters
FILENAME - the compiled BOF file to execute.
ENTRYPOINT_NAME - entry point function of the BOF (go by default).
FORMATTER - string formatter of arguments the given BOF need to run ok. The following are supported:
Type | Description | Unpack With (C) |
---|---|---|
i | 4-byte integer | BeaconDataInt |
s | 2-byte short integer | BeaconDataShort |
z | zero-terminated+encoded string | BeaconDataExtract |
Z | zero-terminated wide-char string | (wchar_t *)BeaconDataExtract |
ARGUMENTS - paramters to pass to the BOF.
Python Modules and BOF
You will likely want to develop a Core Impact Module to run your finalized BOF implementations within Core Impact instead of using the generic Run BOF module. A BOF is a good place to implement a lateral movement technique, an escalation of privilege tool, or a new reconnaissance capability.
The beacon_inline_execute function is the entry point to run a BOF file. Here is a script to run a simple Hello World Program:
__xmldata__ = """
<entity class="module" type="python" name="Hello World BOF Runner">
<property type="container" key="products">
<property type="bool" key="impact">true</property>
</property>
<property type="container" key="features">
<property type="string" key="network"/>
</property>
<property type="string" key="category">Misc</property>
<property type="string" key="classname">HelloWorldBOFRunner</property>
<property type="bool" key="Local" readonly="1">true</property>
<!-- Parameters -->
<property type="container" key="highlight_preconditions" readonly="1">
<!-- Use agent entities as the module target -->
<property type="container" key="TargetClasses" readonly="1">
<property type="string" key="agent" readonly="1"/>
</property>
<!-- Run the module in Windows agents only -->
<property type="container" key="ValidTargets" readonly="1">
<property type="string" key="windows" readonly="1"/>
</property>
<!-- Do not run the module in localagent -->
<property type="container" key="TargetTypes" readonly="1">
<property type="string" key="level0v2" readonly="1"/>
</property>
</property>
<property type="string" key="author">Your name goes here</property>
<property type="string" key="brief">This module runs a Beacon Object File (a.k.a BOF) in the context of an Impact agent.</property>
<property type="string" key="category">Misc</property>
<property type="xmldata" key="Description" readonly="1">
<para>This module loads and executes a beacon object file in the source agent.</para>
<para>
<link>https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/beacon-object-files_main.htm</link>
</para>
</property>
<property type="string" key="version">$Revision: 220138 $</property>
</entity>"""
import impact.modulelib
import impact.config
import impact.entity
from os.path import dirname, exists, basename, join, normpath #, isfile, isdir
import os
from impact.bof import BeaconPack, beacon_inline_execute
class HelloWorldBOFRunner(impact.modulelib.ModuleBase):
def run(self):
params = self.getParameters()
host = impact.entity.get(impact.entity.t_agent().get_host())
arch = "x64" if host.get_arch() == "x86-64" else "x86"
# read in the right BOF file
helloWorldBOF_bof = join(impact.config.getUserPythonModulesPath(),"Hello",r"""Hello.%(Arch)s.o"""%{'Arch':arch})
with local_open(helloWorldBOF _bof, 'rb') as f:
raw_bof_content = f.read()
# pack our arguments
bof_pack = BeaconPack()
# FORMATTER -> zi
# DEFAULT VALUES -> 'Hello World' 1234
# Possible bof_pack functions: addshort(), addint(), addstr(), addWstr()
bof_pack.addstr('Hello World')
bof_pack.addint(1234)
# announce what we are doing
self.logMed("Running Hello BOF")
# execute it
output = beacon_inline_execute(raw_bof_content, bof_pack.getbuffer(), "demo")
# parse the output
self.logMed("BOF Standard Output was:")
self.logMed("{}".format(output[0]))
self.logMed("BOF Error Output was:")
self.logMed("{}".format(output[1]))
self.logMed("BOF File Outputs were:")
if (
output[2] is not None and
isinstance(output[2], list) and
len(output[2]) > 0
):
for fIdx, fOut in enumerate(output[2], start=0):
self.logMed("File output {}: [".format(fIdx))
self.logMed("File Id: {}".format(fOut.fileId))
oBuffer = fOut.buffer
oFName = fOut.filename
actualFileName = join(
impact.config.getBOFOutputFilesPath(),
basename(oFName)
)
self.logMed("File size:\n - (reported): {0}\n - (actual): {1}".format(fOut.fileSize, len(oBuffer)))
self.logMed("File Name\n - (requested): {0}\n - (actual): {1}".format(oFName, actualFileName))
with local_open(actualFileName, "wb") as f:
f.write(oBuffer)
self.logMed("] End of File output {};".format(fIdx))
The script first determines the architecture of the session. An x86 BOF will only run in an x86 Agent session. Conversely, an x64 BOF will only run in an x64 Agent session. This script then reads target BOF into a Python variable. The next step is to pack our arguments. The bof_pack variable packs arguments in a way that is compatible with Agent's internal data parser API and beacon_inline_execute runs the BOF with its arguments.
The beacon_inline_execute function accepts a string containing the BOF content as the first argument, the packed arguments as its second argument and the entry point as its third argument. The option to choose an entrypoint exists in case you choose to combine like-functionality into a single BOF.
Here is the C program that corresponds to the above script:
/*
* Compile with:
* x86_64-w64-mingw32-gcc -c hello.c -o hello.x64.o
* i686-w64-mingw32-gcc -c hello.c -o hello.x86.o
*/
#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include "beacon.h"
void demo(char * args, int length) {
datap parser;
char * str_arg;
int num_arg;
BeaconDataParse(&parser, args, length);
str_arg = BeaconDataExtract(&parser, NULL);
num_arg = BeaconDataInt(&parser);
BeaconPrintf(CALLBACK_OUTPUT, "Message is %s with %d arg", str_arg, num_arg);
}
The demo function is our entrypoint. We declare the datap structure on the stack. This is an empty and unintialized structure with state information for extracting arguments prepared with bof_pack. BeaconDataParse initializes our parser. BeaconDataExtract extracts a length-prefixed binary blob from our arguments. Our pack function has options to pack binary blobs as zero-terminated strings encoded to the session's default character set, a zero-terminated wide-character string, or a binary blob without transformation. The BeaconDataInt extracts an integer that was packed into our arguments. BeaconPrintf is one way to format output and make it available to the operator.