“Just call a guy” is probably a frequently coined phrase at your average household. This week was mostly focused on maintenance of current projects, and I was that guy. It was also very turbulent for Crypto which saw a steady decline in value (thanks, Elon), nonetheless I went to the moon. While I was up there I remembered I forgot to close the backdoor after I left.
1. Just call a guy
Every now and then, things break. It mostly happens when you really need them or at a very inconvenient time. This week is spent the majority of my time staring at my IDE and burried 7 functions deep in WinDbg. It was great.
While I continued development of my Beacon Object Files, I decided to create one master script that would handle the user interface integration, payload creation and encoding, obfuscation and more. The concept is based on StayKit, a persistence kit for Cobalt Strike which uses .NET assembly under the hood. I have dubbed it the PeloponneseanWarKit because let’s be honest, it sounds better than “StayKit”.
The overall concept is quite simple.
- The script maintains a global set of defaults for common parameters and payload creation, which can be updated dynamically
- It hooks into the Cobalt Strike user interface and dynamically implements dialogs and menu items for the different Beacon Object Files
- It dynamically handles payload generation if desired
- It handles payload encoding and obfuscation using XOR encryption and base64 encoding
- It packs all the required arguments using
bof_pack()
so the target BOF can unpack them
The benefits of this approach are ease of use, easy to extend and contribute, avoid reuse of code and a solid base structure to build on top.
With the new concept worked out, I had to port over the functionality of my old scripts, which didn’t come without issues. The greatest thing about Cobalt Strike’s Aggressor Script is definitely not its packing API.
Cobalt Strike uses the bof_pack()
function to pack arguments into a binary structure, so Beacon’s BOF API can unpack them again. It is (supposedly) an easy way to pass user input to a Beacon Object File. bof_pack()
supports different packing types:
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 |
Most of the userdata I had to pack were simple strings so z
was sufficient, the same goes with integer parameters where i
would suffice and Beacon wouldn’t have any issues unpacking the arguments. Since payloads are base64 encoded and then XOR encrypted I can either pack them as binary data or as a string, both work fine. To decrypt and decode the payload, Beacon only needs the size (length) of the data which can be retrieved with BeaconDataExtract(&parser, &size)
. However I quickly ran into issues when packing wide-chars (wchar_t).
A wide character, or in C/C++ wchar_t
, is UTF-16 little-endian encoded on Windows. This is double the size of a regular UTF-8 8-bit character. To use RegKeyPersist, which I will discuss later, I needed to pass some user arguments as wide character strings to Beacon. I used bof_pack("Z")
to pack the data. To unpack the data I used the following code:
datap parser;
BeaconDataParse(&parser, args, alen);
wchar_t* arg = (wchar_t*)BeaconDataExtract(&parser, NULL);
This resulted in a NTSTATUS STATUS_DATATYPE_MISALIGNMENT (0x80000002) when I tried to use arg
in an API call. Strange. In the end I had to resort to packing the arguments as regular strings with z
and using Beacon’s toWideChar()
function to convert to a wide-char.
datap parser;
BeaconDataParse(&parser, args, alen);
char* buff = BeaconDataExtract(&parser, NULL);
// convert from char* to wchar_t*
wchar_t arg[100];
toWideChar(buff, arg, 100);
When I decided on payload management, I figured it would be good to add base64 encoding on top of the 1 byte XOR encryption I was already using. This would add a number of benefits:
- decreased payload size
- no plain XOR’d binary data going over the network
- no plain base64 going over the network
This means I had to implement base64 decoding in my Beacon Object Files. I found a great base64 decoding routine from the FreeBSD project by MIT, however I’m currently still running into issues when parsing the payload size after decoding.
2. To the moon!
In the midst of it all, I updated DomainBorrowingC2 to bypass CDN caching and be a little less noisy by reducing the frequency of callbacks. It’s now fully working and supports tasks. I’m planning on integrating it with the PeloponneseanWarKit and write a C/C++ client that can be spawned with a Beacon Object File.
Unlike Crypto, it gained quite some traction in the infosec community since I released it publicly. After internal discussion with NVISO it has been decided the public version will remain available and will receive future updates and enhancements.
Back to our regular scheduled program!
3. Inviting myself to the party
This week I decided to take a break from process injection and take a look at persistence, because afterall, what’s more fun than letting yourself in uninvited, or should I say self-invited :)
Similar to my process injection shenanigans, I wrote a Beacon Object File that uses direct syscalls to plant a registry key. There are 4 main registry keys that can be used to achieve persistence depending on the context.
Non-elevated: will only run when the current user logs on
- HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
- HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
Elevated: will run anytime the system boots
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
Creating a registry key is surprisingly easy, it only takes 3 steps:
- Obtain a handle to the target key with
NtOpenKey()
- Write a new key-value pair with the payload using
NtSetValueKey()
- Cleaning up the mess with
NtClose()
To obtain a handle, NtOpenKey()
requires 2 parts of the target registry key: the root key and the subkey. There are a total of 7 root keys, they all start with the HKEY prefix. We’re interested in HKEY_LOCAL_MACHINE (HKLM) and HKEY_CURRENT_USER (HKCU).
As mentioned here these root keys are global handles. Some of them also have a corresponding object name starting with \Registry which can be used to access the handle from kernel-mode. HKEY_CURRENT_USER is an exception, it does not have an object name, so we cannot directly use it with NtOpenKey()
. This is because under the hood, HKCU looks like \Registry\User\<user-sid>. We can resolve this issue by calling RtlOpenCurrentUser()
which will give us the full root key.
Once a handle is obtained, a new key-value pair can be written to the target registry key with NtSetValueKey()
. It is possible to create a hidden key by preprending two nullbytes in front of the key name as outlined in this whitepaper.
UNICODE_STRING valueName;
valueName.Buffer = L"\0\0WinRegKeyName";
valueName.Length = 40; // length doesn't matter, it is needed to delete the key
valueName.MaximumLength = 0;
It is noteworthy that NtSetValueKey()
takes a UNICODE_STRING
as parameter, hence valueName.Buffer
is a wide-char (wchar_t). The created key is not really hidden, but when Regedit tries to read it, it will throw an error and not appear in the listing. The key cannot be exported and written to a file either and won’t appear in the Startup tab of Task Manager.
Prepending nullbytes to a user inputted string was a little harder but I managed to work with a prefix.
// < truncated for space >
char* buff = BeaconDataExtract(&parser, NULL);
wchar_t regkeyname[100];
toWideChar(buff, regkeyname, 100);
wchar_t prefix[200] = L"xx";
wcscat(prefix, regkeyname);
UNICODE_STRING valueName;
valueName.Buffer = prefix;
// get length first, because wcslen() will stop at a nullbyte
valueName.length = wcslen(prefix) * 2;
valueName.Buffer[0] = '\0';
valueName.Buffer[1] = '\0';
4. Wrapping up
The PeloponneseanWarKit is a work in progress and will take many more hours to get bug free, stable and add more functionality. I feel like I’ve learnt a huge amount of techniques, discovered a lot of interesting frameworks and tools and found a lot of interesting people in a short amount of time.
The great thing about developing tools is the research that goes into the inner workings of techniques and tools which we would otherwise git clone
, build and run, or worse, download and execute. On the other hand it also tests your patience and induces headaches.
Anyways, I’ve got an error: implicit declaration of function 'itsWeeknd()'
on line 311 which I need to get back to…
NtDelayExecution(FALSE, 604800000);