Brute Ratel v0.6.0 (Resurrection) is now available for download and provides a major update towards the x86 architecture support and various in-memory execution features. This release contains a major rewrite of a portion of the backend which provides better stability, at the same time allowing to make feature additions easier for future releases. I have listed the technical details of the release below, however a detailed list on the features and bug fixes can be found in the release notes.
One of the major feature addition to this release was the support for x86 payload generation. I was doing an engagement a while back where I was dealing with older hardware and windows software, especially in the OT environment where I realized there were only x86 hosts. Adding a new arch support meant building an x86 shellcode loader from scratch, adding support for x86 reflective DLLs for post-exploitation and COFF support. This also meant that I would have to rewrite the server backend so that it can identity what type of payload is running on a target host, find the target host and the payload’s architecture and send a reflective DLL/COFF according to the target process arch, so that the target process does not crash. This took a while, but now Brute Ratel comes with full support for x86 payloads. Everything you could do on x64, now can be done on x86 payloads too.
The HTTP payloads for x86 can be generated from the context menu of the listener.
Whereas the SMB and TCP payloads for x86 can be generated from the Payload Profiler by selecting your options from a drop down box.
One of the most requested feature in the past month was the ability to steal an existing token from another process. This feature already existed in Cobaltstrike as a ‘stealtoken’ command, but I wanted to make this a bit more advanced. I wanted to make sure that you can hot-swap tokens on the fly like any other feature of the badger without having to sacrifice an existing token that you’ve already stolen. So, I decided to add a mini storage container within the badger itself, which can store any number of stolen tokens which are swappable at runtime. You can now use the grab_token
command to grab an existing token from a given process. Remember that you would still need local administrative privilege on the host to steal the token. Reason being, to get a token, you need to use the OpenProcess API and get a HANDLE for that process with token read rights. This is only accessible to the process owner, the parent process or a local administrator on the host. The grab_token
command extracts the token from a process and stores it in the Token Vault. You can use the token_vault
command to view all the tokens stored in the vault, and then use the impersonate
command to impersonate an existing token stored in the vault.
The vault_remove
and vault_clear
commands can be used to remove or clear all tokens from the token vault.
Another major feature requested by several users was the ability to execute C-sharp assembly within the badger, i.e. without creating any new process. This release brings a new addition to the C-sharp reflection with the sharpinline
command. The code for loading CLRs and executing the sharp assembly were optimized heavily and is a part of the badger itself. This also helped me to lower down the size of the sharpreflect
command to a total of 30kb from the previous 150kb of reflective DLL. The sharpinline
works similar to that of sharpreflect
except that the console of the C-Sharp output from the local process is redirected to a buffer and that output is sent over to the server instead of doing a fork and run.
Both the sharpinline
and sharpreflect
command now use randomly generated AppDomains in order to avoid detection from ETW even though the ETW and AMSI is always patched.
The initial object file execution technique I used (objexec
, set_objecpipe
) was heavily different from the one used by Cobalt Strike. Cobaltstrike used in-memory patching of the object file for exported symbols whereas the objexec
command required the user to load the DLL using LoadLibrary and find the exported addresses on their own. However, this is no longer the case with the 0.6 release. This release replaces the objexec
command with the coffexec
command. The coffexec
command simply parses the object file provided by the operator and patches the exported functions on the fly with the internal APIs of badger. 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);
}
As you can see above, there isn’t much different 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 tonnes of API that I’ve built over the past 1 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.
strlen
, but does not call strlen from msvcrt.dll.wcslen
, but does not call strlen from msvcrt.dll.memcpy
, but does not call memcpy from msvcrt.dll.memset
, but does not call memset from msvcrt.dllstrcmp
, but does not call strcmp from msvcrt.dllwcscmp
, but does not call wcscmp from msvcrt.dllatoi
, but does not call the atoi from msvcrt.dllA 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:
A lot of times during an engagement, your payload connectivity might get dropped and you might never know why that happened. It might either be that your payload was flagged due to some post-exploitation stuff, or maybe the system went to sleep. There are heavy chances that the battery went out and your payload was not persistent so it never came back. This was the main reason why crisis_monitor
feature was added to Brute Ratel. This feature when enabled, will constantly check for a selected set of events and whenever that event is executed, it will send a notification back to the server. The monitored events are:
In any of the above scenarios ranging from power changes to session connection, disconnection or user login, badger will send a notification back to the server that an event has occured. This can be extremely helpful in scenarios wherein you can get a quick notification when a member of blueteam logs in and you might want to stop your post-exploitation activites at the moment so that you are not busted. Crsis monitor can be enabled or disabled with a single command-line argument start
or stop
.
Starting with this release, badger will have support for various debugging features. The current debugging features are limited to identification, however this allows the possibilty of future releases to have the capability of CPU instruction modifications. This was added in order to identify and target specific EDRs and monitor if the API calls called by your badger are hooked.
The list_modules
command lists the loaded DLLs in the current process or a target process. This helps to indentify if any DLL from an EDR has been loaded into your process which performs userland hooks.
It takes a PID as an optional argument to list the DLLs loaded in a target process.
The list_exports
command can be used to list all the exports of a given DLL loaded within the current process. This can be useful to identify if a DLL loaded by an EDR has any specific exports that you might want to modify/patch. The current release only supports identification of the exports and does not support modification which would be there in the next release. However, a BOF should be good enough to perform the job of export address patching for now. One important thing to note here is that if an EDR is monitoring the patching of DLL instructions or hooks, then it might raise an alert that the hooks were modified. There are very few EDRs who perform such hooks, however these EDRs also monitor reading (NtCreateFile) of the NTDLL file by the user process, so that the operator does not restore the original instructions for the syscall by reading the DLL from disk and patching the instructions from the original file. In such cases, we might have to deploy ROP gadgets to perform small jumps across the memory section to find the RX/RWX region where the hook routes to, and find the actual instructions and use them, instead of patching the hooked DLL. This is specific to a few syscalls, specially for NTAPIs like NtCreateFile, NtReadVirtualMemory or RtlCreateUserThreads. The next version would however include syscall unhooking which should resolve this issue. However, we should not underestimate the fact that finding exports and patching them across has it’s own set of benefits where you can modify non-hooked exports to route the instruction pointers (eip/rip registers) to your self-controlled memory region and execute your malicious code.
The memhunt
command was added to identify the RX/RWX regions across the current or a target process. It takes a process ID and a PAGE permission in hex to identify and hunt for memory regions and return the addresses. Further releases will include a feature to free up allocated regions of memory which can be a Private Commit, Image Commit or even SEC Commits. According to Microsoft documentation, the PAGE_EXECUTE_READWRITE
stands for 0x40 in hex.
#define PAGE_NOACCESS 0x01
#define PAGE_READONLY 0x02
#define PAGE_READWRITE 0x04
#define PAGE_WRITECOPY 0x08
#define PAGE_EXECUTE 0x10
#define PAGE_EXECUTE_READ 0x20
#define PAGE_EXECUTE_READWRITE 0x40
#define PAGE_EXECUTE_WRITECOPY 0x80
#define PAGE_GUARD 0x100
#define SEC_LARGE_PAGES 0x80000000
So, in order to search for this page type, we can provide the argument ‘40’ to the memhunt
command to search for RWX regions. I executed InternalMonologue.exe using sharpinline
in the below figure, which creates several RWX regions for the mscoree.dll and mscorlib.dll. These RWX regions can be found using the memhunt
command. Badger itself does not have any RWX regions by default unless created by the operator.
Badger now provides 2 commands to set and fetch a kill date. The kill date is set into the badger and will kill any configured badger as per the kill date irrespective of whether the badger is connected to the server or not. The command set_killdate
can provide a kill date to the badger as to when the running badger should simply exit. The get_killdate
command on the other hand returns the current set killdate for the badger. The date formate it accepts is only in the RFC822 format e.g.: 22 Sep 21 22:55 IST. This is basically a cleanup command for your payloads, so that the payload does not connect back to your C2 after the engagement completes.
The above figure shows the badger exiting exactly on the provided killdate and time.
Several times during an engagement, you might want to inject more than just the badger to a target process. The shinject
command and the pcinject
commands can only inject payloads to the child process that was set using the set_child
command. However, there could be scenarios where you might want to just start a suspended process and later decide what you want to use it for. Badger now has this option using the suspended_run
command which can start a suspended process and just leave it. This command does support the dll_block
, set_argument
and the set_parent
commands. Detailed information on which commands support other additional commands can be viewed from the help menu which is more detailed than before.
Several other minimal features and tweaks were added to Brute Ratel. The ps
command shows a bit more detailed information including the full path of the process in a tabular format.
The portscan
arguments now take in a range of ports and not just individual ports.
Several changes were made to the GUI and the server upon user requests. A detailed list can be found in the full release notes. Some of the core functionality changes can be found below.
The default ‘exit’ command of the badger was changed to exit_process
and an added exit_thread
functionality has been added to the badger. This is accessible both from the UI and the badger terminal. Several times when you execute the badger’s shellcode into an existing process, you might want to leave the main process as is and just exit the badger’s thread. The exit_thread
command does the exact same thing and does not kill the whole process.
For every upload and download, the MD5 hashes, path and the hostname are now logged. This can be extremely helpful when you want to know which and where the were artefacts dropped during or after an engagement. The logs are stored in the logs directory of the ratel server and can also be viewed from the Commander in the logs section.
The Ratel server now stores badger configurations seperately in the logs directory under ‘badger_tokens.conf’. This can be helpful for restoring badger connections in events where you stopped and started the ratel server back again. The Ratel server also accepts two additional argument in the command line. An -sp
which can generate a sample profile for beginners and learners and a -b
argument which can accepts the badger configuration file.
The Badger’s terminal has a new addition to search raw strings in the same console alongside an ldap query input box where you can type your raw ldap queries without using the Ldap Sentinel.
The User Activity has an Export CSV option which now also shows MITRE mappings along with every command executed
Commander does not have 2 different versions now. The Elf binary for Commander should work in any linux distro that support Elf, however the commonly supported OS are Kali, Debian 11 and Ubuntu 20.04