COFF Execution/BOFs

BOFs are COFF (Common Object File Format) object files which are not statically or dynamically linked in nature. COFF file is a format for executable code which contain information about runtime or static linking. These are supposed to be linked with other libraries without which the object files cannot be run. The badger, when supplied with a COFF file, will act as a linker for them. This means you can write COFF files which use Windows API and Windows DLLs and supply them to the badger. Badger also exposes a few of it’s own API calls which can be used by the COFF files. The COFF files are linked dynamically at runtime, both for Windows API as well as Badger API calls. In order to link the BOF, badger needs to allocate RW and RX regions to copy the BOF to the RX region in the current process and patch their API calls. This is done using obfuscated syscalls as mentioned in the v0.9 release.

The coffexec command parses the object file provided by the operator and patches the exported functions on the fly with the internal APIs of badger and windows DLLs. This makes the port of existing Cobaltstrike BOFs to Brute Ratel extremely easy. Let’s take the following cobaltstrike’s BOF as an example which was taken directly from their website.

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

DECLSPEC_IMPORT DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID);
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$NetApiBufferFree(LPVOID);

void go(char * args, int alen) {
    DWORD dwRet;
    PDOMAIN_CONTROLLER_INFO pdcInfo;

    dwRet = NETAPI32$DsGetDcNameA(NULL, NULL, NULL, NULL, 0, &pdcInfo);
    if (ERROR_SUCCESS == dwRet) {
        BeaconPrintf(CALLBACK_OUTPUT, "%s", pdcInfo->DomainName);
    }

    NETAPI32$NetApiBufferFree(pdcInfo);
}

As you can see in the code above, the entrypoint for the COFF file for Cobaltstrike is ‘go’. In case of Brute Ratel however, the entrypoint is ‘coffee’. Below is the code for Brute Ratel’s BOF (Badger Object Files?).

#include <windows.h>
#include <stdio.h>
#include <dsgetdc.h>
#include "badger_exports.h"

DECLSPEC_IMPORT DWORD WINAPI NETAPI32$DsGetDcNameA(LPVOID, LPVOID, LPVOID, LPVOID, ULONG, LPVOID);
DECLSPEC_IMPORT DWORD WINAPI NETAPI32$NetApiBufferFree(LPVOID);

void coffee(char** argv, int argc, WCHAR** dispatch) {
    DWORD dwRet;
    PDOMAIN_CONTROLLER_INFO pdcInfo;

    dwRet = NETAPI32$DsGetDcNameA(NULL, NULL, NULL, NULL, 0, &pdcInfo);
    if (ERROR_SUCCESS == dwRet) {
        BadgerDispatch(dispatch, "%s\n", pdcInfo->DomainName);
    }

    NETAPI32$NetApiBufferFree(pdcInfo);
}

There really isn’t much difference except the internal API calls (BeaconPrintf for Cobaltstrike and BadgerDispatch for BruteRatel). The reason why I decided to have a different naming convention for the API calls unlike BeaconPrintf is because I plan to release several more API calls that I’ve built over the past one year. So, I wanted to have a naming convention that suits the APIs of Brute Ratel and not Cobaltstrike. The below figure shows the executed output of the COFF file:

The BOFs do not use any RWX region to work and they get executed like any other internal function of the badger. Below are the few other API calls which are available in this release which can be used in BOFs. These can be found in the badger_exports.h file as well which you would need to include in your COFF generating C file.

  • BadgerDispatch
    • BadgerDispatch is a variadic function which can take any number of arguments like the printf command. The only catch is that the first argument should be a WCHAR** variable which was the last argument in the ‘coffee’ function. This API returns the output in the char* format to the badger which is sent to the server. This argument only takes ANSI (CHAR) strings. For returning any data in WCHAR, use BadgerDispatchW.
  • BadgerDispatchW
    • BadgerDispatchW is a variadic function which can take any number of arguments like the wprintf command. The only catch is that the first argument should be a WCHAR** variable which was the last argument in the ‘coffee’ function. This API returns the output in the char* format to the badger which is sent to the server. This argument only takes Unicode/Widechar (WCHAR) strings. For returning any data in CHAR, use BadgerDispatch.
  • BadgerStrlen
    • Calculates and returns the length of a char* string similar to strlen, but does not call strlen from msvcrt.dll.
  • BadgerWcslen
    • Calculates and returns the length of a wchar* string similar to wcslen, but does not call strlen from msvcrt.dll.
  • BadgerMemcpy
    • Copies n characters from a memory area source to memory area destination like memcpy, but does not call memcpy from msvcrt.dll.
  • BadgerMemset
    • Fills a block of memory with a particular value similar to memset, but does not call memset from msvcrt.dll
  • BadgerStrcmp
    • Compares two strings lexicographically. If first character in both strings are equal, then this function will check the second character, then the third and so on. This process will be continued until a character in either string is NULL or the characters are unequal. This works similar to strcmp, but does not call strcmp from msvcrt.dll
  • BadgerWcscmp
    • Compares two unicode/widechar strings lexicographically. If first character in both strings are equal, then this function will check the second character, then the third and so on. This process will be continued until a character in either string is NULL or the characters are unequal. This works similar to wcscmp, but does not call wcscmp from msvcrt.dll
  • BadgerAtoi
    • Converts an ascii value to integer similar to atoi, but does not call the atoi from msvcrt.dll

A brief example of the usage of all the above APIs can be seen below:

#include <windows.h>
#include <stdio.h>
#include "badger_exports.h"

WINADVAPI WINBOOL WINAPI Advapi32$GetUserNameA(LPSTR lpBuffer, LPDWORD pcbBuffer);
WINADVAPI WINBOOL WINAPI Advapi32$GetUserNameW(LPWSTR lpBuffer, LPDWORD pcbBuffer);
WINBASEAPI int Msvcrt$printf(const char *__format, ...);
WINBASEAPI int Msvcrt$wprintf(const WCHAR *__format, ...);

void coffee(char** argv, int argc, WCHAR** dispatch) {
    CHAR username[MAX_PATH] = { 0 };
    DWORD usernameLength = MAX_PATH;
	Advapi32$GetUserNameA(username, &usernameLength);
    BadgerDispatch(dispatch, "[+] Char Username: %s\n", username);

    int usernamelen = BadgerStrlen(username);
    BadgerDispatch(dispatch, "[+] Username length: %d\n", usernamelen);

    if (argc > 0) {
    int retval = BadgerStrcmp(argv[0], username);
    if (retval) {
        BadgerDispatch(dispatch, "[+] Unequal values: %s\n", argv[0]);
    } else {
        BadgerDispatch(dispatch, "[+] Equal values: %s\n", argv[0]);
    }
    } else {
        BadgerDispatch(dispatch, "[+] No Args provided\n");
    }


    WCHAR usernameW[MAX_PATH] = { 0 };
    usernameLength = MAX_PATH;
	Advapi32$GetUserNameW(usernameW, &usernameLength);
    BadgerDispatchW(dispatch, L"[+] Wchar Username: %ls\n", usernameW);

    int usernamelenW = BadgerWcslen(usernameW);
    BadgerDispatchW(dispatch, L"[+] UsernameW length: %d\n", usernamelenW);

    WCHAR testW[] = L"somevalue\0";

    int retval = BadgerWcscmp(testW, usernameW);
    if (retval) {
        BadgerDispatchW(dispatch, L"[+] Unequal widechar strings\n");
    } else {
        BadgerDispatchW(dispatch, L"[+] Equal widechar strings\n");
    }

    char *intstr = "10";
    int converted = BadgerAtoi(intstr);
    BadgerDispatch(dispatch, "[+] Atoi: %d\n", converted);

    BadgerMemset(testW, 0, sizeof(testW));
    if (BadgerWcslen(testW) == 0) {
        BadgerDispatch(dispatch, "[+] Memset complete\n");
    }

    BadgerDispatch(dispatch, "[+] All Arguments:\n");
    for (int i = 0; i < argc; i++) {
        BadgerDispatch(dispatch, "  - arg[%d]: %s\n", i, argv[i]);
    }
}

You can save the above code as decltest.c and compile it as:

x64 compile: x86_64-w64-mingw32-gcc decltest.c -c -o decltest64.o -m64

x86 compile: i686-w64-mingw32-gcc getdc.c -c -o getdc86.o -m32

Output from coffexec: