🔒
There are new articles available, click to refresh the page.
Today — 29 November 2021Research - Companies

Cobalt Strike: Decrypting DNS Traffic – Part 5

29 November 2021 at 11:14

Cobalt Strike beacons can communicate over DNS. We show how to decode and decrypt DNS traffic in this blog post.

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. 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. And in part 4, we deal with traffic obfuscated with malleable C2 data transforms.

In the first 4 parts of this series, we have always looked at traffic over HTTP (or HTTPS). A beacon can also be configured to communicate over DNS, by performing DNS requests for A, AAAA and/or TXT records. Data flowing from the beacon to the team server is encoded with hexadecimal digits that make up labels of the queried name, and data flowing from the team server to the beacon is contained in the answers of A, AAAA and/or TXT records.

The data needs to be extracted from DNS queries, and then it can be decrypted (with the same cryptographic methods as for traffic over HTTP).

DNS C2 protocol

We use a challenge from the 2021 edition of the Cyber Security Rumble to illustrate how Cobalt Strike DNS traffic looks like.

First we need to take a look at the beacon configuration with tool 1768.py:

Figure 1: configuration of a DNS beacon

Field “payload type” confirms that this is a DNS beacon, and the field “server” tells us what domain is used for the DNS queries: wallet[.]thedarkestside[.]org.

And then a third block of DNS configuration parameters is highlighted in figure 1: maxdns, DNS_idle, … We will explain them when they appear in the DNS traffic we are going to analyze.

Seen in Wireshark, that DNS traffic looks like this:

Figure 2: Wireshark view of Cobalt Strike DNS traffic

We condensed this information (field Info) into this textual representation of DNS queries and replies:

Figure 3: Textual representation of Cobalt Strike DNS traffic

Let’s start with the first set of queries:

Figure 4: DNS_beacon queries and replies

At regular intervals (determined by the sleep settings), the beacon issues an A record DNS query for name 19997cf2[.]wallet[.]thedarkestside[.]org. wallet[.]thedarkestside[.]org are the root labels of every query that this beacon will issue, and this is set inside the config. 19997cf2 is the hexadecimal representation of the beacon ID (bid) of this particular beacon instance. Each running beacon generates a 32-bit number, that is used to identify the beacon with the team server. It is different for each running beacon, even when the same beacon executable is started several times. All DNS request for this particular beacon, will have root labels 19997cf2[.]wallet[.]thedarkestside[.]org.

To determine the purpose of a set of DNS queries like above, we need to consult the configuration of the beacon:

Figure 5: zooming in on the DNS settings of the configuration of this beacon (Figure 1)

The following settings define the top label per type of query:

  1. DNS_beacon
  2. DNS_A
  3. DNS_AAAA
  4. DNS_TXT
  5. DNS_metadata
  6. DNS_output

Notice that the values seen in figure 5 for these settings, are the default Cobalt Strike profile settings.

For example, if DNS queries issued by this beacon have a name starting with http://www., then we know that these are queries to send the metadata to the team server.

In the configuration of our beacon, the value of DNS_beacon is (NULL …): that’s an empty string, and it means that no label is put in front of the root labels. Thus, with this, we know that queries with name 19997cf2[.]wallet[.]thedarkestside[.]org are DNS_beacon queries. DNS_beacon queries is what a beacon uses to inquire if the team server has tasks for the beacon in its queue. The reply to this A record DNS query is an IPv4 address, and that address instructs the beacon what to do. To understand what the instruction is, we first need to XOR this replied address with the value of setting DNS_Idle. In our beacon, that DNS_Idle value is 8.8.4.4 (the default DNS_Idle value is 0.0.0.0).

Looking at figure 4, we see that the replies to the first requests are 8.8.4.4. These have to be XORed with DNS_Idle value 8.8.4.4: thus the result is 0.0.0.0. A reply equal to 0.0.0.0 means that there are no tasks inside the team server queue for this beacon, and that it should sleep and check again later. So for the first 5 queries in figure 4, the beacon has to do nothing.

That changes with the 6th query: the reply is IPv4 address 8.8.4.246, and when we XOR that value with 8.8.4.4, we end up with 0.0.0.242. Value 0.0.0.242 instructs the beacon to check for tasks using TXT record queries.

Here are the possible values that determine how a beacon should interact with the team server:

Figure 6: possible DNS_Beacon replies

If the least significant bit is set, the beacon should do a checkin (with a DNS_metadata query).

If bits 4 to 2 are cleared, communication should be done with A records.

If bit 2 is set, communication should be done with TXT records.

And if bit 3 is set, communication should be done with AAAA records.

Value 242 is 11110010, thus no checkin has to be performed but tasks should be retrieved via TXT records.

The next set of DNS queries are performed by the beacon because of the instructions (0.0.0.242) it received:

Figure 7: DNS_TXT queries

Notice that the names in these queries start with api., thus they are DNS_TXT queries, according to the configuration (see figure 5). And that is per the instruction of the team server (0.0.0.242).

Although DNS_TXT queries should use TXT records, the very first DNS query of a DNS_TXT query is an A record query. The reply, an IPv4 address, has to be XORed with the DNS_Idle value. So here in our example, 8.8.4.68 XORed with 8.8.4.4 gives 0.0.0.64. This specifies the length (64 bytes) of the encrypted data that will be transmitted over TXT records. Notice that for DNS_A and DNS_AAAA queries, the first query will be an A record query too. It also encodes the length of the encrypted data to be received.

Next the beacon issues as many TXT record queries as necessary. The value of each TXT record is a BASE64 string, that has to be concatenated together before decoding. The beacon stops issuing TXT record requests once the decoded data has reached the length specified in the A record reply (64 bytes in our example).

Since the beacon can issue these TXT record queries very quickly (depending on the sleep settings), a mechanism is introduced to avoid that cached DNS results can interfere in the communication. This is done by making each name in the DNS queries unique. This is done with an extra hexadecimal label.

Notice that there is an hexadecimal label between the top label (api in our example) and the root labels (19997cf2[.]wallet[.]thedarkestside[.]org in our example). That hexadecimal label is 07311917 for the first DNS query and 17311917 for the second DNS query. That hexadecimal label consists of a counter and a random number: COUNTER + RANDOMNUMBER.

In our example, the random number is 7311917, and the counter always starts with 0 and increments with 1. That is how each query is made unique, and it also helps to process the replies in the correct order, in case the DNS replies arrive in disorder.

Thus, when all the DNS TXT replies have been received (there is only one in our example), the base 64 string (ZUZBozZmBi10KvISBcqS0nxp32b7h6WxUBw4n70cOLP13eN7PgcnUVOWdO+tDCbeElzdrp0b0N5DIEhB7eQ9Yg== in our example) is decoded and decrypted (we will do this with a tool at the end of this blog post).

This is how DNS beacons receive their instructions (tasks) from the team server. The encrypted bytes are transmitted via DNS A, DNS AAAA or DNS TXT record replies.

When the communication has to be done over DNS A records (0.0.0.240 reply), the traffic looks like this:

Figure 8: DNS_A queries

cdn. is the top label for DNS_A requests (see config figure 5).

The first reply is 8.8.4.116, XORed with 8.8.4.4, this gives 0.0.0.112. Thus 112 bytes of encrypted data have to be received.: that’s 112 / 4 = 28 DNS A record replies.

The encrypted data is just taken from the IPv4 addresses in the DNS A record replies. In our example, that’s: 19, 64, 240, 89, 241, 225, …

And for DNS_AAAA queries, the method is exactly the same, except that the top label is www6. in our example (see config figure 5) and that each IPv6 address contains 16 bytes of encrypted data.

The encrypted data transmitted via DNS records from the team server to the beacon (e.g., the tasks) has exactly the same format as the encrypted tasks transmitted with http or https. Thus the decryption process is exactly the same.

When the beacon has to transmit its results (output of the tasks) to the team server, is uses DNS_output queries. In our example, these queries start with top label post. Here is an example:

Figure 9: beacon sending results to the team server with DNS_output queries

Each name of a DNS query for a DNS_output query, has a unique hexadecimal counter, just like DNS_A, DNS_AAAA and DNS_TXT queries. The data to be transmitted, is encoded with hexadecimal digits in labels that are added to the name.

Let’s take the first DNS query (figure 9): post.140.09842910.19997cf2[.]wallet[.]thedarkestside.org.

This name breaks down into the following labels:

  • post: DNS_output query
  • 140: transmitted data
  • 09842910: counter + random number
  • 19997cf2: beacon ID
  • wallet[.]thedarkestside.org: domain chosen by the operator

The transmitted data of the first query is actually the length of the encrypted data to be transmitted. It has to be decoded as follows: 140 -> 1 40.

The first hexadecimal digit (1 in our example) is a counter that specifies the number of labels that are used to contain the hexadecimal data. Since a DNS label is limited to 63 characters, more than one label needs to be used when 32 bytes or more need to be encoded. That explains the use of a counter. 40 is the hexadecimal data, thus the length of the encrypted data is 64 bytes long.

The second DNS query (figure 9) is: post.2942880f933a45cf2d048b0c14917493df0cd10a0de26ea103d0eb1b3.4adf28c63a97deb5cbe4e20b26902d1ef427957323967835f7d18a42.19842910.19997cf2[.]wallet[.]thedarkestside[.]org.

The name in this query contains the encrypted data (partially) encoded with hexadecimal digits inside labels.

These are the transmitted data labels: 2942880f933a45cf2d048b0c14917493df0cd10a0de26ea103d0eb1b3.4adf28c63a97deb5cbe4e20b26902d1ef427957323967835f7d18a42

The first digit, 2, indicates that 2 labels were used to encode the encrypted data: 942880f933a45cf2d048b0c14917493df0cd10a0de26ea103d0eb1b3 and 4adf28c63a97deb5cbe4e20b26902d1ef427957323967835f7d18a42.

The third DNS query (figure 9) is: post.1debfa06ab4786477.29842910.19997cf2[.]wallet[.]thedarkestside[.]org.

The counter for the labels is 1, and the transmitted data is debfa06ab4786477.

Putting all these labels together in the right order, gives the following hexadecimal data:

942880f933a45cf2d048b0c14917493df0cd10a0de26ea103d0eb1b34adf28c63a97deb5cbe4e20b26902d1ef427957323967835f7d18a42debfa06ab4786477. That’s 128 hexadecimal digits long, or 64 bytes, exactly like specified by the length (40 hexadecimal) in the first query.

The hexadecimal data above, is the encrypted data transmitted via DNS records from the beacon to the team server (e.g., the task results or output) and it has almost the same format as the encrypted output transmitted with http or https. The difference is the following: with http or https traffic, the format starts with an unencrypted size field (size of the encrypted data). That size field is not present in the format of the DNS_output data.

Decryption

We have developed a tool, cs-parse-traffic, that can decrypt and parse DNS traffic and HTTP(S). Similar to what we did with encrypted HTTP traffic, we will decode encrypted data from DNS queries, use it to find cryptographic keys inside the beacon’s process memory, and then decrypt the DNS traffic.

First we run the tool with an unknown key (-k unknown) to extract the encrypted data from the DNS queries and replies in the capture file:

Figure 10: extracting encrypted data from DNS queries

Option -f dns is required to process DNS traffic, and option -i 8.8.4.4. is used to provided the DNS_Idle value. This value is needed to properly decode DNS replies (it is not needed for DNS queries).

The encrypted data (red rectangle) can then be used to find the AES and HMAC keys inside the process memory dump of the running beacon:

Figure 11: extracting cryptographic keys from process memory

That key can then be used to decrypt the DNS traffic:

Figure 12: decrypting DNS traffic

This traffic was used in a CTF challenge of the Cyber Security Rumble 2021. To find the flag, grep for CSR in the decrypted traffic:

Figure 13: finding the flag inside the decrypted traffic

Conclusion

The major difference between DNS Cobalt Strike traffic and HTTP Cobalt Strike traffic, is how the encrypted data is encoded. Once encrypted data is recovered, decrypting it is very similar for DNS and HTTP.

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.

Before yesterdayResearch - Companies

Cobalt Strike: Decrypting Obfuscated Traffic – Part 4

17 November 2021 at 08:42

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.

This encrypted data can be transformed into traffic that looks more benign, using malleable C2 data transforms. In the example we will look at in this blog post, the encrypted data is hidden inside JavaScript code.

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:

Figure 1: beacon with default malleable C2 instructions

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:

Figure 2: beacon with custom malleable C2 instructions

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):

Figure 3: reply transformed with malleable C2 instructions to look like JavaScript code

What we see here, is a GET request by the beacon to the C2 (notice the Cookie with the encrypted metadata) and the reply by the C2. This reply looks like JavaScript code, because of the malleable C2 data transforms that have been used to make it look like JavaScript code.

We copy this reply over to CyberChef in its input field:

Figure 4: CyberChef with obfuscated input

The instructions we need to follow, to deobfuscate this reply, are listed in tool 1768.py’s output:

Figure 5: decoding instructions

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):

Figure 6: dropping 1522 bytes from the end

Then, we need to remove 84 bytes from the beginning of the reply:

Figure 7: dropping 84 bytes from the beginning

And then also dropping 3931 bytes from the beginning:

Figure 8: 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):

Figure 9: decoding BASE64/URL data

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:

Figure 10: hexadecimal representation of the transformed data

The first 4 bytes are the XOR key: b7 85 71 17

We use that with CyberChef’s XOR command:

Figure 11: XORed data

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:

Figure 12: fully transformed data

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.

Figure 13: extracting the cryptographic keys from process memory

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:

Figure 14: analyzing the process memory dump (screenshot 1)
Figure 15: analyzing the process memory dump (screenshot 2)

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:

Figure 16: extracting keys from the decoded section

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:

Figure 17: decrypting the traffic fails

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):

Figure 18: short-hand notations of malleable C2 instructions

For the tasks to be executed (input), it is:

7:Input,4,1:1522,2:84,2:3931,13,15

And for the results to be posted (output), it is:

7:Output,15,13,4

These instructions can be put together (using a semicolon as separator) and fed via option -t to tool cs-parse-http-traffic.py:

Figure 19: decrypted traffic

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.

Conclusion

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:

Figure 20: extracting and decoding the encrypted data

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.

Detecting DCSync and DCShadow Network Traffic

15 November 2021 at 08:30

This blog post on detecting Mimikatz’ DCSync and DCShadow network traffic, accompanies SANS webinar “Detecting DCSync and DCShadow Network Traffic“.

Intro

Mimikatz provides two commands to interact with a Windows Domain Controller and extract or alter data from the Active Directory database.

These two commands are dcsync and dcshadow.

The dcsync command can be used, on any Windows machine, to connect to a domain controller and read data from AD, like dumping all credentials. This is not an exploit or privilege escalation, the necessary credentials are required to be able to do this, for example a golden ticket.

The dcshadow command can be used, on any Windows machine, to connect to a domain controller and write data to AD, like changing a password or adding a user. This too is not an exploit or privilege escalation: proper domain admin credentials are necessary to achieve this.

Both commands rely on the active directory data replication protocol: Directory Replication Service (DRS). This is a protocol (MSRPC / DCE/RPC based) that domain controllers use to replicate their AD database changes between them. The Microsoft API for DRS is DRSUAPI.

Such traffic should only occur between domain controllers. When DRS traffic is detected between a DC and a non-DC (a user workstation for example), alarms should go of.

Alerting

An Intrusion Detection System can detect DRSUAPI traffic with proper rules.

Figure 1: IDS inspecting traffic between workstation and DC

The IDS needs to be positioned inside the network, at a location where traffic between domain controllers and non-domain controllers can be inspected.

DCE/RPC traffic is complex to parse properly. For example, remote procedure calls are done with an integer that identifies the procedure to call. The name of the function, represented as a string for example, is not used in the DCE/RPC protocol. Furthermore, function integers are only unique within an API: for example, function 0 is the DsBind function in the DRSUAPI function, but function 0 is also the DSAPrepareScript in the DSAOP interface.

A very abstract view of such traffic, can be represented like this:

Figure 2: abstraction of DCE/RPC traffic

If an IDS would just see or inspect packet B, it would not be able to determine which function is called. Sure, it is function 0, but for which API? Is it DsBind in the DRSUAPI API or is is DSAPrepareScript in the DSAOP interface? Or another one …

So, the IDS needs to keep track of the interfaces that are requested, and then it can correctly determine which functions are requested.

Alerting dcsync

Here is captured dcsync network traffic, visualized with Wireshark (dcerpc display filter):

Figure 3: DCSync network traffic

Frame 28 is our packet A: requesting the DRSUAPI interface

Frame 41 is our packet B: requesting function DsGetNCChanges

Notice that these packets do belong to the same TCP connection (stream 4).

Thus, a rule would be required, that triggers on two different packets. This is not possible in Snort/Suricata: simple rules inspect only one packet.

What is typically done in Suricata for such cases, is to make two rules: one for packet A and one for packet B. And an alert is only generated when rule B triggers after rule A triggers.

This can be done with a flowbit. A flowbit is literally a bit kept in memory by Suricata, that can be set or cleared.

These bits are linked to a flow. Simply put, a flow is a set of packets between the same client and server. It’s more generic than a connection.

Thus, what needs to be done to detect dcsync traffic using a flowbit, is to have two rules:

  1. Rule 1: detect packet of type A and set flowbit
  2. Rule 2: detect packet of type B and alert if flowbit is set

Suricata rules that implement such a detection, look like this:

alert tcp $WORKSTATIONS any -> $DCS any (
msg:"Mimikatz DRSUAPI"; 
flow:established,to_server; 
content:"|05 00 0b|"; depth:3; 
content:"|35 42 51 e3 06 4b d1 11 ab 04 00 c0 4f c2 dc d2|"; depth:100; 
flowbits:set,drsuapi; 
flowbits:noalert; 
reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000010; rev:1;)

alert tcp $WORKSTATIONS any -> $DCS any (
msg:"Mimikatz DRSUAPI DsGetNCChanges Request";
flow:established,to_server;
flowbits:isset,drsuapi; 
content:"|05 00 00|"; depth:3; 
content:"|03 00|"; offset:22; depth:2;
reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000011; rev:1;)

The first rule (Mimikatz DRSUAPI) is designed to identify a DCERPC Bind to the DRSUAPI API. The packet data has to start with 05 00 0B:

Figure 4: DCERPC packet header

5 is the major version of the protocol, 0 is the minor version, and 0B (11 decimal) is a Bind request.

A UUID is used to identify the DRSUAPI interface (this is not done with a string like DRSUAPI, but with a UUID that uniquely identifies the DRSUAPI interface):

Figure 5: DRSUAPI UUID

The UUID for DRSUAPI is

e3514235-4b06-11d1-ab04-00c04fc2dcd2

In network packet format, it is

35 42 51 e3 06 4b d1 11 ab 04 00 c0 4f c2 dc d2.

When both content clauses are true, the rule triggers. The action that is triggered, is setting a flowbit named drsuapi:

flowbits:set,drsuapi;

A second action, is to prevent the rule from generating an alert when setting this flowbit:

flowbits:noalert;

This explains the first rule.

The second rule (Mimikatz DRSUAPI DsGetNCChanges Request) is designed to detect packets with a DRSUAPI request for function DsGetNCChanges. The packet data has to start with 05 00 00:

Figure 6: DRSUAPI Request

5 is the major version of the protocol, 0 is the minor version, and 00 is an RPC request.

And further down in the packet data (position 22 to be precise) the number of the function is specified:

Figure 6: DRSUAPI DsGetNCChanges

Number 3 is DRSUAPI function DsGetNCChanges.

When flowbit drsuapi is set and both content clauses are true, the rule triggers.

flowbits:isset,drsuapi;

And an alert is generated.

Notice that the rule names contain the word Mimikatz, but these rules are not specific to Mimikatz: they will also trigger on regular replication traffic between DCs. The key to use these rules properly, is to make them inspect network traffic between domain controllers and non-domain controllers. Replication traffic should only occur between DCs.

Alerting dcshadow

Mimikatz dcshadow command also generates DRSUAPI network traffic, and the rules defined for dcsync also trigger on dcshadow traffic.

One lab in SANS training SEC599, Defeating Advanced Adversaries – Purple Team Tactics & Kill Chain Defenses, covers dcsync and its network traffic detection. If you take this training, you can also try out dcshadow in this lab.

The dcshadow command requires two instances of Mimikatz to run. First, one running as system to setup the RPC server:

Figure 7: first instance of Mimikatz for dcshadow (screenshot a)
Figure 8: first instance of Mimikatz for dcshadow (screenshot b)

And a second one running as domain admin to start the replication:

Figure 9: second instance of Mimikatz for dcshadow

This push instruction starts the replication:

Figure 10: second instance of Mimikatz for dcshadow (screenshot b)

dcshadow network traffic looks like this in Wireshark (dcerpc display filter):

Figure 11: dcshadow network traffic

Notice the DRSUAPI bind requests and the DsGetNCChanges requests -> these will trigger the dcsync rules.

DRSUAPI_REPLICA_ADD is also an interesting function to detect: it adds a replication source. The integer that identifies this function is 5.

Figure 12: DRSUAPI_REPLICA_ADD

A rule to detect this function can be created based on the rule to detect DsGetNCChanges.

What needs to be changed:

  1. The opnum: 03 00 -> 05 00
  2. The rule number, sid:1000011 -> sid:1000014 (for example)
  3. And the rule message (preferably): “Mimikatz DRSUAPI DsGetNCChanges Request” -> “Mimikatz DRSUAPI DRSUAPI_REPLICA_ADD Request”
alert tcp $WORKSTATIONS any -> $DCS any (
msg:"Mimikatz Mimikatz DRSUAPI DRSUAPI_REPLICA_ADD Request";
flow:established,to_server;
flowbits:isset,drsuapi; 
content:"|05 00 00|"; depth:3; 
content:"|05 00|"; offset:22; depth:2;
reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000014; rev:1;)

More generic rules

It is also possible to change the flowbit setting rule (rule for packet A), to generate alerts. This is done by removing the following clause:

flowbits:noalert;

Alerts are generated whenever the DRSUAPI interface is bound to, regardless of which function is called.

And a generic rule for a DRSUAPI function call can also be created, by removing the following clause from the DsGetNCChanges rule (for example):

content:”|03 00|”; offset:22; depth:2;

Byte order and DCEPRC

DCERPC is a flexible protocol, that allows different byte orders. A byte order, is the order in which bytes are transmitted over the network. When dealing with integers that are encoded using more than one byte, for example, different orders are possible.

The opnum detected in the dcsync rule, is 3. This integer is encoded with 2 bytes: a most significant byte (00) and a least significant byte (03).

When the byte order is little-endian, the least significant byte (03) is transmitted first, followed by the most significant byte (00). This is what is present in the captured network traffic.

But when the byte order is big-endian, the most significant byte (00) is transmitted first, followed by the least significant byte (03).

And thus, the rules would not trigger for big-endian byte-order.

The byte order is specified by the client in the data representation bytes of the DCERPC packet data:

Figure 13: DCERPC data representation

If the first nibble of the first byte of the data representation is one, the byte order is little-endian.

Big-endian is encoded with nibble value zero.

We have developed rules that check the byte-order, and match the opnum value accordingly:

alert tcp $WORKSTATIONS any -> $DCS any (msg:"Mimikatz DRSUAPI DsGetNCChanges Request"; flow:established,to_server; flowbits:isset,drsuapi; content:"|05 00 00|"; depth:3; byte_test:1,>=,0x10,4; byte_test:1,<=,0x11,4; content:"|03 00|"; offset:22; depth:2; reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000012; rev:1;)
alert tcp $WORKSTATIONS any -> $DCS any (msg:"Mimikatz DRSUAPI DsGetNCChanges Request"; flow:established,to_server; flowbits:isset,drsuapi; content:"|05 00 00|"; depth:3; byte_test:1,>=,0x00,4; byte_test:1,<=,0x01,4; content:"|00 03|"; offset:22; depth:2; reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000013; rev:1;)

alert tcp $WORKSTATIONS any -> $DCS any (msg:"Mimikatz DRSUAPI DRSUAPI_REPLICA_ADD Request"; flow:established,to_server; flowbits:isset,drsuapi; content:"|05 00 00|"; depth:3; byte_test:1,>=,0x10,4; byte_test:1,<=,0x11,4; content:"|05 00|"; offset:22; depth:2; reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000015; rev:1;)
alert tcp $WORKSTATIONS any -> $DCS any (msg:"Mimikatz DRSUAPI DRSUAPI_REPLICA_ADD Request"; flow:established,to_server; flowbits:isset,drsuapi; content:"|05 00 00|"; depth:3; byte_test:1,>=,0x00,4; byte_test:1,<=,0x01,4; content:"|00 05|"; offset:22; depth:2; reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000016; rev:1;)

Notice that Suricata and Snort can also be configured to enable the dcerpc preprocessor. This allows for the creation of rules that don’t have to take implementation details into account, like byte-order:

alert dcerpc $WORKSTATIONS any -> $DCS any (msg:"Mimikatz DRSUAPI DsGetNCChanges Request"; flow:established,to_server; dce_iface:e3514235-4b06-11d1-ab04-00c04fc2dcd2; dce_opnum:3; reference:url,blog.didierstevens.com; classtype:policy-violation; sid:1000017; rev:1;)

But such rules can have a significantly higher performance impact, because of the extra processing performed by the dcerpc preprocessor.

Conclusion

In this blog post we show how to detect Active Directory replication network traffic. Such traffic is normal between domain controllers, but it should not be detected between a non-domain controller (like a workstation or a member server) and a domain controller. The presence of unexpected DRS traffic, is a strong indication of an ongoing Active Directory attack, like Mimikatz’ DCSync or DCShadow.

The rules we start with operate at a low network layer level (TCP data), but we show how to develop rules at a higher level, that are more versatile and require less attention to implementation details.

Finally, the rules presented in this blog post are alerting rules for a detection system. But they can easily be modified into blocking rules for a prevention system, by replacing the alert action by a drop or reject action.

All the rules presented here can also be found on our IDS rules Github repository.

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.

Cobalt Strike: Using Process Memory To Decrypt Traffic – Part 3

3 November 2021 at 19:18

We decrypt Cobalt Strike traffic with cryptographic keys extracted from process memory.

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. And in part 2, we decrypted Cobalt Strike traffic starting with a private RSA key. In this blog post, we will explain how to decrypt Cobalt Strike traffic if you don’t know the private RSA key but do have a process memory dump.

Cobalt Strike network traffic can be decrypted with the proper AES and HMAC keys. In part 2, we obtained these keys by decrypting the metadata with the private RSA key. Another way to obtain the AES and HMAC key, is to extract them from the process memory of an active beacon.

One method to produce a process memory dump of a running beacon, is to use Sysinternals’ tool procdump. A full process memory dump is not required, a dump of all writable process memory is sufficient.
Example of a command to produce a process dump of writable process memory: “procdump.exe -mp 1234”, where -mp is the option to dump writable process memory and 1234 is the process ID of the running beacon. The process dump is stored inside a file with extension .dmp.

For Cobalt Strike version 3 beacons, the unencrypted metadata can often be found in memory by searching for byte sequence 0x0000BEEF. This sequence is the header of the unencrypted metadata. The earlier in the lifespan of a process the process dump is taken, the more likely it is to contain the unencrypted metadata.

Figure 1: binary editor view of metadata in process memory

Tool cs-extract-key.py can be used to find and decode this metadata, like this:

Figure 2: extracted and decoded metadata

The metadata contains the raw key: 16 random bytes. The AES and HMAC keys are derived from this raw key by calculating the SHA256 value of the raw key. The first half of the SHA256 value is the HMAC key, and the second half is the AES key.

These keys can then be used to decrypt the captured network traffic with tool cs-parse-http-traffic.py, like explained in Part 2.

Remark that tool cs-extract-key.py is likely to produce false positives: namely byte sequences that start with 0x0000BEEF, but are not actual metadata. This is the case for the example in figure 2: the first instance is indeed valid metadata, as it contains a recognizable machine name and username (look at Field: entries). And the AES and HMAC key extracted from that metadata, have also been found at other positions in process memory. But that is not the case for the second instance (no recognizable names, no AES and HMAC keys found at other locations). And thus that is a false positive that must be ignored.

For Cobalt Strike version 4 beacons, it is very rare that the unencrypted metadata can be recovered from process memory. For these beacons, another method can be followed. The AES and HMAC keys can be found in writable process memory, but there is no header that clearly identifies these keys. They are just 16-byte long sequences, without any distinguishable features. To extract these keys, the method consists of performing a kind of dictionary attack. All possible 16-byte long, non-null sequences found in process memory, will be used to try to decrypt a piece of encrypted C2 communication. If the decryption succeeds, a valid key has been found.

This method does require a process memory dump and encrypted data.
This encrypted data can be extracted using tool cs-parse-http-traffic.py like this: cs-parse-http-traffic.py -k unknown capture.pcapng

With an unknown key (-k unknown), the tool will extract the encrypted data from the capture file, like this:

Figure 3: extracting encrypted data from a capture file

Packet 103 is an HTTP response to a GET request (packet 97). The encrypted data of this response is 64 bytes long: d12c14aa698a6b85a8ed3c3c33774fe79acadd0e95fa88f45b66d8751682db734472b2c9c874ccc70afa426fb2f510654df7042aa7d2384229518f26d1e044bd

This is encrypted data, sent by the team server to the beacon: it contains tasks to be executed by the beacon (remark that in these examples, we look at encrypted traffic that has not been transformed, we will cover traffic transformed by malleable instructions in an upcoming blog post).

We can attempt to decrypt this data by providing tool cs-extract-key.py with the encrypted task (option -t) and the process memory dump: cs-extract-key.py -t d12c14aa698a6b85a8ed3c3c33774fe79acadd0e95fa88f45b66d8751682db734472b2c9c874ccc70afa426fb2f510654df7042aa7d2384229518f26d1e044bd rundll32.exe_211028_205047.dmp.

Figure 4: extracting AES and HMAC keys from process memory

The recovered AES and HMAC key can then be used to decrypt the traffic (-k HMACkey:AESkey):

Figure 5: decrypting traffic with HMAC and AES key provided via option -k

The decrypted tasks seen in figure 5, are “data jitter”. Data jitter is a Cobalt Strike option, that sends random data to the beacon (random data that is ignored by the beacon). With the default Cobalt Strike beacon profile, no random data is sent, and data is not transformed using malleable instructions. This means that with such a beacon profile, no data is sent to the beacon as long as there are no tasks to be performed by the beacon: the Content-length of the HTTP reply is 0.

Since the absence of tasks results in no encrypted data being transmitted, it is quite easy to determine if a beacon received tasks or not, even when the traffic is encrypted. An absence of (encrypted) data means that no tasks were sent. To obfuscate this absence of commands (tasks), Cobalt Strike can be configured to exchange random data, making each packet unique. But in this particular case, that random data is useful to blue teamers: it permits us to recover the cryptographic keys from process memory. If no random data would be sent, nor actual tasks, we would never see encrypted data and thus we would not be able to identify the cryptographic keys inside process memory.

Data sent by the beacon to the team server contains the results of the tasks executed by the beacon. This data is sent with a POST request (default), and is known as a callback. This data too can be used to find decryption keys. In that case, the process is the same as shown above, but the option to use is -c (callback) in stead of -t (tasks). The reason the options are different, is that the way the data is encrypted by the team server is slightly different from the way the data is encrypted by the beacon, and the tool must be told which way to encrypt the data was used.

Some considerations regarding process memory dumps

For a process memory dump of maximum 10MB, the “dictionary” attack will take a couple of minutes.

Full process dumps can be used too, but the dictionary attack can take much longer because of the larger size of the dump. Tool cs-extract-key.py reads the process memory dump as a flat file, and thus a larger file means more processing to be done.

However, we are working on a tool that can parse the data structure of a dump file and extract / decode memory sections that are most likely to contain keys, thus speeding up the key recovery process.

Remark that beacons can be configured to encode their writable memory while they are not active (sleeping): in such cases, the AES and HMAC keys are encoded too, and can not be recovered using the methods described here. The dump parsing tool we are working on will handle this situation too.

Finally, if the method explained here for version 3 beacons does not work with your particular memory dump, try the method for version 4 beacons. This method works also for version 3 beacons.

Conclusion

Cryptographic keys are required to decrypt Cobalt Strike traffic. The best situation is to have the corresponding private RSA key. If that is not the case, HMAC and AES keys can be recovered using a process memory dump and capture file with encrypted traffic.

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.

Cobalt Strike: Using Known Private Keys To Decrypt Traffic – Part 2

27 October 2021 at 08:49

We decrypt Cobalt Strike traffic using one of 6 private keys we found.

In this blog post, we will analyze a Cobalt Strike infection by looking at a full packet capture that was taken during the infection. This analysis includes decryption of the C2 traffic.

If you haven’t already, we invite you to read part 1 first: Cobalt Strike: Using Known Private Keys To Decrypt Traffic – Part 1.

For this analysis, we are using capture file 2021-02-02-Hancitor-with-Ficker-Stealer-and-Cobalt-Strike-and-NetSupport-RAT.pcap.zip, this is one of the many malware traffic capture files that Brad Duncan shares on his web site Malware-Traffic-Analysis.net.

We start with a minimum of knowledge: the capture file contains encrypted HTTP traffic of a Cobalt Strike beacon communicating with its team server.

If you want to know more about Cobalt Strike and its components, we highly recommend the following blog post.

First step: we open the capture file with Wireshark, and look for downloads of a full beacon by stager shellcode.

Although beacons can come in many forms, we can identify 2 major categories:

  1. A small piece of shellcode (a couple of hundred bytes), aka the stager shellcode, that downloads the full beacon
  2. The full beacon: a PE file that can be reflectively loaded

In this first step, we search for signs of stager shellcode in the capture file: we do this with the following display filter: http.request.uri matches “/….$”.

Figure 1: packet capture for Cobalt Strike traffic

We have one hit. The path used in the GET request to download the full beacon, consists of 4 characters that satisfy a condition: the byte-value of the sum of the character values (aka checksum 8) is a known constant. We can check this with the tool metatool.py like this:

Figure 2: using metatool.py

More info on this checksum process can be found here.
The output of the tool shows that this is a valid path to download a 32-bit full beacon (CS x86).
The download of the full beacon is captured too:

Figure 3: full beacon download

And we can extract this download:

Figure 4: export HTTP objects
Figure 5: selecting download EbHm for saving
Figure 6: saving selected download to disk

Once the full beacon has been saved to disk as EbHm.vir, it can be analyzed with tool 1768.py. 1768.py is a tool that can decode/decrypt Cobalt Strike beacons, and extract their configuration. Cobalt Strike beacons have many configuration options: all these options are stored in an encoded and embedded table.

Here is the output of the analysis:

Figure 7: extracting beacon configuration

Let’s take a closer look at some of the options.

First of all, option 0x0000 tells us that this is an HTTP beacon: it communicates over HTTP.
It does this by connecting to 192.254.79[.]71 (option 0x0008) on port 8080 (option 0x0002).
GET requests use path /ptj (option 0x0008), and POST requests use path /submit.php (option 0x000a)
And important for our analysis: there is a known private key (Has known private key) for the public key used by this beacon (option 0x0007).

Thus, armed with this information, we know that the beacon will send GET requests to the team server, to obtain instructions. If the team server has commands to be executed by the beacon, it will reply with encrypted data to the GET request. And when the beacon has to send back output from its commands to the team server, it will use a POST request with encrypted data.

If the team server has no commands for the beacon, it will send no encrypted data. This does not necessarily mean that the reply to a GET request contains no data: it is possible for the operator, through profiles, to masquerade the communication. For example, that the encrypted data is inside a GIF file. But that is not the case with this beacon. We know this, because there are no so-called malleable C2 instructions in this profile: option 0x000b is equal to 0x00000004 -> this means no operations should be performed on the data prior to decryption (we will explain this in more detail in a later blog post).

Let’s create a display filter to view this C2 traffic: http and ip.addr == 192.254.79[.]71

Figure 8: full beacon download and HTTP requests with encrypted Cobalt Strike traffic

This displays all HTTP traffic to and from the team server. Remark that we already took a look at the first 2 packets in this view (packets 6034 and 6703): that’s the download of the beacon itself, and that communication is not encrypted. Hence, we will filter these packets out with the following display filter:

http and ip.addr == 192.254.79.71 and frame.number > 6703

This gives us a list of GET requests with their reply. Remark that there’s a GET request every minute. That too is in the beacon configuration: 60.000 ms of sleep (option 0x0003) with 0% variation (aka jitter, option 0x0005).

Figure 9: HTTP requests with encrypted Cobalt Strike traffic

We will now follow the first HTTP stream:

Figure 10: following HTTP stream
Figure 11: first HTTP stream

This is a GET request for /ptj that receives a STATUS 200 reply with no data. This means that there are no commands from the team server for this beacon for now: the operator has not issued any commands at that point in the capture file.

Remark the Cookie header of the GET request. This looks like a BASE64 string: KN9zfIq31DBBdLtF4JUjmrhm0lRKkC/I/zAiJ+Xxjz787h9yh35cRjEnXJAwQcWP4chXobXT/E5YrZjgreeGTrORnj//A5iZw2TClEnt++gLMyMHwgjsnvg9czGx6Ekpz0L1uEfkVoo4MpQ0/kJk9myZagRrPrFWdE9U7BwCzlE=

That value is encrypted metadata that the beacon sends as a BASE64 string to the team server. This metadata is RSA encrypted with the public key inside the beacon configuration (option 0x0007), and the team server can decrypt this metadata because it has the private key. Remember that some private keys have been “leaked”, we discussed this in our first blog post in this series.

Our beacon analysis showed that this beacon uses a public key with a known private key. This means we can use tool cs-decrypt-metadata.py to decrypt the metadata (cookie) like this:

Figure 12: decrypting beacon metadata

We can see here the decrypted metadata. Very important to us, is the raw key: caeab4f452fe41182d504aa24966fbd0. We will use this key to decrypt traffic (the AES adn HMAC keys are derived from this raw key).

More metadata that we can find here is: the computername, the username, …

We will now follow the HTTP stream with packets 9379 and 9383: this is the first command send by the operator (team server) to the beacon:

Figure 13: HTTP stream with encrypted command

Here we can see that the reply contains 48 bytes of data (Content-length). That data is encrypted:

Figure 14: hexadecimal view of HTTP stream with encrypted command

Encrypted data like this, can be decrypted with tool cs-parse-http-traffic.py. Since the data is encrypted, we need to provide the raw key (option -r caeab4f452fe41182d504aa24966fbd0) and as the packet capture contains other traffic than pure Cobalt Strike C2 traffic, it is best to provide a display filter (option -Y http and ip.addr == 192.254.79.71 and frame.number > 6703) so that the tool can ignore all HTTP traffic that is not C2 traffic.

This produces the following output:

Figure 15: decrypted commands and results

Now we can see that the encrypted data in packet 9383 is a sleep command, with a sleeptime of 100 ms and a jitter factor of 90%. This means that the operator instructed the beacon to beacon interactive.

Decrypted packet 9707 contains an unknown command (id 53), but when we look at packet 9723, we see a directory listing output: this is the output result of the unknown command 53 being send back to the team server (notice the POST url /submit.php). Thus it’s safe to assume that command 53 is a directory listing command.

There are many commands and results in this capture file that tool cs-parse-http-traffic.py can decrypt, too much to show here. But we invite you to reproduce the commands in this blog post, and review the output of the tool.

The last command in the capture file is a process listing command:

Figure 16: decrypted process listing command and result

Conclusion

Although the packet capture file we decrypted here was produced more than half a year ago by Brad Duncan by running a malicious Cobalt Strike beacon inside a sandbox, we can decrypt it today because the operators used a rogue Cobalt Strike package including a private key, that we recovered from VirusTotal.

Without this private key, we would not be able to decrypt the traffic.

The private key is not the only way to decrypt the traffic: if the AES key can be extracted from process memory, we can also decrypt traffic. We will cover this in an upcoming blog post.

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.

Cobalt Strike: Using Known Private Keys To Decrypt Traffic – Part 1

21 October 2021 at 08:59

We found 6 private keys for rogue Cobalt Strike software, enabling C2 network traffic decryption.

The communication between a Cobalt Strike beacon (client) and a Cobalt Strike team server (C2) is encrypted with AES (even when it takes place over HTTPS). The AES key is generated by the beacon, and communicated to the C2 using an encrypted metadata blob (a cookie, by default).

RSA encryption is used to encrypt this metadata: the beacon has the public key of the C2, and the C2 has the private key.

Figure 1: C2 traffic

Public and private keys are stored in file .cobaltstrike.beacon_keys. These keys are generated when the Cobalt Strike team server software is used for the first time.

During our fingerprinting of Internet facing Cobalt Strike servers, we found public keys that are used by many different servers. This implies that they use the same private key, thus that their .cobaltstrike.beacon_keys file is shared.

One possible explanation we verified: are there cracked versions of Cobalt Strike, used by malicious actors, that include a .cobaltstrike.beacon_keys? This file is not part of a legitimate Cobalt Strike package, as it is generated at first time use.

Searching through VirusTotal, we found 10 cracked Cobalt Strike packages: ZIP files containing a file named .cobaltstrike.beacon_keys. Out of these 10 packages, we extracted 6 unique RSA key pairs.

2 of these pairs are prevalent on the Internet: 25% of the Cobalt Strike servers we fingerprinted (1500+) use one of these 2 key pairs.

This key information is now included in tool 1768.py, a tool developed by Didier Stevens to extract configurations of Cobalt Strike beacons.

Whenever a public key is extracted with known private key, the tool highlights this:

Figure 2: 1768.py extracting configuration from beacon

At minimum, this information is further confirmation that the sample came from a rogue Cobalt Strike server (and not a red team server).

Using option verbose, the private key is also displayed.

Figure 3: using option verbose to display the private key

This can then be used to decrypt the metadata, and the C2 traffic (more on this later).

Figure 4: decrypting metadata

In upcoming blog posts, we will show in detail how to use these private keys to decrypt metadata and decrypt C2 traffic.

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.

  • There are no more articles
❌