A few years ago, a new feature was added to Cobalt Strike called “Beacon Object Files” (BOFs). These provide a way to extend a beacon agent post-exploitation with new features, perhaps to respond to conditions that you find after exploring an environment. Since then, the community has created many BOFs to cover many common scenarios, and we’ve been leveraging some of them to more closely emulate adversary actions on objectives.
GitHub: https://github.com/nettitude/RunOF
While doing this we’ve wanted to have a way to help us more easily debug and test our own BOFs, as well as use them across all the tooling we use. Therefore, we’re introducing RunOF – a tool that allows you to run BOFs outside of the Cobalt agent, as well as within PoshC2.
What is RunOF?
The aim of this project is to create a .NET application that is able to load arbitrary BOFs, pass arguments to them, execute them and collect and return any output. Additionally, the .NET application should be able to run within C2 frameworks, such as PoshC2.
The overall process is broadly similar to that used by the RunPE tool that we recently released, and so the RunOF tool uses some of the same techniques. The high-level process is as follows:
- Receive or open a BOF file to run
- Load it into memory
- Resolve any relocations that are present
- Set memory permissions correctly
- Locate the entry point for the BOF
- Execute in a new thread
- Retrieve any data output by the BOF
- Cleanup memory artifacts before exiting
How RunOF works
The first step in developing RunOF was to understand in detail what Beacon Object Files are. To do this, we looked at the publicly available documentation, and some of the example BOFs produced by the community.
A BOF contains an exported routine (typically a function called ‘go’ – but it can be anything you like), as well as calls to routines such as BeaconPrintf
to return data back to the agent. There is also a convention that allows access to the Windows API by calling a function of the form DLL_name$function_name – e.g. kernel32$VirtualAlloc
.
BOFs are, as the name suggests, “object” files, with some specifications for how symbols should be imported so the beacon loader can resolve them dynamically. An object file is something you are most likely to have encountered as an intermediary file when compiling code, typically with a .o
extension. When you are developing a C application for example, there are actually multiple steps happening – often abstracted by the Makefile or other build system that you are using. The first are preprocessing and compilation; these are taking the human-readable code, dealing with #defines and #includes before converting it into machine code that can be executed by processor. The second is linking: this step takes all the outputs of the previous step and resolves any references between them, before constructing an executable file that allows the operating system to load and execute the binary.
The object file is the output from the first preprocessing and compilation stage, so it contains unlinked relocatable machine code, along with debugging and other metadata. On Windows (which we’re targeting with RunOF) object files use the Common Object File Format (COFF) which Microsoft documents as part of the PE format (https://docs.microsoft.com/en-us/windows/win32/debug/pe-format).
A COFF file is made up of a collection of headers containing information about the file itself, symbol and string tables, and then a collection of sections that contain the code to be executed, data it needs and information on how to load that data into memory.
What each section is for is a little out of scope for this article, but the key ones we need to use are:
- .text: This contains the machine code to be executed.
- .data: Storage for initialized static variables.
- .bss: Storage for uninitialized static variables.
- .rdata: Storage for read-only initialized variables (e.g. constants).
- .reloc: Information on which bits of the file need to be updated when the load address is known.
As well as sections, an important part of the file we need to parse is the symbol table. This gives the location in the file of functions we have implemented, as well as functions we are expecting to import from other DLLs.
For example, in the screenshot above, we can see the go symbol is located in ‘SECT1’ (which is the .text section), whereas the symbols such as __imp_BeaconPrintf
are ‘UNDEF’ which means we need to provide them. Normally this would be done by the linker as part of the overall build process we outlined above, but we will have to do that step in our loader.
The loading process follows the following high level steps:
The most complex part of the process is probably resolving the relocation entries. When the code is compiled the compiler doesn’t know where in memory items (such as functions, variables or data) will be located when the application runs – the values could be in other object files, or need to be loaded from an Operating System API. Therefore, the compiler has a set of architecture specific-rules to choose from that allow it to specify that the address needs to be ‘filled in’ at linking time.
There is a small subset in the diagram above, the full list is quite large. Many appear to not be used in practice (and, for example, tools like Ghidra don’t support them) so we’ve only implemented the ones seen in the most common compilers. A relocation entry has, in effect, three fields – the symbol the relocation references, the address the relocation is to be applied to and the relocation type. As an example, the last one in the list (IMAGE_REL_AMD64_REL32
) means the loader has to find the address of the referenced symbol, calculate a 32-bit relative address from the relocation location to that symbol and write the value to the relocation address.
Once the relocations have been applied, memory permissions set correctly and the entry point located the BOF can be executed.
Getting it done with .NET
We wanted this to run in .NET to give us greater flexibility in how we use it as part of our other C2 tooling. This poses a challenge, since .NET is an interpreted language and so the code we write will be running inside the Common Language Runtime (CLR). Fortunately, .NET provides functionality for working with unmanaged code called Interop that allows us to manipulate native memory to load the BOF and then call a native Windows API function to execute it. We use the same technique as developed for RunPE of launching the code in a new thread, and we install an exception handler to prevent any buggy BOFs from crashing the entire process.
Another challenge we faced was in getting any output produced by the BOF back to the .NET parent application so it could be returned down a C2 channel. The Cobalt agent defined a set of Beacon*
functions (e.g. BeaconPrintf
) that the BOF can call to pass data back to the implant. These need to be implemented as native code for the BOF to be able to call them, and we need to have a way of passing the data they produce between the native code and the .NET parent. To implement this, we have a small ‘beacon_functions’ COFF file that is loaded by the .NET loader first. This contains implementations of the Beacon*
functions that write their output into a buffer that is grown to contain the data output by the BOF. When the actual BOF is loaded the addresses of the already loaded Beacon*
functions can then be provided during the symbol resolution step. Once BOF execution completes the .NET parent can read from the memory buffer to retrieve any output generated.
The final piece of the puzzle is how we provide arguments to the BOF file. In Cobalt, BOFs are loaded with an ‘aggressor’ script that allows you to pass arguments of differing types to the BOF file, where they are retrieved by using the data API defined in beacon.h
:
To allow BOFs to accept arguments in RunOF we have to accept them on the command line of our application, then provide them in a way that can be consumed by the native code once it is loaded. To do this, we serialize them into a shared memory buffer using a custom type, length, value (TLV) format. Our internal implementation of the data API can then read from that buffer when invoked by the BOF:
There are two important caveats to this approach:
- The arguments must be provided on the command line in the order the BOF is expecting to receive them. You can get this from the aggressor script used to load the BOF, or from looking at the BOF code.
- The arguments must be provided with the correct type (e.g. Int/Short etc.). Again, this can usually be seen from the aggressor script. In some cases, the aggressor script may itself do some parsing (e.g. converting a DNS lookup type such as A or AAAA into a numeric code for the BOF’s internal use) – in which case you have to provide the internal code.
You can see a lot more detail on this in the project README, and the command line help offers a summary:
Debug Capability
As well as running BOFs, the RunOF project can also be used to help develop new BOF capability. The project files contain a ‘Debug’ build target – if this is used then the loader will pause before executing the BOF to allow a debugger to be attached. You’ll also get lots of information about the loading process itself.
Conclusion
We hope that RunOF gives Red Teamers a way to use existing BOF functionality in other C2 frameworks, and to help develop new and innovative BOF capability. The RunOF project can be found at the link below.