SHARE
Security / January 20, 2021

DNS C2 Sandwich: A Novel Approach

Threat actors have been using the domain name system (DNS) for command and control (C2) for years. DNS is a useful channel for malware C2 for many reasons. First, DNS is reliable. Since most (all?) devices need DNS to function properly, the protocol is almost never blocked and is rarely restricted. Second, DNS is able to bounce around the internet until it is routed to its final destination, which can frustrate analysis at the IP level. Finally, many organizations will prioritize HTTP(S) proxy logging, and will neglect collection/analysis of DNS logs as they deem it too costly for minimal reward.

While researching DNS C2, I noticed that threat actors almost always use this technique the same way. The most common method will be described below, followed by a novel technique.

Standard DNS C2

Adversaries often leverage DNS for C2 by putting commands into the domain name fields in DNS lookups, and encoding the commands such that they look like valid domains. Compromised devices send these DNS lookups to their DNS server, which will pass the lookups upstream, eventually hitting the malicious DNS server. Here’s a simplistic example of this data encoding in action:

Hello, World!

Original data to encode.

First,  we encode the data so that it is a valid subdomain. In this case, we used base32:

JBSWY3DPFQQFO33SNRSCC

Original data encoded with base32.

Then, we prepend the fake subdomain to a real domain that we control and perform a DNS lookup. For this example, let’s say we own example[.]com. Below is how the DNS lookup appears on the wire:

|21|JBSWY3DPFQQFO33SNRSCC|07|example|03|com|00 00 10 00 01|

Lookup on the wire: a DNS record request for JBSWY3DPFQQFO33SNRSCC[.]example[.]com.

Upon receiving the message encoded in the lookup, the C2 server may respond with data for the compromised device and can pass commands. Below, we show an example response containing a base32 encoded command.

|21|JBSWY3DPFQQFO33SNRSCC|07|example|03|com|00 00 10 00 01 00 00 00 3c 00 38|IBSWG2DPEBXWMZQNBJSGK3BAMM5FYV2JJZCE6V2TLRZXS43UMVWTGMQ=

Response to our malware’s DNS record request: a base32 encoded command for the malware to run.

After decoding the response, we get the command to run:

@echo off
del c:\WINDOWS\system32

Decoded data; the command for our theoretical malware to run on the theoretical infected machine.

Novel DNS C2

Okay, so why would anyone want to perform obtuse DNS manipulations in the first place? For evasion, of course! Lots of network controls let DNS out, or don’t monitor it very closely. In fact, if there is monitoring, it is likely logging only the most pertinent fields of DNS while ignoring the rest. We can theoretically go outside of the standard format of DNS messages in order to construct our novel DNS C2 channel to evade this type of network monitoring.

I saw how most DNS C2 was conducted, with tools like IodineDNSCat2, and Cobalt Strike and noticed that they mostly behave similarly to our example above. But why does everyone do it this way? The DNS protocol has a lot of other stuff going on in it that doesn’t seem to be used much. Like what’s the deal with Additional records? Why not throw a bunch of data in there? Why not send multiple Questions? Why not send a response packet as a request?

Libraries Don’t Make It That Easy

Most DNS libraries don’t expose DNS operations at a low enough level to perform these types of manipulations. For example, the Windows DNS API only allows a developer to make a request with one question, under the IN class. Linux is similar, but does allow one to specify a class for a query.

In addition to this, if you take a look at the source code of popular DNS server software like BIND, you’ll see that including more than one question will actually return a “format error” code back to the requester, as is described in this Stack Overflow post. Additional records and authority records will get dropped or not passed through to the next server, as they are thought to be errant.

Since we’ve seen that most DNS APIs don’t allow you to do much besides specify record type (but not the class) and the domain name, we will have to do this the hard way. Let’s dust off our favorite socket library and make it ourselves.

Several days later….

Here are some highlights of my results:

  1. It’s painful
  2. Most servers will play dumb and/or reject your wacky requests
  3. Most monitoring tools will still treat these as standard DNS packets, because technically they are, but they might not reveal their peculiarity

Even though most servers will reject my odd DNS payloads, I still have one (pretty good) option.

Direct DNS Connections

I’ve often heard people hating on the idea of direct DNS for command and control. “It’s too obvious” or “no self-respecting company allows that.” Oh but they do. More than 90% of organizations in our sample data showed valid DNS resolutions from at least one out of the top three public DNS resolvers. Adjusting for internal DNS servers, that number dropped to 85 percent. Since our data is skewed towards more mature organizations, I would say that this technique should work just fine (with about 85% of organizations).

Real traffic from an APT32 RAT using direct DNS C2.

As seen in the example above, threat actors often utilize direct DNS connections for command and control, so why shouldn’t we? Rolling with this, we find there are a plethora of ways we can tunnel information into and out of the network.

‘sudo make me a DNS sandwich’

Screenshot of Wireshark’s interpretation of the DNS C2 sandwich.

DNS is restricted to UDP packets that are under 512 bytes in length total (unless you want to switch to TCP, which I’m trying to avoid in this scheme). Since this is a thing, if we’re trying to communicate some amount of data that is larger than this cutoff, we’re going to need to split our message up into packages of up to 512 bytes total. When we do this, we can run into issues of packets arriving out of order. Another problem is that we don’t really know how “big” our message is going to be.

In order to account for these things, let’s go ahead and repurpose the question class (qclass) number for numbering our message. It’s a “short” value (up to 65535), so it should cover our bases in terms of message sizes (up to 15MB, conservatively).

To track if we’ve received the last message in the stream, we can tag the DNS header with the “z” flag which is typically reserved and intended to be 0. We can keep appending data we receive associated with a given transaction id and not interpret/decode it until we’ve seen that “z”.

Direct DNS C2 tool in action using the DNS sandwich C2 scheme.

How Does It Fare?

As you can see from the demo video, it does, in fact, work. I was able to use this C2 channel effectively for running shell commands and receiving output.

Although anyone can write software to “do bad things” on an endpoint, our goal from the beginning was to validate how logging was occurring for the DNS protocol (so as to detect all methods of DNS C2). In order to validate proper visibility into this technique, I tested it against both Zeek and Suricata, which are both major solutions in this community.

Zeek

This method flies completely under the radar of Zeek (the NSM formerly known as Bro), as Zeek logs the last question in a DNS query, and the rest are ignored. My novel technique has successfully illuminated a visibility blind spot where a threat actor could potentially sneak through.

Suricata

In versions of Suricata prior to 5.x, Suricata will not see/recognize these packets as dns. This means that any alerts utilizing the dns protocol will not run on these packets. Additionally, the DNS logs that Suricata can produce (if you opt for them) would not log these packets.

As of version 5.x, however, DNS logging was changed fairly significantly, and multiple questions are now logged! In other words, this particular scheme is very visible if you’re using Suricata’s DNS logs (tested on Suricata 6.0.0). The dns protocol being used within alerts should also trigger on these packets.

It Fares!

Sweet — so we’re able to send data, all the while most security tools think we’re DNS traffic, and for all intents and purposes, we actually are DNS traffic. We’re just DNS traffic that’s not quite normal, but we’re abnormal in a way that people are unlikely to notice. This is the exact sweet spot we wanted to be in.

Conclusion

In detection engineering, I am always hoping to understand threats and the way that activity is presented in data. If the data is showing an incomplete picture, I must ask myself if I can consistently detect the bad behaviors I am aiming to detect. If not, I must remedy the data collection.

In this case, we found that there was DNS data being dropped on the floor. It allowed us to bypass security monitoring and maintain C2 without fear of detection. Now that we are aware of this gap in visibility, we can adjust our logging facilities.

Are there other methods, yet to be explored, that still allow an adversary freedom to operate? Of course there are; however, based on this exercise, we’ve been able to shine a light on one method that is no longer available (for threats that don’t want to be discovered).


Appendix

Below is a series of Suricata rules that alert on strange DNS packets. These rules might be paired with some thresholding clauses in order to not overwhelm analysts and to increase rule fidelity.

Multiple questions in a query packet:
alert udp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Multiple Questions (udp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 1, 4; flowbits:set,ATR.dns.query.has_multiple_questions; classtype: misc-activity; sid: 5555; rev: 1;)
alert tcp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Multiple Questions (tcp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 1, 4; flowbits:set,ATR.dns.query.has_multiple_questions; classtype: misc-activity; sid: 5556; rev: 1;)
alert dns $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Multiple Questions"; dns.opcode:0; byte_test:2, !&, 32768, 2; byte_test:2, >, 1, 4; flowbits:set,ATR.dns.query.has_multiple_questions; classtype: misc-activity; sid: 5557; rev: 1;)
alert udp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Response Packet with Multiple Questions (udp)"; byte_test:2, &, 32768, 2; byte_test:2, >, 1, 4; flowbits:set,ATR.dns.response.has_multiple_questions; classtype: misc-activity; sid: 5558; rev: 1;)
alert tcp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Response Packet with Multiple Questions (tcp)"; byte_test:2, &, 32768, 2; byte_test:2, >, 1, 4; flowbits:set,ATR.dns.response.has_multiple_questions; classtype: misc-activity; sid: 5559; rev: 1;)
alert dns $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Response Packet with Multiple Questions"; dns.opcode:0; byte_test:2, &, 32768, 2; byte_test:2, >, 1, 4; flowbits:set,ATR.dns.response.has_multiple_questions; classtype: misc-activity; sid: 5560; rev: 1;)
Any answers in a query packet:
alert udp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Answers (udp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 0, 6; flowbits:set,ATR.dns.query.has_answers; classtype: misc-activity; sid: 5561; rev: 1;)
alert tcp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Answers (tcp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 0, 6; flowbits:set,ATR.dns.query.has_answers; classtype: misc-activity; sid: 5562; rev: 1;)
alert dns $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Answers"; dns.opcode:0; byte_test:2, !&, 32768, 2; byte_test:2, >, 0, 6; flowbits:set,ATR.dns.query.has_answers; classtype: misc-activity; sid: 5563; rev: 1;)
Any authorities in a query packet:
alert udp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Authorities (udp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 0, 8; flowbits:set,ATR.dns.query.has_authorities; classtype: misc-activity; sid: 5564; rev: 1;)
alert tcp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Authorities (tcp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 0, 8; flowbits:set,ATR.dns.query.has_authorities; classtype: misc-activity; sid: 5565; rev: 1;)
alert dns $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Authorities"; dns.opcode:0; byte_test:2, !&, 32768, 2; byte_test:2, >, 0, 8; flowbits:set,ATR.dns.query.has_authorities; classtype: misc-activity; sid: 5566; rev: 1;)
Multiple additionals in a query packet:
alert udp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Additionals (udp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 1, 10; flowbits:set,ATR.dns.query.has_additionals; classtype: misc-activity; sid: 5567; rev: 1;)
alert tcp $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Additionals (tcp)"; byte_test:2, !&, 32768, 2; byte_test:2, >, 1, 10; flowbits:set,ATR.dns.query.has_additionals; classtype: misc-activity; sid: 5568; rev: 1;)
alert dns $HOME_NET any -> any 53 (msg:"ATR IN_DEVELOPMENT DNS Query Packet with Additionals"; dns.opcode:0; byte_test:2, !&, 32768, 2; byte_test:2, >, 1, 10; flowbits:set,ATR.dns.query.has_additionals; classtype: misc-activity; sid: 5569; rev: 1;)

Featured Webinars
Hear from our experts on the latest trends and best practices to optimize your network visibility and analysis.

CONTINUE THE DISCUSSION

People are talking about this in the Gigamon Community’s Security group.

Share your thoughts today


Back to top