tl;dr: download local.rules from https://github.com/dnlongen/Snort-DNS and add to your Snort installation; this will trigger an alert on DNS responses from OpenDNS that indicate likely malware, phishing, or adult content.
As I've written before, I use OpenDNS Family Shield as my domain name service. It's an effective way of minimizing one popular avenue of infection and phishing attacks. DNS is bit like a phone book: it translates human-friendly website names into computer-friendly network addresses. OpenDNS takes advantage of this to provide a layer of protection: for most websites, it will tell my computer the real network address, but for domains known to host malware, phishing attacks, or adult content, OpenDNS instead gives the network address of a page with a warning message.
Blocking access to malicious websites is a great (and simple) protection layer, but I got to thinking, what if I could get a notice anytime a block occurs? What if I could get an alert if I (or more to the point, others on my network - I have a house full of teenagers) tried to visit an undesired website? Several companies make network intrusion detection products that monitor traffic coming OUT of the network, and trigger alerts if certain patterns are detected. The best known are perhaps Damballa and FireEye: they watch for connections from the local network to known botnet command and control servers. Botnets work by having each “bot” receive instructions from a C&C server. Since there is generally no legitimate reason to be accessing the C&C server, the presence of connections to such a server indicates that something on the network is infected and operating as a bot.
Damballa’s and FireEye’s products are designed (and priced) for commercial use. They are probably beyond the budget of most home networks - including my own. With a network tap and open source Snort software though, I can build a “poor man’s” equivalent. If you make use of a malware-filtering DNS such as OpenDNS or Norton ConnectSafe, it is quite simple to write a snort rule that inspects DNS query responses and takes action when the response indicates an undesired site.
Step 1: get the data
Before I can assemble an alerting system, I have to have data to look at, and I have to get that data to a device or program that knows what to do with it.
First challenge: the homemade network tap works with 10/100 megabit devices, but not with faster (and now common) gigabit devices. Ethernet and Fast Ethernet cables use two pairs of wires within a cable roughly the thickness of a thin pencil. One pair is used for transmitting data, and one is used for receiving. It's a bit like a highway - one lane goes north, and one lane goes south; cars don't (or at least shouldn't) go against the flow of traffic. Gigabit Ethernet uses 4 pairs of wires. Traffic flows in both directions on each pair, so each device has to keep track of which direction data is flowing. You might envision this as a highway with "rush hour" lanes that flow north in the morning, and south in the evening. My network tap is purely a rewired cable with no intelligence to keep track of the direction of traffic flow. It is effective for tapping 10/100 data, but not gigabit data.
Second challenge: most of the devices on my network are wireless. There is nowhere within my network to put a wired network tap, because the data isn't going across wires. The only way to intercept their traffic with a hard-wired device is to put it on the outside of my wireless router. Since the router's purpose is to insulate my private network from the public Internet, traffic on that side of the router doesn't identify the individual devices on my network. The router uses a technique known as "network address translation," or "NAT." Each device inside my network has a private network address (often something like 192.168.0.1 or 10.0.0.1), but to the outside world, they all look like a single address (the "WAN" or public address of my router). Knowing that something on my network is compromised, without knowing *which* device, doesn't help me much.
Since everything on the network flows through the router though, that makes the router a natural place to look for a solution. I use an ASUS RT-AC87U - a very capable router that just happens to be built on Linux. Linux has some built-in functions that help here. IPTables controls how network information flows through a Linux device. Oskar Andreasson wrote an excellent tutorial on IPTables, which explains the inner workings. For my purposes, I logged into the router and ran the following command, which sends a copy of all traffic crossing my router to my Raspberry Pi running Kali:
iptables -t mangle -A POSTROUTING -j ROUTE --tee --gw <x.x.x.x>
Packets inbound from any network interface (WAN or LAN) first hit the "mangle" table's "PREROUTING" chain. Mangle is where packet modification including network address translation (NAT) occurs; PREROUTING is the chain that is processed prior to doing anything to the packet, while "POSTROUTING" is processed after the packet has been modified as needed. "x.x.x.x" is the IP address for my Kali Pi. --tee means to send a copy of the traffic, instead of redirecting the traffic.
Step 2: look at the data
At this point, I had a copy of all network traffic going to my Raspberry Pi. As I wrote before, I've installed Kali Linux (a Linux build that comes with a great many network analysis and hacking tools) and Snort (a program for analyzing network traffic and alerting when certain patterns are seen) on this RPi. The next step was to figure out what network traffic might indicate a problem.
This is where OpenDNS comes in. Using a known phishing domain I have investigated before, I can compare the DNS answers from Google (a standard DNS resolver that just gives an answer) and OpenDNS (which gives a false answer for blocked domains):
Google lookup:
>nslookup www3usaa.com
Server: google-public-dns-a.google.com
Address: 8.8.8.8
Non-authoritative answer:
Name: www3usaa.com
Addresses: 109.201.137.229
OpenDNS lookup:
>nslookup www3usaa.com
Server: resolver1-fs.opendns.com
Address: 208.67.222.123
Non-authoritative answer:
Name: www3usaa.com
Addresses: 67.215.65.133
109.201.137.229 is the real address (the malicious site was taken down long ago), while 67.215.65.133 is the warning page OpenDNS provides for a known phishing site.
Through some trial and error I determined the warning pages OpenDNS gives in answer to requests for phishing, adult content, and sites known to host malware:
67.215.66.149 is hit-malware.opendns.com
67.215.66.150 is malware.opendns.com
67.215.65.130 is hit-adult.opendns.com
67.215.65.133 is hit-phish.opendns.com
My thought was to write some Snort rules that would look for a DNS response packet containing one of these 4 addresses as the answer. The assumption is it is unlikely anyone on my network would intentionally look up one of these warning pages, so any DNS response with one of these addresses probably means someone tried to browse to somewhere that OpenDNS blocked.
Easy, right?
Well, almost. Alas Snort makes it easy to filter on the source and destination IP address (i.e. the two computers talking to one another), but the DNS request and answer are buried in the packet data. DNS packets follow a standard format of headers, followed by "questions" (the names to look up) and "answers" (answers to the questions). I could not find a built-in way to pull out the answer field specifically. I started working on parsing the DNS packet, using byte_test on the header data to calculate offsets and pull out the requested names and the answers given by OpenDNS, but decided there was an easier way.
Within a DNS response packet, the IP address is given in binary format:
43 d7 41 85 is the hexadecimal equivalent of 67.215.65.133. Snort supports binary matching by putting the hexadecimal equivalent within "|" brackets, like so:
content:"|43 D7 41 85|"
With lots of experimentation (and a thank you to Twitter follower @vintsurf for some suggestions that paid off, including this blog post), I came up with some snort rules that did the trick:
# Match a response of phish.opendns.com
alert udp any 53 -> $HOME_NET any (msg:"DNS request for a phishing site"; sid:10000001; rev:001; content:"|43 D7 41 85|";)
# Match a response of adult.opendns.com
alert udp any 53 -> $HOME_NET any (msg:"DNS request for adult content"; sid:10000002; rev:001; content:"|43 D7 41 82|";)
# Match a response of malware.opendns.com
alert udp any 53 -> $HOME_NET any (msg:"DNS request for a malware site"; sid:10000003; rev:001; content:"|43 D7 42 95|";)
alert udp any 53 -> $HOME_NET any (msg:"DNS request for a malware site"; sid:10000004; rev:001; content:"|43 D7 42 96|";)
One additional rule isn't specifically to detect malware responses, but will detect attempts to circumvent this by using a different DNS service (as in a form of attack Brian Krebs wrote about):
# Permit alternate DNS from my research box, then alert on DNS queries from anything else
pass udp x.x.x.x any <> any 53 (msg:"Allow alternate DNS from test box"; sid:10000001; rev:001;)
alert udp $HOME_NET any -> ![$HOME_NET,$DNS_IPS] 53 (msg:"DNS request not to OpenDNS"; sid:10000002; rev:001;)
These rules and a few related ones are included in my Git repository. Each rule matches any packet from port udp 53 (the standard DNS port) on any server, to any port on any address within my network, which contains the binary value in the content field. Technically, it is possible for an unrelated packet to contain that binary value somewhere within the data, but realistically it is unlikely to occur in a DNS packet, making this a reasonably reliable, simple rule. Msg specifies the text for the notification; sid is an arbitrary but locally unique rule ID; and rev is an optional revision number, useful if you frequently revise rules and want to keep track of which version applied to a given alert.
I now get an alert any time a computer on my network makes any request for which OpenDNS replies with one of the 4 block addresses I have defined. I have Snort send alerts as syslog events to my Splunk server, so I see something like this:
For my own reference, here are a couple of commands I used in testing:
- Snort test run, alerting to console: snort -A console -q -c /etc/snort/snort.conf -i eth0
- View pcap logs: tcpdump -n -r tcpdump.log.timestamp
If you've read this far, there are a couple of shortcomings in the current approach. I'd welcome suggestions for how to improve this (feel free to comment below). The problems I see are:
- The alert tells me the IP address of the offending computer or device, but not the domain name that was requested. I have Snort configured to store each packet that triggered an alert, and can use tcpdump to analyse the packets - but that's a bit of a pain. Do any readers know of a way to include payload fields from a DNS packet in the alert message?
- I've identified 4 specific "warning page" DNS responses, but OpenDNS owns far more addresses that they may use for other conditions now or in the future. At a minimum, OpenDNS owns the ranges 67.215.64.0/19 and 204.194.232.0/21 -- all told, about 10,000 addresses. Snort supports matching IP ranges in CIDR notation for the source and destination, but my approach currently does a binary match in the payload. Do any readers have an example of a Snort rule that parses DNS packets into their component fields?
More interesting, I've spent some time the past two days trying to determine why my the Snort rule for adult content is getting triggered by my son's PC. I trust him when he says he is not viewing inappropriate material (and with a drivers' license in the very near future, he is well aware of the value of keeping my trust), but if that is true, then there is a flaw in my monitoring logic.
It turns out my mistake is this: 67.215.65.130 (my Snort trigger for adult content) resolves to hit-adult.opendns.com - but it is not exclusively used for adult content. I have set up OpenDNS to block a handful of categories - pornography, nudity, sexuality, "tasteless" content (which OpenDNS describes as sites that contain torture, mutilation, horror, or the grotesque, as well as pro-suicide and pro-anorexia content), and adware. The OpenDNS name servers respond with the address for hit-adult.opendns.com for anything that violates my selected block categories. And guess what is triggering this rule? Adware. Adware associated with the games he has discovered on Steam. Adware and browser plug-ins that his AV never flagged. Without this project I never would have known that his web browser showed more ads than content (and he never complained ... how many other users just think crappy browser experience is the norm?)
I am sure the adware will be the subject of a future blog post. For now though, I have learned that a DNS response of 67.215.65.130 means blocked content, not necessarily adult content. Solution? Thus I have modified the message in the Snort rule. Even so, this blocked content is something I will want to investigate, so my project has yielded the desired result.
Update January 10: The firmware (at least as of RT-AC87U firmware 3.0.04.378_3754) appears to reload the firewall default rules periodically, thus wiping away the iptables mangle command I ran to copy traffic to my snort IDS. Until I determine exactly why this is happening, I've come up with a workaround.
ASUS does not include crontab (the scheduled task editor) in the firmware, but it does include the crond (task scheduling) service. The service itself looks in the directory/var/spool/cron/crontabs for a filename matching the user (for instance, /var/spool/cron/crontabs/root). By creating a file by that name with the following content:
*/5 * * * * iptables -t mangle -C POSTROUTING -j ROUTE --tee --gw x.x.x.x || iptables -t mangle -A POSTROUTING -j ROUTE --tee --gw x.x.x.x
And then restarting the crond service with the following command:
service restart_crond
I can cause the scheduler to run this command every 5 minutes, thus re-adding the firewall rule that sends a copy of all network traffic to my IDS. Note the syntax - first I do iptables -C to check if the rule already exists, and only if it does not will the second half of the command (the part after the || ) execute to add the rule. Without doing this, I would in fact be adding a duplicate rule every minute, quickly causing the rules table to grow and slow down if not crash the router.
Update February 21: Lenovo consumer PCs from late 2014 into early 2015 included a piece of adware/malware known as "Superfish Visual Discovery," which broke SSL in order to inject advertisements into web pages. The key failing was installing a self-signed root certificate. The developers of the software argued that it posed no security threat ... bad thing to argue when the entire security community is looking at you. Robert Graham (ErrataRob) used a Raspberry Pi 2 (the next generation of Pi - more RAM, a faster processor, but still essentially the same thing) to show how easy it is to exploit the root certificate for malicious gain. The Pi is a fantastic, cheap device for hacking and research.
Update December 2, 2015: A reader brought to my attention that openDNS has changed their landing page IP addresses for blocked content, and has published a table of landing pages.