Time flies when you’re having fun! Can you believe it has been over two (2) years since the release of beacon object files (BOFs)? BOFs were released June 25, 2020, according to the release notes for Cobalt Strike. At that time, I wrote about what made BOFs special in terms of Cobalt Strike, as well as some of the ‘gotchas’ that might be hit when coding against them. Over these last two (2) years, the landscape in which BOFs exist has significantly changed. I feel that now would be a good time to reflect on BOFs and to opine on why they have become so prevalent.
BOF, Where Art Thou
In my opinion, BOFs are the manifestation of the closest thing to a native language plugin framework for implants that will exist. BOFs were preceded by C# assemblies, which were used to implement functionality once before using it in multiple locations. C# assemblies were predated by reflective DLLs. Each of these technologies sought to develop a technique once, and then use a variety of methods to deploy it.
A BOF is special, in that it greatly reduces the amount of data that needs to be sent to execute a given technique. This is because we only need to send the object file for our given task and nothing else. This differs from the previously mentioned techniques that would send entire executables, along with all the executables’ dependencies and initialization code.
A BOF is particularly special because some program is already running on the target computer. That program is what loads and executes the object file. Said program is not blind to what is happening, as was the case in C# assemblies and reflective DLLs. Rather, the program is intimately involved in the loading and linking of the object code. This presents an opportunity, in that the object file can now define and use functions that that loader (typically an implant) has implemented via internal functions. Now, we can not only execute a short burst of custom code but also provide custom functions to do things like pass messages back to an operator.
To date, BOFs have been adopted by several projects. In case you missed it, Kevin Haubris of TrustedSec posted both a Windows BOF runner and a POSIX BOF runner under the projects COFFLoader and ELFLoader, respectively. To back up my claim, you can now see BOFs used in:
…and probably others, but my point is made.
A few frameworks attempt to extend or replace the implant-provided functionality beyond what was originally released with Cobalt Strike. This is primarily seen in Brute Ratel’s documentation. This gives an advantage of greater customization, while having a disadvantage of potentially breaking existing compatibility if the previous names are not also supported.
BOF the Unstandardized Standard
The wonderful thing about a BOF is that, in theory, if you write a BOF, it is usable in all the frameworks that have a BOF runner. The terrible thing about BOF is that you may have no idea how that runner was implemented and what limitations it may have. Without that knowledge it is entirely possible that a BOF that works perfectly fine in a particular implant could cause crashing issues or return unexpected results in another.
A prime example of this is the handling of dynamic function resolution in Cobalt Strike compared to that which is implemented in COFFLoader. In the original 4.1 release of the BOF runner inside of Cobalt Strike, you were limited to 32 unique functions (increased to 64 in 4.7), using that method of resolution. For anything more, you had to use LoadLibrary and GetProcAddress to handle those additional functions. COFFLoader, our current opensource BOF runner implementation, does not impose such limitations.
BOF loaders also differ in the support for built-in functions. nanodump is a BOF put out by Forta that uses Cobalt Strike’s undocumented file download API via BeaconOutput calls. Running this same BOF in any of the other frameworks mentioned will likely not yield the intended results unless additional modifications have been made at the framework level to recognize these special messages and handle them accordingly.
Thus far, we have talked about how the object code itself is handled by the loader such that it can execute on a target. Often when executing these units of code, an operator may need to provide arguments so that the behavior can be changed without needing to recompile the object file. This is where the handling of BOF as a technology varies wildly between implementations. Cobalt Strike does the best job, in my opinion. It implements an entire programing language that can be utilized to handle creating logical defaults, doing logic checking, and allowing optional arguments or flags. This contrasts with many other implementations where you are forced to provide a format string and all arguments without an abstraction layer. For TrustedSec’s public COFFLoader and testing, you must use an entirely separate helper program to generate the binary string that gets provided to the executing program.
These differences can still be overcome. Sometimes it means moving logical defaults out of a cna script and into the object file. Other times, it just means giving more explicit help documentation, such as inputting 1 or 0 in a positional for ‘true/false’ rather than either providing or not providing a flag.
The Way of the BOF
Over this time period, many projects have come about that show the inherent power of being able to easily write custom code and execute it on target. Our own Situational Awareness and Remote Ops repositories have had many additions in the last two (2) years. Across GitHub, a search for ‘BOF’ now returns around 187 C-based repositories. While, of course, false positives are included in that number, it shows that a substantial number of people in the InfoSec community have experimented with BOF as a technology.
I do not believe the use-case for BOF has wildly changed. It is still best used for short-running tasks where information and execution can be completed in under a minute or so. Most implementations I have seen hijack the main communications thread, and the BOF must finish executing before the implant regains normal control flow again.
BOFs have not replaced techniques like execute-assembly or the use of reflective DLLs. Rather, BOFs have complemented these capabilities with a lighter-weight solution that can be employed for smaller tasks. BOFs have enabled many implant developers to get away from either coding all capabilities within an implant or using a fork and exec type of model to run extensions. For anyone looking into BOFs, it is important to realize this is not the end-all-be-all, but rather another tool in the toolkit.
If you are a developer who has been coding against BOFs over these last two (2) years, I hope you have found the technology as pleasant to work with as I have. If you are an operator who has benefited from BOF as a technique, I hope you have been able to appreciate what breaking the fork and exec model did for detection metrics. A co-worker and I used to frequently speak about how it would be cool if the red-teaming community could come together and develop a common plugin framework. With BOFs, we are finally as close to that dream as we will reasonably get.