Encrypted Cobalt Strike C2 traffic can be obfuscated with malleable C2 data transforms. We show how to deobfuscate such traffic.
This series of blog posts describes different methods to decrypt Cobalt Strike traffic. In part 1 of this series, we revealed private encryption keys found in rogue Cobalt Strike packages. In part 2, we decrypted Cobalt Strike traffic starting with a private RSA key. And in part 3, we explain how to decrypt Cobalt Strike traffic if you don’t know the private RSA key but do have a process memory dump.
In the first 3 parts of this series, we have always looked at traffic that contains the unaltered, encrypted data: the data returned for a query and the data posted, was just the encrypted data.
But how do we know if a beacon is using such instructions to obfuscate traffic, or not? This can be seen in the analysis results of the latest version of tool 1768.py. Let’s take a look at the configuration of the beacon we started with in part 1:
We see for field 0x000b (malleable C2 instructions) that there is just one instruction: Print. This is the default, and it means that the encrypted data is received as-is by the beacon: it does not need any transformation prior to decryption.
And for field 0x000d (http post header), we see that the Build Output is also just one instruction: Print. This is the default, and it means that the encrypted data is transmitted as-is by the beacon: it does not need any transformation after encryption.
Let’s take a look at a sample with custom malleable C2 data transforms:
Here we see more than just a Print instruction: “Remove 1522 bytes from end”, “Remove 84 bytes from begin”, …
These are instructions to transform (deobfuscate) the incoming traffic, so that it can then be decrypted. To understand in detail how this works, we will do the transformation manually with CyberChef. However, do know that tool cs-parse-http-traffic.py can do these transformations automatically.
This is the network capture for a single GET request by the beacon and reply from the team server (C2):
We copy this reply over to CyberChef in its input field:
The instructions we need to follow, to deobfuscate this reply, are listed in tool 1768.py’s output:
So let’s get started. First we need to remove 1522 bytes from the end of the reply. This can be done with a CyberChef drop bytes function and a negative length (negative length means dropping from the end):
Then, we need to remove 84 bytes from the beginning of the reply:
And then also dropping 3931 bytes from the beginning:
And now we end up with output that looks like BASE64 encoded data. Indeed, the next instruction is to apply a BASE64 decoding instructions (to be precise: BASE64 encoding for URLs):
The next instruction is to XOR the data. To do that we need the XOR key. The malleable C2 instruction to XOR, uses a 4-byte long random key, that is prepended to the XORed data. So to recover this key, we convert the binary output to hexadecimal:
The first 4 bytes are the XOR key: b7 85 71 17
We use that with CyberChef’s XOR command:
Notice that the first 4 bytes are NULL bytes now: that is as expected, XORing bytes with themselves gives NULL bytes.
And finally, we drop these 4 NULL bytes:
What we end up with, is the encrypted data that contains the C2 commands to be executed by the beacon. This is the result of deobfuscating the data by following the malleable C2 data transform. Now we can proceed with the decryption using a process memory dump, just like we did in part 3.
Tool cs-extract-key.py is used to extract the AES and HMAC key from process memory: it fails, it is not able to find the keys in process memory.
One possible explanation that the keys can not be found, is that process memory is encoded. Cobalt Strike supports a feature for beacons, called a sleep mask. When this feature is enabled, the process memory with data of a beacon (including the keys) is XOR-encoded while a beacon sleeps. Thus only when a beacon is active (communicating or executing commands) will its data be in cleartext.
We can try to decode this process memory dump. Tool cs-analyze-processdump.py is a tool that tries to decode a process memory dump of a beacon that has an active sleep mask feature. Let’s run it on our process memory dump:
The tool has indeed found a 13-byte long XOR key, and written the decoded section to disk as a file with extension .bin.
This file can now be used with cs-extract-key.py, it’s exactly the same command as before, but with the decoded section in stead of the encoded .dmp file:
And now we have recovered the cryptographic keys.
Notice that in figure 16, the tool reports finding string sha256\x00, while in the first command (figure 13), this string is not found. The absence of this string is often a good indicator that the beacon uses a sleep mask, and that tool cs-analyze-processdump.py should be used prior to extracting the keys.
Now that we have the keys, we can decrypt the network traffic with tool cs-parse-http-traffic.py:
This fails: the reason is the malleable C2 data transform. Tool cs-parse-http-traffic.py needs to know which instructions to apply to deobfuscate the traffic prior to decryption. Just like we did manually with CyberChef, tool cs-parse-http-traffic.py needs to do this automatically. This can be done with option -t.
Notice that the output of tool 1768.py contains a short-hand notation of the instructions to execute (between square brackets):
For the tasks to be executed (input), it is:
And for the results to be posted (output), it is:
These instructions can be put together (using a semicolon as separator) and fed via option -t to tool cs-parse-http-traffic.py:
And now we finally obtain decrypted traffic. There are no actual commands here in this traffic, just “data jitter”: that is random data of random length, designed to even more obfuscate traffic.
We saw how malleable C2 data transforms are used to obfuscate network traffic, and how we can deobfuscate this network traffic by following the instructions.
We did this manually with CyberChef, but that is of course not practical (we did this to illustrate the concept). To obtain the decoded, encrypted commands, we can also use cs-parse-http-traffic.py. Just like we did in part 3, where we started with an unknown key, we do this here too. The only difference, is that we also need to provide the decoding instructions:
And then we can take one of these 3 encrypted data, to recover the keys.
Thus the procedure is exactly the same as explained in part 3, except that option -t must be used to include the malleable C2 data transforms.
About the authors
Didier Stevens is a malware expert working for NVISO. Didier is a SANS Internet Storm Center senior handler and Microsoft MVP, and has developed numerous popular tools to assist with malware analysis. You can find Didier on Twitter and LinkedIn.
You can follow NVISO Labs on Twitter to stay up to date on all our future research and publications.