The Why
Discussing why BOFs are commonly used, and the advantages they provide.
When a compiler turns your source into machine code, there are several steps it must perform. Once the lexing, parsing, and checking of a translation unit (a single source file, as well as all of its included headers) is finished, you're left with an object file, usually ending with ".o", or more commonly on Windows, ".obj". These files contain generated code, COFF sections, and a couple of other important things we'll get to in a moment, but under most circumstances, they can't be executed yet. This is because they haven't undergone the linking process, the final step in compilation, which combines all object files generated by your program and links them together into a final executable.
For the linking of an object file to finish successfully, all symbols within that file must be resolved. A symbol is essentially just a name that correlates with something in your program, which might be a variable, function, or imported function. This is the reason that header files and definitions exist in C and C++. They provide a convenient way to assert the fact that "there will be something here later".
As you can see, I've defined a function called "foo", and provided its return type and arguments, but I haven't provided any actual code that would tell the compiler what the foo function's body is, or in other words, the code for the function itself. In this scenario, "foo" would be considered an "external symbol", because the body of the function is defined in another translation unit somewhere, which the linker will need to resolve.
In the context of Beacon Object Files, this is important, because it allows Malware Developers to use the definitions of Beacon API functions while letting the C2 developer be the one who implements them. Take BeaconPrintf, for example. This function formats a string, similar to C's printf, and then returns that output to the operator. But the implementation of something like that will be completely different depending on the C2 in question, and different steps will have to be taken to return that output. The same can be said for a function like BeaconInjectProcess, which can be used to perform process injection. The techniques and tradecraft used to perform this operation will be drastically different across C2 agents. Some might use stealthier techniques, different functions to spawn the process itself, etcetera. Leaving the function as a definition allows a C2 developer to provide their own implementation of these API functions while allowing BOFs written for Cobalt Strike to still work for their C2.
This is also part of why BOFs are so convenient. Let's consider an alternative plugin system for C2 agents, Reflective DLLs. If you're unaware, these are DLLs that are loaded and executed entirely in memory, similar to BOFs. However, returning output from a reflective DLL ran by a C2 implant is significantly more challenging, because these DLLs write their output directly to stdout, rather than using something like BeaconPrintf. This typically means you'd need to spawn a child process, inject that DLL into the process, and use an IPC mechanism like a pipe to redirect the stdout into a buffer controlled by the implant, finally returning it to the C2 operator. This is a much more painstaking, and frankly more risky endeavor.
Last updated