{
	"id": "24b993a9-a252-4631-9603-0f8c9b043a12",
	"created_at": "2026-04-06T00:10:55.287248Z",
	"updated_at": "2026-04-10T13:12:07.23296Z",
	"deleted_at": null,
	"sha1_hash": "a9fb8f2df91ffd8c5b23745dd58543318b6d1a29",
	"title": "Writing a BugSleep C2 server and detecting its traffic with Snort",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2358040,
	"plain_text": "Writing a BugSleep C2 server and detecting its traffic with Snort\r\nBy Aaron Boyd\r\nPublished: 2024-10-30 · Archived: 2026-04-05 19:16:45 UTC\r\nWednesday, October 30, 2024 06:00\r\nIn June 2024, security researchers published their analysis of a novel implant dubbed “MuddyRot”(aka\r\n\"BugSleep\"). This remote access tool (RAT) gives operators reverse shell and file input/output (I/O) capabilities\r\non a victim’s endpoint using a bespoke command and control (C2) protocol. This blog will demonstrate the\r\npractice and methodology of reversing BugSleep’s protocol, writing a functional C2 server, and detecting this\r\ntraffic with Snort. \r\nKey findings \r\nBugSleep implant implements a bespoke C2 protocol over plain TCP sockets. \r\nBugSleep operators have demonstrated multiple file-obfuscation techniques to avoid detection. \r\nBugSleep implements reverse shell, file I/O, and persistence capabilities on the target system. \r\nSending and receiving data \r\nThis blog will use sample b8703744744555ad841f922995cef5dbca11da22565195d05529f5f9095fbfca for\r\nanalysis. Two of the lowest functions in the C2 stack, referred to as SendSocket (FUN_1400034c0) and\r\nReadSocket (FUN_140003390), are very light wrappers for the send and receive API functions and handle\r\npayload encryption. They include some error handling by attempting to send or receive data 10 times before\r\nfailing.  \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 1 of 14\n\nThis protocol uses a pseudo-TLV (Type Length Value) structure with only two types: integer or string. Integers are\r\nsent as little-endian 4- or 8-byte values, and strings are prepended with the 4-byte value of its length. Payloads are\r\nthen encrypted by subtracting a static value from each byte in the buffer (in this sample it is three).  \r\nType  Value  Plain text  Cipher text \r\nIntegerMsg  6  06 00 00 00  03 FD FD FD \r\nStringMsg   Talos  05 00 00 00 48 65 6C 6C 6F  02 FD FD FD 51 5E 69 6C 70\r\nFigure 1: Example of data encryption used by BugSleep\r\nThere are two main functions for handling C2 communications: C2Loop (FUN_1400012c0) and\r\nCommandHandler (FUN_1400028a0). C2Loop is responsible for setting up socket connections with the server\r\nand sending a beacon, while CommandHandler is responsible for processing and executing commands from the\r\nserver. \r\nAfter setting up the socket connection, the implant beacons (FUN_140003d80) to the C2 server for a command.\r\nThe beacon is a StringMsg in the form ComputerName/Username. If the server responds with an IntegerMsg\r\nequal to 0x03, BugSleep will terminate itself. We suspect this is remnants of an old kill command or an emergency\r\nkill without the overhead of reading the real kill command later. \r\nEach BugSleep command is sent as an IntegerMsg after the beacon response. The following enumeration defines\r\nall the command IDs discovered. \r\nFigure 2: Command IDs used by implant \r\nPhoning home \r\nThe implant communicates using plain TCP sockets, which can be seen using a Netcat listener and Wireshark. \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 2 of 14\n\nFigure 3: BugSleep beacon as seen through Wireshark. \r\nRecalling the message encryption demonstrated in Figure 1, the beacon can be decrypted with a little bit of Python\r\n(Figure 4). This will be used again when building the rest of the C2 server.  \r\nFigure 4: Decoding beacon data \r\nPython C2 server \r\nWith an understanding of the protocol basics, it is time to start building the C2 server. Full source code can be\r\nfound here. \r\nBeacon \r\nAs mentioned earlier, the BugSleep beacon function sends a StringMsg and reads an IntegerMsg response from\r\nthe server. Since the IntegerMsg returned can be anything but 0x03, we returned the length of the Computer\r\nName/Username string received by the server.\r\nFigure 5: Output from C2 server receiving beacon data \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 3 of 14\n\nPing command \r\nThe simplest command to implement is the Ping command. It has the command ID of 0x63 (BugSleep subtracts\r\none from whatever ID it receives). The code is simple: send back 4 bytes.  \r\nFigure 6: Switch case for handling ping command \r\nOnce the beacon comes in, the server is responsible for: \r\n1. Sending 4 bytes for beacon response \r\n2. Sending 4 bytes for Ping command ID \r\n3. Reading 4 bytes of Ping data \r\nThe ping command was observed sending back 4 bytes recently allocated on the heap, so it's not guaranteed to\r\nknow what that data looks like. To validate things are really working, a breakpoint can be set in WinDbg and\r\nmemory set manually before being sent. \r\nFigure 7: Confirming 0xdeadbeef written to memory is received by the server in a Ping command \r\nFile commands \r\nThe next set of commands are responsible for downloading files onto the compromised system or uploading files\r\nto the C2 server (PutFile and GetFile, respectively). These commands are inverses of each other, so only the\r\nGetFile command will be discussed in detail. The methodology was to trace each call to SendSocket or\r\nReadSocket and implement the response for that call in Python. In CommandHandler, the implant reads the length\r\nand value off the wire. This is the file to be retrieved.  \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 4 of 14\n\nFigure 8: GetFile reading path string length and path string from socket \r\nThe CmdGetFile function opens the target file and chunks it over the socket one page at a time. The list of\r\nSendSocket calls is as follows: \r\nFigure 9: SendSocket calls made by CmdGetFile function \r\nFigure 10: Example C2 server output from GetFile command \r\nThe PutFile command differs slightly from the GetFile command with how it uses pointer math to process\r\nincoming pages. \r\nFigure 11: Tricky file pointer math \r\nThis translates to each page starting with a 4-byte page number followed by 1020 bytes (or 0x3fc) of file data,\r\nwhich the GetFile command does not do; it sends full 1024-byte pages of file data without page numbers. \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 5 of 14\n\nReverse shell \r\nThe last command is the reverse shell. This is the most complex because it requires many reads and writes over\r\nthe socket. The disassembly is rather long and difficult to keep track of the socket calls, so we have omitted it.\r\nEffectively, the implant spawns a cmd.exe process (FUN_1400016e0) and reads the command to execute from the\r\nsocket. The shell command and its output are marshaled between the processes via pipes during the session. The\r\ncomplexity of this operation comes from BugSleep incrementally reporting return values from pipe API calls\r\nwhile attempting to read shell output (FUN_140003840). The implant will enter this loop of reading commands\r\nand sending output until it receives the string “terminate\\n”.  \r\nFigure 12: Example output from C2 server running the reverse shell command \r\nThe rest of the commands are less complex but have been implemented and are viewable here. \r\nSnort detection \r\nThis server gives Talos the ability to emulate any number of conversations between BugSleep and its operators.\r\nThis traffic is crucial for writing and validating our detections’ performance in the wild. \r\nThe initial candidate for detection would be the beacon. It is the first opportunity to shut down communications,\r\nisolating any BugSleep instance from receiving commands. It was observed that each beacon has the form of\r\n\u003clen\u003e\u003cdata\u003e, where data is sub_string(COMPUTER_NAME + \"/\" + USERNAME, 3). This string is not long or\r\nstatic, which makes it a poor candidate for a fast_pattern; however, recall that each beacon is prepended with a 4-\r\nbyte length of this string. A Computer Name/Username string from any given victim is unlikely to be longer than\r\n255 characters. This means most length fields are going to look like |XX 00 00 00| or |XX FD FD FD| when\r\nencoded. This could be a quick match, early in the stream, at a static offset, making it a decent fast_pattern\r\ncandidate. \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 6 of 14\n\nFigure 13: Detecting higher order encoded zero bytes of beacons sent from BugSleep \r\n This will work but is likely to cause false-positives (FP) in the wild. Every sample of BugSleep was seen using\r\nport 443. The implant is also reaching outside the network to a C2 server, so traffic to be inspected by this rule can\r\nbe reduced using the following header: \r\nFigure 14: Restricting rule to inspect traffic leaving the network to port 443 \r\nThe flow:to_server,established option can be used to restrict Snort to data coming from a client over established\r\nTCP streams. The FP-rate on this rule still isn't great. Any TCP traffic leaving the network on port 443 with |FD\r\nFD FD| at offset 1 will alert. That might sound unique, but it does not indicate with confidence that the traffic is a\r\nBugSleep beacon. \r\nOne powerful tool in Snort to add more logic or state to rules is flowbits. These allow a writer to have a sense of\r\nstate within a stream across multiple rules. In this case, the beacons aren't enough to reliably alert on. What if we\r\nuse flowbits to chain beacons with the commands being sent back? The commands themselves don't provide much\r\ncontent, as they are variable length non-deterministic strings (e.g., get, put, etc.) or a nondeterministic 4-byte\r\ninteger (e.g., heartbeat, increment timeout, etc.). They do, however, all start with a 4-byte command ID. Setting a\r\nflowbit when a beacon leaves the network will allow another rule down the line to alert with higher confidence if\r\nit sees a command ID come back in the same stream. \r\nCommand rules \r\nThe pcre rule option can be used to reduce 11 rules down to one. Like the beacon rule, the three zero bytes,\r\nencoded as |03|, can be used as a fast_pattern. Once the rule has entered, the bugsleep_beacon flowbit check can\r\nbe performed to help the rule exit quickly in the event of a false positive. After the three |03| bytes are confirmed\r\nto be at offset five, a PCRE can verify one of the command IDs is present.  \r\nFigure 15: Snort rule for detecting BugSleep command sent from C2 server \r\nSharp edges \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 7 of 14\n\nSometimes, we are reminded that Snort can handle or interpret data differently than expected. Conveniently, this\r\nsample’s traffic was a perfect example and opportunity to peek under the hood and see what Snort sees. Originally,\r\nour beacon rule looked like this, trying to catch the encoded forward-slash that is always present in the Computer\r\nName/Username string (encoded as a comma).  \r\nFigure 16: Beacon rule attempting to catch forward-slash in Computer Name/Username string \r\nRecall that the implant will: \r\n1. Connect to the server \r\n2. Send a string length (4 bytes) \r\n3. Send the PC/User string N bytes \r\n4. Read 4 bytes back to ensure a response \r\n5. Read 4-byte command ID and N command data bytes \r\n6. Start sending command responses \r\nAs Snort is reading data over the wire, it is interpreting it and sorting it into different buffers (pkt_data, file_data,\r\njs_data, http_*, etc.). In this case, as TCP data is being chunked along the wire, Snort is looking at those\r\nindividual TCP segments. Only after it has enough data will it flush into the larger \"TCP stream\" buffer so a rule\r\ncan parse the entire stream sent from a client or server. \r\nInitially, the get command traffic was alerting while the put command traffic was not. Fortunately, Snort 3 comes\r\nwith a tracing module to help debug these issues. The buffer option will print out Snort’s different buffers as they\r\nare filled and rule_eval will trace the rule as it is evaluated. The following screenshots are output from individual\r\nruns of Snort against each PCAP. “snort.raw” represents an individual packet, while “snort.stream_tcp” represents\r\na reassembled TCP stream. \r\nAt the start of the working GetFile command, the beacon size and data can be seen as two separate packets (Figure\r\n17).  \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 8 of 14\n\nFigure 17: Individual beacon packets being processed by Snort \r\nFurther down, the reassembled TCP stream can be seen being inspected and alerted on. Moving from the top to\r\nbottom in Figure 18, the cursor position and state of the buffer can be observed changing as the rule is evaluated.\r\nAt the end, the flowbit is set and made available for the command rule.\r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 9 of 14\n\nFigure 18: Snort trace output setting flowbit for BugSleep beacon \r\nFurther down, the TCP stream for the command data is processed. The higher-order zeroes of the command are\r\nfound, the flowbit checked, the PCRE performed, and the SID alerts as expected.  \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 10 of 14\n\nFigure 19: Get file command rule alerts on traffic as expected \r\nWhen the results of the put file command traffic are inspected, a different behavior is observed. The individual\r\npackets for beacon length and beacon data are seen coming in; however, the first reassembled TCP stream that\r\nSnort is inspecting is the command being sent back to the implant. Figure 20 shows the command ID being found\r\nand then the flowbit check failing. \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 11 of 14\n\nFigure 20: Put file command traffic failing flowbit check \r\nScrolling further in the log reveals the TCP stream for the beacon data is eventually populated and Snort sets the\r\nflowbit as expected. The stream for the command ID, however, has already passed and failed analysis because of\r\nthe unset flowbit, resulting in no alert. The cause of this issue is the raw packets coming from the client not being\r\nreassembled into a TCP stream by the time the server packets are reassembled and inspected. This happens\r\nbecause Snort only reassembles when it has enough data, and 20 bytes is not enough yet. \r\nThe fix \r\nUnfortunately, the beacon rule must be tweaked so it can alert as soon as possible and not rely on the TCP\r\nreassembly. Recall that the beacon function invokes SendSocket twice, once for 4-length bytes and again for the\r\nbeacon data. This means the first packet Snort sees will only be 4 bytes long. Adding “bufferlen:=4” restricts Snort\r\nto only look at 4-byte packets, significantly reducing any FP rate. Our solution ended up being this:  \r\nFigure 21: Fixed beacon rule looking for 4-byte length segments \r\n Now the rules work as expected!  \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 12 of 14\n\nFigure 22: Snort output alerting on traffic from all BugSleep commands \r\nConclusion \r\nSince BugSleep is a new implant and weekly releases were observed being deployed, this protocol might change\r\nand bypass these rules. However, two things have been accomplished: \r\n1. This variant will no longer communicate over our customers’ networks. \r\n2. Attackers must invest development time and money to use BugSleep again. \r\nThe published Snort SIDs covering this traffic are 63937 and 63938.  \r\nIndicators of compromise \r\nIOCs for this research can be found in our GitHub repository here. \r\nHosts: \r\n1[.]235[.]234[.]202 \r\n146[.]19[.]143[.]14 \r\n46[.]19[.]143[.]14 \r\n5[.]239[.]61[.]97 \r\nHashes \r\nThe following Windows executables were collected during our research. Assuming these have not been\r\nmanipulated, the compilation time for this set of binaries indicates weekly releases of BugSleep. \r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 13 of 14\n\nSHA256  Compile Time \r\nb8703744744555ad841f922995cef5dbca11da22565195d05529f5f9095fbfca \r\nWed., May 8 00:55:53 2024\r\nUTC \r\n94278fa01900fdbfb58d2e373895c045c69c01915edc5349cd6f3e5b7130c472 \r\nWed., May 22 21:56:39\r\n2024 UTC \r\n73c677dd3b264e7eb80e26e78ac9df1dba30915b5ce3b1bc1c83db52b9c6b30e \r\nFri., May 31 23:29:21 2024\r\nUTC \r\n5df724c220aed7b4878a2a557502a5cefee736406e25ca48ca11a70608f3a1c0 \r\nSun., Jul 07 21:09:49 2024\r\nUTC \r\n960d4c9e79e751be6cad470e4f8e1d3a2b11f76f47597df8619ae41c96ba5809 \r\nSat., Jul 15 09:15:20 2079\r\nUTC \r\nSource: https://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nhttps://blog.talosintelligence.com/writing-a-bugsleep-c2-server/\r\nPage 14 of 14\n\nincoming pages. Figure 11: Tricky file pointer math    \nThis translates to each page starting with a 4-byte page number followed by 1020 bytes (or 0x3fc) of file data,\nwhich the GetFile command does not do; it sends full 1024-byte pages of file data without page numbers.\n   Page 5 of 14   \n\nstatic, which byte length of makes it a poor candidate this string. A Computer for a Name/Username fast_pattern; however, string recall that from any given each beacon is victim is unlikely prepended with to be longer a 4- than\n255 characters. This means most length fields are going to look like |XX 00 00 00| or |XX FD FD FD| when\nencoded. This could be a quick match, early in the stream, at a static offset, making it a decent fast_pattern\ncandidate.      \n   Page 6 of 14",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://blog.talosintelligence.com/writing-a-bugsleep-c2-server/"
	],
	"report_names": [
		"writing-a-bugsleep-c2-server"
	],
	"threat_actors": [],
	"ts_created_at": 1775434255,
	"ts_updated_at": 1775826727,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/a9fb8f2df91ffd8c5b23745dd58543318b6d1a29.pdf",
		"text": "https://archive.orkl.eu/a9fb8f2df91ffd8c5b23745dd58543318b6d1a29.txt",
		"img": "https://archive.orkl.eu/a9fb8f2df91ffd8c5b23745dd58543318b6d1a29.jpg"
	}
}