raw-data memdumps

BugSleep network protocol reversing

October 1, 2024

Back in July cp<r> Check Point Research team released a technical analysis of, at the time, a new backdoor they observed in phishing campaigns leveraged by the threat actor MuddyWater, also tracked by other Intel shops as ATK51 or TA450.

Based on their investigation and analysis, they named the malware family BugSleep due to the many calls to the Windows API Sleep and bugs observed in some of its functionalities.

The article goes into great details peeling out the different layers of the attack chain while focusing on BugSleep loader and payload components. Now, something I thought would have been interesting to investigate, was the set of supported commands the C2 can instruct BugSleep to execute and more precisely how the two components (the client and the server) interact with each other.

On the top of this, as I could not find at the time any pcap with some live C2 traffic, this would have also served me well as a playground for developing a fake C2 server from scratch and trigger on-demand function of the backdoor, which based on Check Point report, seems to be between 10 or 11 commands depending on the version of the backdoor.

Given that the malware family does not come obfuscated, it uses sockets for communication and it employs a weak data encryption strategy it sounds reasonable to treat this family as a guinea pig to get hands dirty on network protocol RE.

So, without further ado, fasten your seat belt and let’s get started! 👾

A Bug that sleeps

The sample we are going to investigate can be found in the blog and has a SHA256 hash value of b8703744744555ad841f922995cef5dbca11da22565195d05529f5f9095fbfca .

Also, keep in mind that

The function in charge of managing the network connection and in general handling C2 operations is sub_1400012C0. After cleaning-up a bit the IDA database by applying the right data types, and renaming functions with meaningful names, the pseudo-c code becomes more readable, getting from something like in the screenshot below

to a more talkative pseudo code like this

Scrolling to the button of this function, subroutines sub_140003D80 and sub_140028A0 stores respectively C2 check-in and C2 message dispatcher logic. We will focus first on the easiest of the two, the C2 check-in part, as it simply sends a "hello" message to the server so that the client can be enrolled into the bots pool manged on the server side.

sub_140003D80 (C2 check-in)

By analyzing this function, at its core it can be seen how packets are crafted before being sent to the server. A first message, 4 bytes in size, stores the size of the data that will follow, finally the data message itself is sent. The overall structure can be broken down as sketched below

What follows is a light commented function which should provide a high level overview of the initial communication process and messages exchange between the backdoor and the C2 server.

What can be observed from this first part, it’s that 1) a string composed of the infected Computer Name and the Windows User name (running the backdoor) is sent to the C2 server and that 2) BugSleep expects some reply which content is not really used but the size of the same matters, as we will shortly see.

By inspecting the function sub_1400034C0 here renamed into mw_EncSendMsgToC2 it can be seen how the exchanged packets between the client and the C2 are not only based on a custom protocol but they are also “light encrypted”, with a sort of Caesar cipher. The encryption, which follows the same implementation used for hiding strings in the binary, subtracts in this case hex 0x3 to every processed byte.

If we were intercepting C2 check-in traffic, a possible message could look like this

00000000  11 fd fd fd                                       ....
00000004  41 30 50 48 51 2d 4d 2a  51 2d 3e 2e 2e 42 4f 2c  A0PHQ-M*Q->..BO,
00000014  52 70 30 4f                                       Rp0O

the message can be easily interpreted on the C2 side by simply adding hex 0x3 to every byte.

import sys
from typing import List


def decodeMsgs(encStrings: List[str])-> None:
    for encString in encStrings:
        szEncString = list(encString)
        for i in range(len(szEncString)):
            szEncString[i] = chr(ord(szEncString[i]) + 3)
        print(''.join(szEncString))

if __name__ == "__main__":
    encStrings = [
        "A0PHQ-M*Q->..BO,Rp0O"
    ]
    sys.exit(decodeMsgs(encStrings))

The string A0PHQ-M*Q->..BO,Rp0O is so decrypted into D3SKT0P-T0A11ER/Us3R. While, for what concerns instead the size of the data, which is stored in the first part of the message being sent, and in this example is set to 11 fd fd fd, the conversion follows the same logic.

It requires adding hex value 0x3 to every byte of the sequence. By converting the first hex byte 0x14 to an integer in base10 we get 20, which is - correctly - the size of the submitted string.

>>> hex(len("D3SKT0P-T0A11ER/Us3R"))
'0x14'

Finally, to successfully complete the C2 check-in handshake, the server must reply with a message which length must be greater than 3 bytes, otherwise the backdoor will simply terminate itself by calling ExitProcess(0).

The overall C2 check-in handshake is summarised in the following diagram

sub_140028A0 (C2 message dispatcher)

With the C2 check-in operation out of the way, it’s now time to interact with the C2 server and inspect the logic which glues together transmitted C2 commands to the respective backdoor’s operative functions.

We will not cover all instructions offer by the analyzed variant, but definitely of interest are the first three, which are

Command hex code Expected parameter Functionalities description
0x0 Full path of a file on disk Uploads a file from the infected system into the C2 by reading it in chunks
0x1 Full path to a file to be dropped on the host Downloads a file from the C2 and stores it into the location defined by the operator
0x2 Command to execute on the host Gives operator Hands-On-Keyboard by starting a reverse shell on the host

Inspecting sub_140028A0 reveals the main logic which reads incoming messages from the server and branches into specialised functions in charge of actively interact with the infected system.

At this stage, BugSleep expects the following

For instance, if the result of the subtraction is 0x0, the backdoor will call a function which uploads a file from the infected host into the C2 server, while if a 0x1 is returned instead, a file is downloaded from the C2 server into the host, and so on.

Let’s go step by step, shall we? ;)

Cmd 0x0

In this first case

the overall logic looks like this

If all three conditions are met, the function here renamed into mw_wrap_ReadFromFile is called, giving in input a pointer to the buffer storing the decrypted data message, which will be a string describing a full Windows path to a file.

All in all, the full message sent from the C2 to trigger code handled by the case 0, looks like this

Let’s now investigate what happens within the renamed function mw_wrap_ReadFromFile.

Finally, content of the file is streamed, via sockets, to the C2

The full message exchange process for case 0 can so be broken down in 5 steps which are depicted in the diagram below and as it can be seen, at this stage the C2 is passively waiting for data and processing it on its end but nothing else.

Cmd 0x1

In this second case instead, a file can be downloaded from the C2 into the infected host

The initial C2 command extraction logic is the same as described previously, but this time the variable lpBufferC2MsgData will sore instead a Windows full path to a file which will be filled, so to speak, with some content defined on the C2 side, let’s investigate mw_wrap_WriteToFile.

The function argument, it’s a pointer to the buffer storing the decrypted string of a Windows full path to a file, let’s pretend something like C:\\Users\\<WindowsUserName>\\Desktop\\2ndStagePayload.bin;

A light commented function is reported below to showcase the transmission process

While implementing the fake C2 server I was not successful at the beginning to correctly transmit a file of whatever size from the server to the infected host without losing some bytes during the process, by refining how chunks were forged on the server side I eventually reach a point where only the last 4 bytes of the original transmitted file were missing.

I am not sure if it’s a standard implementation on the C2 side or a bug ( 😉 ) in how the last block is written to disk but, by looking a the pseudo-c code line ⤵️

mw_WriteFile(hObject, (file_content + 1), v11 - 4, 0LL, 0LL);

it seems that v11 - 4 is likely skipping 4 bytes. This aligns with the behaviour observed during the transmission tests, were the last 4 bytes were always missing from the sent file.

To address the case, the fake C2 implements some padding strategy, with 4 null bytes (b'\x00' * 4) added “on-demand”, so ensuring that the final message is always 1024 bytes long even if the last data block is smaller.

In this way, when the binary reaches BugSleep, the last 4 padded bytes will be skipped but the original content of the file will be preserved and correctly stored to disk.

Cmd 0x2

Two down, one to go! this last command starts a reverse shell giving Hands-On-Keyboard access to the operator.

It leverages common Windows APIs such as CreatePipe, PeekNamedPipe, SetHandleInformation, SetInformationJobObject , CreateProcessW, and ReadFile among other to setup and handle the shell. The same is based on the creation of a new Command Prompt instance (cmd.exe) which stdErr, stdOut and stdIn are encapsulated within the socket connection, allowing the operator to directly interact “live” with the compromised host.

As also observed in previous cases, BugSleep will notify the C2 server by sending an integer of value 1, letting the back-end know that the control command (0x2) was correctly received and the reverse shell is being created. The StdOut is read in chunks, here again by using the same strategy of 0x400 bytes per block with a final call for sending the remaining chunk.

Finally, when the transmission is completed, the client will notify the server once again by sending this time a 4 bytes message filled with zeros, e.g. 0x0000.

This final part (sending 4 zero bytes) plays definitely an interesting role on the handling of reverse shell logic on the fake C2 side. There is no need to meticulously check every single chunk message transmitted from the client, as it will be enough to read data out from the socket until a “marker” of 0x0000 is sent to notify the end of the message itself.

Also interesting to mention is that, BugSleep will inspect every single message received during reverse shell session to ensure no command terminate\n is being transmitted, in which case, it will simply exit the created session and wait for a new control message from the C2.

Also in this case, the communication flow can be sketch like this

Follows an example of the interactive shell offered by the BugSleepC2Emulator, as it can be seen it’s far from being stable, as status messages of the executed commands on the client side are not handled (read this as simply hide command and size of the same from user view within the custom shell), nevertheless, mission accomplished as we have now access to the host and with directory listing capabilities it’s now easy to download (by sending command 0x1) or upload (by sending command 0x0) a file from/to the endpoint.

Final thoughts

It was a fun ride! when implementing a fake C2 server from scratch there are different ways one can follow to slowly build all the required functionalities, definitely FakeNet-NG is one of them thanks to the base custom response modules, but also creating your custom one from scratch if time is not a constraint works just fine as in some cases, code snippets are all what you need to tests stuff out, but of course it depends on the complexity of the malware protocol and how you can trigger some behaviour on-demand on the malware (client) side.

Being able to interact with the backdoor provides also some visibility on how - possibly - a live C2 traffic would look like allowing the creation of network detection rules for the observed network pattern, Lua scripting - paired with the right tool - might be come in handy …