Last Updated: 2021-11-06 00:05:33 UTC
by Didier Stevens (Version: 1)
In diary entry "Decrypting Cobalt Strike Traffic With a "Leaked" Private Key" I showed how to decrypt Cobalt Strike network traffic with private RSA keys.
In this diary entry, I will show how to decrypt Cobalt Strike network traffic with AES keys extracted from the beacon's process memory.
Inside a sandbox, I start the beacon and let it communicate with the C2 while I capture network traffic. And I make a process memory dump of the beacon process.
Analyzing the beacon with my tool 1768.py shows that it communicates over HTTP and that it uses a public RSA key for which no private keys have been leaked. Starting with version 0.0.9 of my tool 1768.py, I also parse the malleable instructions that are used to transform data transmitted and received by the beacon. In this case, the input and output instructions are just a print instruction. This means that the incoming, encrypted data requires no further transformations, and neither does the outgoing, encrypted data:
First I need to extract encryption keys from the process memory dump I made from a running beacon (using procdump). I do this with my tools cs-extract-key.py:
No keys are found: this means that the beacon is most like a version 4.x beacon, and that encrypted data is necessary to help identify encryption keys.
I can extract encrypted data from my capture file using my tool cs-parse-http-traffic.py. Using option -k with value unknown, means that I don't know the decryption keys and that the tool should extract the encrypted data from the capture file, like this:
Two packets with encrypted data were found: one is the reply to a GET request (packet 222) and the other is the data of a POST request (packet 229). The received data (packet 222) contains encrypted instructions (tasks), and the transmitted data (packet 229) contains the encrypted output (callbacks) of the tasks. Although incoming and outgoing data is encrypted using the same AES key and method, there is a slight difference: outgoing data is preceded by a 32-bit integer that specifies the lenght of the encrypted data. That's why you see some 00 bytes at the beginning of the posted, encrypted data.
What my tool cs-extract-key.py can do, is take encrypted data and a process memory dump, and then try out all 16-byte long, non-null sequences it finds in memory, as a key. There are 2 keys in memory: one AES key, and one HMAC key.
Here I provide the encrypted data for tasks as option. I use option -t (task) to do this:
My tool finds an HMAC key that validates the signature of the encrypted data: 5aa2e0ea3a612d37c066cc163de40eb4.
And it also finds an AES key that decrypts the data: 83c5ea41fe1913bc0a7f95aa32f3bb69.
Both these keys are derived from the SHA256 of the raw key. The HMAC key is the first half of the SHA256 value, and the AES key is the second half of the SHA256 value (HMAC:AES): 5aa2e0ea3a612d37c066cc163de40eb4:83c5ea41fe1913bc0a7f95aa32f3bb69.
And then I can use this key and my tool cs-parse-http-traffic.py to decrypt the captured traffic:
And now we can see that the command sent by the operator was a whoami command, and that the reply by the beacon was the username (output of whoami command).
I obtained the key via the encrypted task data. Remark that I can also try to recover the keys via the encrypted callback data. Since there is a slight difference in data format, another option of cs-parse-http-traffic.py must be used, namely option -c (callback), like this:
As expected, the same HMAC and AES keys were found and extracted.