Contents

Boflink: A Linker For Beacon Object Files

Intro

This is a blog post written for a project I recently released. The source code for it can be found here on Github.

Background

The design of Cobalt Strike’s Beacon Object Files is rather unique when compared to other runtime code execution implementations. These are small programs compiled into COFF object files which are loaded and executed by a COFF loader. Another addition that Beacon Object Files make is the concept of dynamic function resolution or DFR which allows the COFF to invoke functions from external DLLs.

The popularity of Beacon Object Files has exploded since they were first introduced in Cobalt Strike. Due to their popularity, many projects have been released to take advantage of this new technique. TrustedSec released a blog post on developing a Beacon Object File loader shortly after the initial concept was introduced. The blog post and COFFLoader implementation made it easier for developers to both develop their own BOFs and also include this capability into their own tools. BOF loaders have become widely adopted by many different red team tools. They can provide a fairly lightweight extension system that is agnostic to the underlying platform which makes BOFs ideal from a developer standpoint.

Although the concept behind BOFs was first introduced back in 2020, the development process and tooling has largely stayed the same. There has been an increase in public project templates for BOF development, such as the bof-vs template, which can help with some development aspects.

These templates still suffer some of the same limitations that come with the nature of Beacon Object Files.

Current Beacon Object File Limitations

One of the limitations with BOFs is the way it handles external imports. It uses a method known as DFR which consists of changing the names of imported symbols to include the name of the library the symbol is from. I wrote a previous blog post on the complications that come with this: “Writing Beacon Object Files Without DFR”.

Another design limitation is that BOFs rely on using object files as a “final file format”. The main purpose of object files is to act as an intermediary compilation artifact during the build process rather than a complete file suitable for other uses. This makes the raw object files produced by compilers optimized more for later use by a system linker for building a complete executable file.

Compilers may emit various symbols and other compilation artifacts inside the object file which are useful to the linker but may cause issues if not handled properly by a loader. This results in the loader requiring extra logic to handle these cases in order to make a decision on how to handle certain artifacts depending on the context. If these are not handled properly, it may cause issues where the compiler generated code assumes that the artifact was processed correctly.

Object files are also the product of compiling a single translation unit which typically means a single C or C++ source file. The source can be split up between multiple files but ultimately, these files will have to be combined either through #include statements or by concatenating their contents in order for the compiler to compile it. This can potentially make things difficult to maintain or debug.

Boflink is a tool designed to act as a sort of fill-in for the missing linking stage that comes with the BOF development process. It is a linker that takes unmodified object files generated by a compiler and links them together into a Beacon Object File capable of being loaded by a BOF loader.

Its main goal is to act as a bridge between the BOF development and the BOF loading process to help simplify them.

On the BOF development side, Boflink will perform symbol resolution on imported symbols and rewrite them to match the BOF DFR format. BOF developers can use symbols imported from DLLs normally without needing to first prefix them with the library name they are imported from. Another feature is that it is capable of linking multiple COFFs together. These aspects help make the source code for BOFs more similar to the source code for traditional executables since the BOF specific artifacts are handled by Boflink.

On the BOF loader side, Boflink will create COFFs that are better optimized for BOF loaders to process rather than system linkers. Compilers will oftentimes include artifacts and structure things in ways that are more ideal for a linker to process rather than a loader. Some examples of this include COMDAT sections, relocation labels, grouped sections, COMMON symbols and linker metadata sections. Simplifying the COFFs loaded by a BOF loader can help reduce the risk of a BOF crashing since the loader will not need to include additional complexities for handling these cases.

Setup and Usage

Pre-built artifacts are provided on the releases page for download. These are built in CI and published for each release.

Setting up Boflink on Windows only requires the boflink executable. This executable can be placed in one of the %PATH% directories so that it is easier to run.

On Linux, Boflink is designed to integrate with the MinGW toolchain and invoked via a compile driver. It is possible to use it with a custom toolchain; however, the MinGW one provided by many Linux distributions is the easiest to get started with.

A special symlink needs to be created for integrating Boflink with MinGW GCC. The reasoning for this is because GCC does not support using a custom linker directly. This symlink is used to get around that limitation. This workaround was used by the mold linker when it did not have GCC support initially. A symlink needs to be created in an empty directory with the symlink name being ld and the target being the path to the installed boflink executable

The release archives include a small install script which will install the executable to ~/.local/bin/boflink and create the symlink at ~/.local/libexec/boflink/ld.

Install From Source

Installing from source requires Rust version >=1.85. This can be checked by running rustc --version in a terminal window.

Building and installing from source requires downloading the source code and then running cargo xtask install.

The cargo xtask ... command is not any form of external plugin that needs to be installed but is an alias for cargo to build and run a small executable for installing the program correctly.

It will run cargo install –path . –bin boflink to build the executable from source and copy it into the $CARGO_HOME/bin path. On Linux, it also sets up the ld symlink for MinGW GCC by placing it in the ~/.local/libexec/boflink directory.

The cargo xtask uninstall command can be used to undo everything from the install command. This only works as a means of uninstalling the program if it was installed using cargo xtask install and will not uninstall the executable if it was installed from a release archive.

Usage

Boflink acts as a sort of “dumb linker”. This means that it does not include any form of default configuration and expects it to be provided from the command line or by other means.

Windows

On Windows, Boflink should be used while inside a Visual Studio developer console. This environment includes a list of search paths that will be automatically included when searching for link libraries.

The boflink executable can be invoked directly in a Visual Studio developer console passing in any command line options.

boflink [-o <output>] [options] <files>...
boflink -o mybof.bof file1.obj file2.obj -lkernel32 -ladvapi32

Linux

On Linux, the boflink executable is not meant to be invoked directly and instead should be used through a compile driver such as MinGW GCC or Clang. The compile driver will then invoke boflink passing in its default configuration for boflink to use.

For MinGW GCC, this requires the following command line flags.

x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles <args>...
x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles -o mybof.bof source.c object.o

The example above assumes that the ~/.local/libexec/boflink directory has the ld symlink set up as mentioned in the setup section. The -fno-lto and -nostartfiles flags are also required for it to work properly.

Clang includes a --ld-path= flag for setting the linker. This needs to be the absolute path to the boflink executable.

clang --ld-path=/path/to/boflink --target=x86_64-windows-gnu -nostartfiles <args>...
clang --ld-path=/path/to/boflink --target=x86_64-windows-gnu -nostartfiles -o mybof.bof source.c object.o

The --ld-path= flag is very particular on its formatting and requires two hyphens for the flag prefix and an equal = sign. The -nostartfiles option is also required like with MinGW GCC to exclude startup files from the linker inputs.

Features

These are some of the currently implemented features of Boflink and how to use them.

Symbol Resolution

Boflink will resolve undefined symbols using the link libraries and library search paths specified on the command line. In the output BOF, the undefined symbols will have the resolved library names included in the symbol name to match the BOF DFR format.

For this example source file.

#include <windows.h>

#include "beacon.h"


void go(void) {
    DWORD pid = GetCurrentProcessId();
    BeaconPrintf(CALLBACK_OUTPUT, "pid: %lu\n", pid);
}

Compiling it will cause the GetCurrentProcessId and BeaconPrintf symbols to show up as being undefined.

matt@laptop ~> ls
beacon.h  example.c  Makefile
matt@laptop ~> make example.o
x86_64-w64-mingw32-gcc    -c -o example.o example.c
matt@laptop ~> rabin2 -s example.o
[Symbols]
nth paddr      vaddr      bind   type size lib name                          demangled
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000000 0x00000000 LOCAL  FILE 4        .file
0   0x0000012c 0x00000000 GLOBAL FUNC 4        go
0   0x0000012c 0x00000000 LOCAL  SECT 4        .text
0   0x00000000 0x00000040 LOCAL  SECT 4        .data
0   0x00000000 0x00000050 LOCAL  SECT 4        .bss
0   0x0000016c 0x00000060 LOCAL  SECT 4        .rdata
0   0x0000017c 0x00000070 LOCAL  SECT 4        .xdata
0   0x00000188 0x00000080 LOCAL  SECT 4        .pdata
0   0x00000194 0x00000090 LOCAL  UNK  4        .rdata$zzz
0   ---------- ---------- NONE   UNK  4        imp.__imp_GetCurrentProcessId
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
matt@laptop ~>

Passing the built example.o COFF through Boflink will result in Boflink resolving these symbols and rewriting them in the BOF DFR format with the name of the DLL that Boflink finds them defined in.

matt@laptop ~> ls
beacon.h  example.c  example.o  Makefile
matt@laptop ~> x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles -o example.bof example.o
matt@laptop ~> rabin2 -s example.bof
[Symbols]
nth paddr      vaddr      bind   type size lib name                                   demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000104 0x00000000 LOCAL  SECT 4        .text
0   0x00000104 0x00000000 GLOBAL FUNC 4        go
0   0x00000000 0x00000040 LOCAL  SECT 4        .data
0   0x00000000 0x00000050 LOCAL  SECT 4        .bss
0   0x00000144 0x00000060 LOCAL  SECT 4        .rdata
0   0x00000194 0x000000b0 LOCAL  SECT 4        .xdata
0   0x000001a0 0x000000c0 LOCAL  SECT 4        .pdata
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
0   ---------- ---------- NONE   UNK  4        imp.__imp_KERNEL32$GetCurrentProcessId
matt@laptop ~>

MinGW GCC includes kernel32 as a search library in its default configuration which is why it does not need to be included manually. In order to link against kernel32 on Windows using MSVC, Boflink needs to be run in a Visual Studio developer console with kernel32 passed as an additional link library on the command line.

 matth@laptop ~> ls

        Directory: C:\Users\matth


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962  beacon.h
-a---         5/27/2025   1:50 PM            162  example.c

 matth@laptop ~> cl /c /GS- .\example.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35207.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

example.c
 matth@laptop ~> ls

        Directory: C:\Users\matth


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962 beacon.h
-a---         5/27/2025   1:50 PM            162 example.c
-a---         5/27/2025   1:52 PM           1225 example.obj

 matth@laptop ~> rabin2 -s .\example.obj
[Symbols]
nth paddr      vaddr      bind   type size lib name                          demangled
--------------------------------------------------------------------------------------
0   ---------- ---------- LOCAL  ABS  4        @comp.id-0x01048987
0   ---------- ---------- LOCAL  ABS  4        @feat.00-0x80010090
0   ---------- ---------- LOCAL  ABS  4        @vol.md-0x00000003
0   0x0000012c 0x00000000 LOCAL  SECT 4        .drectve
0   0x00000189 0x00000060 LOCAL  SECT 4        .debug$S
0   0x00000219 0x000000f0 LOCAL  SECT 4        .text$mn
0   ---------- ---------- NONE   UNK  4        imp.__imp_GetCurrentProcessId
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
0   0x00000219 0x000000f0 GLOBAL FUNC 4        go
0   0x00000219 0x000000f0 LOCAL  6    4        $LN3
0   0x0000025f 0x00000120 LOCAL  SECT 4        .xdata
0   0x0000025f 0x00000120 LOCAL  UNK  4        $unwind$go
0   0x00000267 0x00000130 LOCAL  SECT 4        .pdata
0   0x00000267 0x00000130 LOCAL  UNK  4        $pdata$go
0   0x00000291 0x00000140 LOCAL  SECT 4        .data
0   0x00000291 0x00000140 LOCAL  UNK  4        $SG74138
0   0x0000029b 0x00000150 LOCAL  SECT 4        .chks64
 matth@laptop ~> boflink -o example.bof .\example.obj -lkernel32
 matth@laptop ~> rabin2 -s .\example.bof
[Symbols]
nth paddr      vaddr      bind   type size lib name                                   demangled
-----------------------------------------------------------------------------------------------
0   0x000000b4 0x00000000 LOCAL  SECT 4        .text
0   0x000000b4 0x00000000 GLOBAL FUNC 4        go
0   0x000000dc 0x00000030 LOCAL  SECT 4        .xdata
0   0x000000dc 0x00000030 LOCAL  UNK  4        $unwind$go
0   0x000000e4 0x00000040 LOCAL  SECT 4        .pdata
0   0x000000e4 0x00000040 LOCAL  UNK  4        $pdata$go
0   0x000000f0 0x00000050 LOCAL  SECT 4        .data
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
0   ---------- ---------- NONE   UNK  4        imp.__imp_KERNEL32$GetCurrentProcessId
 matth@laptop  ~>

If any symbols could not be resolved, Boflink will return an error listing out what symbols were left undefined.

#include <windows.h>

#include <lmcons.h>

#include "beacon.h"


extern void UnresolvedSymbol(void);


static void my_function(void) {
    UnresolvedSymbol();
}

void go(void) {
    my_function();

    char username[UNLEN + 1] = {0};
    if (GetUserNameA(username, &(DWORD){sizeof(username)}) != 0) {
        BeaconPrintf(CALLBACK_OUTPUT, "Your username is %s", username);
    }
}
 matth@laptop ~> cl /c /GS- .\example.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35207.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

example.c
 matth@laptop ~> boflink -o example.bof .\example.obj -lkernel32
boflink: error: undefined symbol: __declspec(dllimport) GetUserNameA
>>> referenced by .\example.obj:(go)

boflink: error: undefined symbol: UnresolvedSymbol
>>> referenced by .\example.obj:(my_function)
 matth@laptop ~ [1]>

Linking Multiple Files

Boflink is also capable of linking multiple object files together into a single BOF. This is commonly referred to as “partial linking” where multiple object files are combined to produce a single object file rather than a full executable or shared library.

go.c

#include "beacon.h"

#include "other.h"


void go(void) {
    BeaconPrintf(CALLBACK_OUTPUT, "Hello world from the go() function");
    other_function();
}

other.h

#ifndef OTHER_H
#define OTHER_H


void other_function(void);


#endif // OTHER_H

other.c

#include "other.h"

#include "beacon.h"


void other_function() {
    BeaconPrintf(CALLBACK_OUTPUT, "Hello world from other_function()");
}

Using MinGW GCC

matt@laptop ~> ls
beacon.h  go.c  Makefile  other.c  other.h
matt@laptop ~> x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles -o mybof.bof go.c other.c
matt@laptop ~> ls
beacon.h  go.c  Makefile  mybof.bof  other.c  other.h
matt@laptop ~> rabin2 -s mybof.bof
[Symbols]
nth paddr      vaddr      bind   type size lib name                                   demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000104 0x00000000 LOCAL  SECT 4        .text
0   0x00000104 0x00000000 GLOBAL FUNC 4        go
0   0x00000134 0x00000030 GLOBAL FUNC 4        other_function
0   0x00000000 0x00000090 LOCAL  SECT 4        .data
0   0x00000000 0x000000a0 LOCAL  SECT 4        .bss
0   0x00000194 0x000000b0 LOCAL  SECT 4        .rdata
0   0x00000244 0x00000160 LOCAL  SECT 4        .xdata
0   0x0000025c 0x00000180 LOCAL  SECT 4        .pdata
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
0   ---------- ---------- NONE   UNK  4        imp.__imp_KERNEL32$GetCurrentProcessId
matt@laptop ~>

Using MSVC on Windows

matth@laptop ~> ls

        Directory: C:\Users\matth
 

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962 beacon.h
-a---         5/27/2025   2:33 PM            164 go.c
-a---         5/27/2025   2:33 PM            273 other.c
-a---         5/27/2025   2:33 PM             85 other.h
 
 matth@laptop ~> cl /c /GS- .\go.c .\other.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35207.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

go.c
other.c
Generating Code...
 matth@laptop ~> ls

        Directory: C:\Users\matth


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962 beacon.h
-a---         5/27/2025   2:33 PM            164 go.c
-a---         5/27/2025   2:35 PM           1225 go.obj
-a---         5/27/2025   2:33 PM            273 other.c
-a---         5/27/2025   2:33 PM             85 other.h
-a---         5/27/2025   2:35 PM           1364 other.obj

 matth@laptop ~> boflink -o mybof.bof .\go.obj .\other.obj -lkernel32
 matth@laptop ~> ls

        Directory: C:\Users\matth
 

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962 beacon.h
-a---         5/27/2025   2:33 PM            164 go.c
-a---         5/27/2025   2:35 PM           1225 go.obj
-a---         5/27/2025   2:35 PM            947 mybof.bof
-a---         5/27/2025   2:33 PM            273 other.c
-a---         5/27/2025   2:33 PM             85 other.h
-a---         5/27/2025   2:35 PM           1364 other.obj

 matth@laptop ~> rabin2 -s .\mybof.bof
[Symbols]
nth paddr      vaddr      bind   type size lib name                                   demangled
-----------------------------------------------------------------------------------------------
0   0x000000b4 0x00000000 LOCAL  SECT 4        .text
0   0x000000b4 0x00000000 GLOBAL FUNC 4        go
0   0x000000d4 0x00000020 GLOBAL FUNC 4        other_function
0   0x0000010c 0x00000060 LOCAL  SECT 4        .xdata
0   0x0000010c 0x00000060 LOCAL  UNK  4        $unwind$go
0   0x00000114 0x00000068 LOCAL  UNK  4        $unwind$other_function
0   0x0000011c 0x00000070 LOCAL  SECT 4        .pdata
0   0x0000011c 0x00000070 LOCAL  UNK  4        $pdata$go
0   0x00000128 0x0000007c LOCAL  UNK  4        $pdata$other_function
0   0x00000134 0x00000090 LOCAL  SECT 4        .data
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
0   ---------- ---------- NONE   UNK  4        imp.__imp_KERNEL32$GetCurrentProcessId
 matth@laptop ~>

Partial linking is a feature also supported in GNU ld. The difference between partial linking in Boflink and GNU ld is that GNU ld’s partial linking is mainly for doing incremental linking in a larger build process.

Incremental linking is an optimization feature used to help speed up rebuilds for a program. This works by linking multiple object files together to produce a new “pre-linked” object file for the final linking stage.

It is pretty common for linkers to embed additional metadata in the “pre-linked” object file during incremental linking to help speed up the final linking process.

Here is an example of using MinGW ld to perform partial linking with the files above.

matt@laptop ~> x86_64-w64-mingw32-gcc -r -o combined.o go.c other.c
matt@laptop ~> ls
beacon.h  combined.o  go.c  Makefile  other.c  other.h
matt@laptop ~> rabin2 -s combined.o
[Symbols]
nth paddr      vaddr      bind   type size lib name                          demangled
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000000 0x00000000 LOCAL  FILE 4        .file
0   0x0000012c 0x00000000 GLOBAL FUNC 4        go
0   0x0000012c 0x00000000 LOCAL  SECT 4        .text
0   0x00000000 0x00000090 LOCAL  SECT 4        .data
0   0x00000000 0x000001d0 LOCAL  SECT 4        .bss
0   0x000001bc 0x000000a0 LOCAL  SECT 4        .rdata
0   0x000002c4 0x000001b0 LOCAL  SECT 4        .xdata
0   0x000002ac 0x00000190 LOCAL  SECT 4        .pdata
0   0x0000026c 0x00000150 LOCAL  UNK  4        .rdata$zzz
0   0x00000000 0x00000000 LOCAL  FILE 4        .file
0   0x0000015c 0x00000030 GLOBAL FUNC 4        other_function
0   0x0000015c 0x00000030 LOCAL  SECT 4        .text
0   0x00000000 0x00000090 LOCAL  SECT 4        .data
0   0x00000000 0x000001d0 LOCAL  SECT 4        .bss
0   0x000001ec 0x000000d0 LOCAL  SECT 4        .rdata
0   0x000002d0 0x000001bc LOCAL  SECT 4        .xdata
0   0x000002b8 0x0000019c LOCAL  SECT 4        .pdata
0   0x0000022c 0x00000110 LOCAL  UNK  4        .rdata$zzz
0   ---------- ---------- NONE   UNK  4        imp.__imp_GetCurrentProcessId
0   ---------- ---------- NONE   UNK  4        imp._pei386_runtime_relocator
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
matt@laptop ~>

You can see above that MinGW ld does not deduplicate the section symbols. It will also embed the undefined _pei386_runttime_relocator symbol because it knows that it will need it later on in the final linking process. This is helpful for further linking together a full program but can cause issues in a BOF loader.

Boflink’s partial linking process includes various linker passes specifically designed for tailoring the COFF for a loader to process. This includes: deduplicating section symbols, discarding linker metadata sections, merging grouped sections, applying relocations to same-section defined symbols, allocating space for COMMON symbols, handling COMDAT sections and some others. The result of this is that the final output COFF is much simpler than what MinGW ld produces which makes it easier for BOF loaders to load.

When using MSVC toolchains, it is common for them to embed additional metadata inside the compiled COFF for the linker to process. These are known as comment records and are added using the #pragma comment preprocessor directive. Some of these comment records include additional options for the linker to process.

Boflink currently supports processing lib records for including additional search libraries when resolving symbols.

#include <windows.h>

#include <lmcons.h>

#include "beacon.h"


#pragma comment(lib, "kernel32")
#pragma comment(lib, "advapi32")


void go(void) {
    DWORD pid = GetCurrentProcessId();
    BeaconPrintf(CALLBACK_OUTPUT, "Current process id is %lu", pid);

    char username[UNLEN + 1] = {0};
    if (GetUserNameA(username, &(DWORD){sizeof(username)}) != 0) {
        BeaconPrintf(CALLBACK_OUTPUT, "Your username is %s", username);
    }
}

When processing this file, Boflink will see the “kernel32” and “advapi32” link libraries and include them in the symbol resolution phase.

matth@laptop ~> ls

        Directory: C:\Users\matth


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962 beacon.h
-a---         5/27/2025   3:33 PM            457 example.c

 matth@laptop ~> cl /c /GS- .\example.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35207.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

example.c
 matth@laptop ~> ls

        Directory: C:\Users\matth


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a---         5/27/2025   1:50 PM          15962 beacon.h
-a---         5/27/2025   3:33 PM            457 example.c
-a---         5/27/2025   3:36 PM           1476 example.obj

 matth@laptop ~> boflink -o example.bof .\example.obj
 matth@laptop ~> rabin2 -s .\example.bof
[Symbols]
nth paddr      vaddr      bind   type size lib name                                   demangled
-----------------------------------------------------------------------------------------------
0   0x000000b4 0x00000000 LOCAL  SECT 4        .text
0   0x000000b4 0x00000000 GLOBAL FUNC 4        go
0   0x00000128 0x00000080 LOCAL  SECT 4        .xdata
0   0x00000128 0x00000080 LOCAL  UNK  4        $unwind$go
0   0x00000134 0x00000090 LOCAL  SECT 4        .pdata
0   0x00000134 0x00000090 LOCAL  UNK  4        $pdata$go
0   0x00000140 0x000000a0 LOCAL  SECT 4        .data
0   ---------- ---------- NONE   UNK  4        imp.__imp_BeaconPrintf
0   ---------- ---------- NONE   UNK  4        imp.__imp_KERNEL32$GetCurrentProcessId
0   ---------- ---------- NONE   UNK  4        imp.__imp_ADVAPI32$GetUserNameA
 matth@laptop ~>

Note that this method of passing link libraries to Boflink depends on if the compiler used to compile the source files supports embedding comment records.

BSS Handling

A common limitation with many BOF loaders is that they do not support loading the .bss section. Boflink includes a --merge-bss flag to add .bss compatibility for BOF loaders that do not support it. This will zero-initialize .bss defined symbols and place them at the end of the .data section. It effectively does what many BOF developers will do in their code where they default initialize global variables or use attributes to have the compiler define them in the .data section. It does not “magically fix” the .bss section for BOF loaders but instead offloads the task of zero-initializing them to Boflink.

I am curious what the reasonings are for many BOF loaders not adding support for loading the .bss section. From the BOF loader perspective, an uninitialized data section and a file-backed section are loaded very similarly. The only difference is that the uninitialized data section will set the IMAGE_SCN_CNT_UNINITIALIZED_DATA flag in the section header characteristics to indicate that the section is not file-backed. If this flag is present, the loader does not initialize the section with data from the file but instead zero-initializes it. The size needed for the allocated section is stored in the SizeOfRawData field like with regular file-backed sections. Adding support for the .bss section in a BOF loader involves checking for that flag in the section header and either zero-initializing the allocated section or initializing it with the data found at the PointerToRawData file offset in the header.

TrustedSec’s COFFLoader will zero-initialize any sections which have a non-zero size and a PointerToRawData field set to NULL. This means that it does have support for loading the .bss section since the .bss section’s PointerToRawData field will be set to NULL and the SizeOfRawData field will be non-zero.

bsstest.c

#include <windows.h>

#include "beacon.h"


static int bss_variable;
static float other_bss_variable;


void go(void) {
    bss_variable = 123;
    BeaconPrintf(CALLBACK_OUTPUT, "bss_variable: %d\n", bss_variable);

    other_bss_variable = 3.14;
    BeaconPrintf(CALLBACK_OUTPUT, "other_bss_variable: %0.2f\n", other_bss_variable);
}
matt@laptop ~/COFFLoader (main)> x86_64-w64-mingw32-gcc -c -o bsstest.o bsstest.c
matt@laptop ~/COFFLoader (main)> ./COFFLoader64.exe go bsstest.o
Got contents of COFF file
Running/Parsing the COFF file
bss_variable: 123
other_bss_variable: 3.14
Ran/parsed the coff
Outdata Below:

bss_variable: 123
other_bss_variable: 3.14

matt@laptop ~/COFFLoader (main)>

What may cause confusion with the .bss section are the differences between .bss defined symbols and COMMON symbols. MaskRay’s blog post “All about COMMON symbols“ does a great job explaining them and why they can cause many issues in C.

GCC 10 and Clang 11 (released around 2020) included a change where the -fno-common flag is now added by default during compilation. This will cause public uninitialized global variables to be treated as .bss defined symbols rather than COMMON symbols.

Here is an example of what COMMON symbols look like and how they differ from .bss defined symbols.

commonstest.c

#include <windows.h>

#include "beacon.h"


int common_symbol;
static int bss_symbol;


void go(void) {
    common_symbol = 123;
    BeaconPrintf(CALLBACK_OUTPUT, "common_symbol: %d\n", common_symbol);

    bss_symbol = 123;
    BeaconPrintf(CALLBACK_OUTPUT, "bss_symbol: %d\n", bss_symbol);
}

The common_symbol is a public uninitialized global variable. This is what causes COMMON symbols to appear in a compiled object file.

The -fcommon compile flag is needed when using MinGW GCC to treat common_symbol as a COMMON symbol since the -fno-common flag is included by default. The bss_symbol variable is a regular .bss defined symbol since it was declared with static storage duration. These symbols can be viewed in the COFF symbol table using llvm-readobj.

matt@laptop ~> ls
beacon.h  commonstest.c
matt@laptop ~> x86_64-w64-mingw32-gcc -fcommon -c -o commonstest.o commonstest.c
matt@laptop ~> llvm-readobj -s commonstest.o
 Symbol {
    Name: bss_symbol
    Value: 0
    Section: .bss (3)
    BaseType: Null (0x0)
    ComplexType: Null (0x0)
    StorageClass: Static (0x3)
    AuxSymbolCount: 0
  }
  Symbol {
    Name: common_symbol
    Value: 4
    Section: IMAGE_SYM_UNDEFINED (0)
    BaseType: Null (0x0)
    ComplexType: Null (0x0)
    StorageClass: External (0x2)
    AuxSymbolCount: 0
  }

The bss_symbol shows up as being defined in the .bss section at address 0. The common_symbol is rather different. COMMON symbols in COFFs are represented as having a section number of IMAGE_SYM_UNDEFINED and a Value that is non-zero. This is very similar to regular undefined symbols but with the only difference being that the Value field is non-zero for COMMONS. The system linker will handle resolving COMMON symbols as outlined in MaskRay’s blog post mentioned above but they ultimately will not end up defined in the .bss section until after linking.

MSVC’s defaults are different than MinGW GCC or Clang and will compile public uninitialized global variables as COMMON symbols. This is equivalent to passing the -fcommon flag like in the example above.

One way to avoid COMMON symbols when using MSVC is by making uninitialized global variables static. The drawback with this is that the symbol will be local to the translation unit it is being compiled in and cannot be referenced externally.

As far as I have discovered, the only way to create a public .bss defined symbol with MSVC is by wrapping it in a bss_seg pragma.

#include <windows.h>

#include "beacon.h"


#pragma bss_seg(push, bss, ".bss")
int bss_symbol;
#pragma bss_seg(pop, bss)


void go(void) {
    bss_symbol = 123;
    BeaconPrintf(CALLBACK_OUTPUT, "bss_symbol: %d\n", bss_symbol);
}

I can see why the confusion around COMMON symbols is the reason why BOF loaders decided to not support BOFs with uninitialized global variables. MinGW GCC and Clang changed their defaults for them after the initial BOF loaders were released which would cause COMMON symbols to show up more frequently during testing.

There is not a lot of easily accessible information online explaining COMMON symbols in-depth which makes them rather obscure. Many C development courses do not mention them at all because they are a compiler-specific “feature” not really part of the language itself. There are many pitfalls when using COMMON symbols in C and exist mainly for FORTRAN compatibility.

BOF loaders should be able to support .bss defined symbols fairly easily. As stated above, TrustedSec’s COFFLoader does support them. I personally do not expect BOF loaders to handle COMMON symbols due to the complexity but they can be avoided pretty easily in the BOF source code.

Boflink does support handling COMMON symbols like a regular linker does and will allocate them at the end of the .bss section if they do appear.

Custom Beacon API

Beacon Object Files can utilize what is known as a “Beacon API” or a set of API functions the BOF loader exposes for the BOF to interact with the loader itself.

Cobalt Strike’s Beacon API is included by default but developers can utilize a custom Beacon API instead of the default Cobalt Strike one.

The custom Beacon API is an import library that is passed in using the --custom-api command line flag.

Here is an example on how to use a custom Beacon API with Boflink.

The custom API for this example includes four functions: MyApiVersion, MyApiPrintf, MyApiAlloc and MyApiFree. A Module-Definition File can be used for creating an import library that exports these symbols.

myapi.def

LIBRARY MyApi
EXPORTS
  MyApiVersion
  MyApiPrintf
  MyApiAlloc
  MyApiFree

Using llvm-dlltool on Linux

llvm-dlltool -l libmyapi.a -d myapi.def

Using lib.exe on Windows

lib /machine:x64 /def:myapi.def /out:myapi.lib

Using MinGW dlltool

x86_64-w64-mingw32-dlltool -l libmyapi.a -d myapi.def

It is recommended to use llvm-dlltool over MinGW dlltool. The reason for this is that the import libraries that llvm-dlltool and lib.exe create are better optimized when compared to MinGW dlltool.

Here is the example source code.

myapi.h

#ifndef MYAPI_H
#define MYAPI_H

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus


#include <stdint.h>

__declspec(dllimport) int MyApiVersion(void);
__declspec(dllimport) void MyApiPrintf(const char *format, ...);
__declspec(dllimport) void *MyApiAlloc(size_t size);
__declspec(dllimport) void MyApiFree(void *ptr);


#ifdef __cplusplus
};
#endif // __cplusplus

#endif // MYAPI_H

custom-api.c

#include "myapi.h"


void go(void) {
    int version = MyApiVersion();
    MyApiPrintf("MyApiVersion: %d", version);

    int *value = MyApiAlloc(sizeof(int));
    *value = 123;
    MyApiPrintf("value: %d", *value);

    MyApiFree(value);
}

The BOF can be compiled and linked with the import library that contains the API symbols.

MinGW GCC

# llvm-dlltool is preferred over x86_64-w64-mingw32-dlltool
llvm-dlltool -l libmyapi.a -d myapi.def

# x86_64-w64-mingw32-dlltool is supported but should be used only if llvm-dlltool is not available
x86_64-w64-mingw32-dlltool -l libmyapi.a -d myapi.def

# Usage
x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles -Wl,--custom-api=libmyapi.a -o custom-api.bof custom-api.c

Clang

llvm-dlltool -l libmyapi.a -d myapi.def

clang --ld-path=$(which boflink) --target=x86_64-windows-gnu -nostartfiles -Wl,--custom-api=libmyapi.a -o custom-api.bof custom-api.c

MSVC

lib /machine:x64 /def:myapi.def /out:myapi.lib

cl /GS- /c /Fo:custom-api.obj custom-api.c
boflink --custom-api myapi.lib -o custom-api.bof custom-api.obj

The --custom-api command flag also provides some flexibility on how Boflink finds the import library. It can either be the path to the import library on disk or the name of a link library for Boflink to search for in the list of library search paths.

If the custom API import library is a part of some packaged tool chain stored at /toolchains/myapi/lib/libmyapi.a, then it can be linked to using the following command line flags.

x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles -Wl,--custom-api=myapi -o custom-api.bof custom-api.c -L /toolchains/myapi/lib

boflink -o custom-api.bof custom-api.o –custom-api myapi -L /toolchains/myapi/lib

Boflink will add the /toolchains/myapi/lib search path to its list of link library search paths and search for the libmyapi.a library.

Internally, Boflink creates a directed graph that is used for performing various linking operations. During the course of development, it became useful to visualize this graph with an external tool.

The --dump-link-graph flag will write out the state of the link graph in GraphViz DOT format to the file specified.

x86_64-w64-mingw32-gcc -B ~/.local/libexec/boflink -fno-lto -nostartfiles -Wl,--dump-link-graph=graph.dot -o example.bof example.c

clang --ld-path=$(which boflink) --target=x86_64-windows-gnu -nostartfiles -Wl,--dump-link-graph=graph.dot -o example.bof example.c

boflink --dump-link-graph graph.dot -o example.bof example.c

This will create a file named graph.dot which is a plaintext GraphViz dot file containing the link graph. The dot command line tool can be used to render this file into an SVG or PNG file so it can be examined using an image viewer or web browser.

# Installing the Graphviz dot command line tool
sudo apt install graphviz
winget install –id Graphviz.Graphviz

# Converting to an SVG
dot -Tsvg graph.dot -o graph.svg

# Converting to a PNG
dot -Tpng graph.dot -o graph.png

This feature does not add anything feature-wise to the linked BOF but may be useful as a tool for debugging.

MSVCRT Pitfalls

It is fairly common for BOF developers to include and use C standard library functions from msvcrt.dll in their BOFs. The issue with using msvcrt.dll is that Microsoft basically abandoned all support for it back in 2010 since it had many incompatibilities with C89 (yes, the C standard released in 1989) and it could not add C89 support without breaking ABI compatibility. Microsoft does not even ship an import library for it and has not for a while now.

This means that you cannot link with msvcrt.dll on Windows without creating a custom import library for it. Even if you do decide to use msvcrt.dll, the functions contained are not compatible with C89 so the C standard library functions defined in the header files Microsoft provides may not be valid for it.

On the MinGW side, MinGW’s solution for dealing with the msvcrt.dll issues is to basically not deal with it at all. If you try to link with certain functions from msvcrt.dll using MinGW, MinGW will instead statically link their own implementation instead. This is so that you do not accidentally shoot yourself in the foot trying to use some C standard library function without knowing that the implementation for it inside msvcrt.dll is completely different than the header definition for it.

The solution for this with Boflink is to use UCRT instead. UCRT is the replacement for msvcrt.dll and is installed by default starting with Windows 10. Boflink supports linking with the UCRT both on Windows with MSVC and using MinGW.

As for UCRT support with BOF loaders, I am not sure how every single BOF loader is implemented so it may vary. I do know that TrustedSec’s COFFLoader has worked when using UCRT in some of my testing.

To link with UCRT on Windows pass -lucrt as an additional link flag for finding C standard library functions.

MinGW ships a separate UCRT toolchain with UCRT support. It includes x86_64-w64-mingw32ucrt-gcc as a separate compiler instead of x86_64-w64-mingw32-gcc. Using x86_64-w64-mingw32ucrt-gcc will link with UCRT.

Linking against msvcrt.dll is highly discouraged due to the pitfalls stated above. If using UCRT is not an option, it is recommended to try and find alternative functions from shlwapi.h or strsafe.h. Another option is to use compiler intrinsic functions if the compiler includes them.

This may be rather inconvenient to many people who are used to using msvcrt.dll in their BOFs. Seeing that it is very prevalent in many open source BOFs, I did spend a lot of time figuring out how to add some support for it as a fallback. I could not figure out a good solution that would work in a non-intrusive way and since Microsoft has been trying to migrate to UCRT, supporting that is a better option.

Language Support

Boflink was developed and tested with BOFs written in C. This is because C is by far the most common language used when developing BOFs. Languages outside of C, such as C++, have not been tested at all with Boflink.

That being said, some C++ may work currently but since it has not been tested, the state of C++ support is unknown. Most of the COFF spec is implemented in Boflink but there may be some edge cases with C++ compilers that Boflink does not handle properly. On top of this, there are some language features in C++ which cannot be expressed properly in a COFF. Those features would require some form of runtime initialization to happen either during startup or with the BOF loader.

Conclusion

This project was designed with the goal to help streamline the BOF development process. There are many aspects with BOF development that are notoriously tedious and can make it somewhat more difficult. On the fly capability development has become an increasingly more demanding task. Time and research spent on further streamlining the development process is often neglected but can provide a lot of long term value.

As an aside, some people may be asking: “Isn’t this tool a little bit overkill for typical BOF development scenarios?”. Yes, I absolutely agree that this is a pretty over the top solution for the current issues BOF developers face. This project was also a personal research project for me to understand more about how linkers work. I decided to use Beacon Object Files for this since it can provide some value as a development tool and writing a linker for BOFs is a lot simpler than for full executables.