Thursday, October 27, 2011

3WoO: Watching for Potentially Malicious Domains with OSSEC

I like logs, lots and lots of logs. When I find out certain logging capabilities aren't turned on I get confused. When I find out that they're turned on but not monitored I get angry.

DNS has been a thorn at a few places I've done work for in the recent past. There are requirements to record and monitor DNS queries, but no one seems to have a good solution. The general purpose of monitoring DNS was to look for malware that used DNS to connect to outside sites for data exfiltration. It may sound lame (not to me), but it can be quite effective depending on the intelligence you have on the threats most important to your organization.

Using snort to monitor DNS queries and responses has provided less than perfect results. Usually the sensor is placed in such a way that only recursive lookups are seen, so we end up with a bunch of alerts blaming the DNS server itself.

This post will help document part of a plan to monitor DNS. This only covers the major *nix daemons, not Windows (if someone gets me Windows logs I'll work on that too). It also only monitors the queries, not the responses. I had something that worked with bro-ids to monitor responses, but I haven't been able to test it in quite a while. I'm planning on waiting for the next major bro-ids release to update it. This method can also be worked around in a number of ways (including using domain names that aren't known to be bad, or subdomains not in the list), but it's a start.

I've tested this method with both bind and unbound, but should work with any software that logs these queries and an appropriate OSSEC decoder.

The first step is to turn on the proper logging.

Setting up the unbound.conf is simple:

log-queries: yes

Configuring named.conf is a bit more difficult:

logging {

        channel "default2" {
                syslog local7;
                severity info;
        };

        category lame-servers { null; };
        category "queries" { "default2"; };
        category "unmatched" { "default2"; };
};

I just have to make sure my syslogd is watching for local7 alerts, and writing them to a file. Configuring OSSEC to watch this logfile is pretty simple. On my system I just add the following to ossec.conf:


<localfile>
  <log_format>syslog</log_format>
  <location>/var/log/local7</location>
</localfile>

After making the logging changes to your dns daemon you'll have to restart it. Make sure logs are being populated with client queries.

Example unbound logs (differences in timestamps are from rsyslog vs OpenBSD's syslogd):

2011-10-26T15:46:15.508083-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 www.ossec.net. A IN
2011-10-26T15:45:54.895874-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 www.google.com. A IN
2011-10-26T15:46:48.366164-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 caladan.example.com. A IN
2011-10-26T15:47:24.372937-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 wallach9.example.com. A IN
2011-10-26T15:47:24.373670-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 wallach9.be.example.com. A IN

And bind:
Oct 26 16:07:08 ix named[14044]: client 192.168.17.9#22193: query: www.ossec.net IN A +
Oct 26 16:05:51 ix named[14044]: client 192.168.1.9#19095: query: wallach9.example.com IN A +
Oct 26 16:05:51 ix named[14044]: client 192.168.1.9#26269: query: wallach9.be.example.com IN A +
Oct 26 16:03:21 ix named[14044]: client 192.168.1.16#38892: query: www.google.com IN A +

Let's look at these logs in ossec-logtest. bind first:
[root@zanovar ossec]# /var/ossec/bin/ossec-logtest
2011/10/26 16:07:46 ossec-testrule: INFO: Reading local decoder file.
2011/10/26 16:07:46 ossec-testrule: INFO: Started (pid: 11804).
ossec-testrule: Type one log per line.

Oct 26 16:07:08 ix named[14044]: client 192.168.17.9#22193: query: www.ossec.net IN A +


**Phase 1: Completed pre-decoding.
       full event: 'Oct 26 16:07:08 ix named[14044]: client 192.168.17.9#22193: query: www.ossec.net IN A +'
       hostname: 'ix'
       program_name: 'named'
       log: 'client 192.168.17.9#22193: query: www.ossec.net IN A +'

**Phase 2: Completed decoding.
       decoder: 'named'
       srcip: '192.168.17.9'
       url: 'www.ossec.net'

The domain name is decoded in the url section. Let's try the unbound log:
[root@zanovar ossec]# /var/ossec/bin/ossec-logtest
2011/10/26 16:11:47 ossec-testrule: INFO: Reading local decoder file.
2011/10/26 16:11:47 ossec-testrule: INFO: Started (pid: 11805).
ossec-testrule: Type one log per line.

2011-10-26T15:46:15.508083-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 www.ossec.net. A IN


**Phase 1: Completed pre-decoding.
       full event: '2011-10-26T15:46:15.508083-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 www.ossec.net. A IN'
       hostname: 'arrakis'
       program_name: 'unbound'
       log: '[8113:0] info: 127.0.0.1 www.ossec.net. A IN'

**Phase 2: Completed decoding.
       No decoder matched.

There is no unbound decoder currently. That's easy to fix. (in case there are any errors in the HTMLization of the decoders below feel free to grab this file)
Add the following to /var/ossec/etc/local_decoder.xml:
<decoder name="unbound">
  <program_name>^unbound</program_name>
</decoder>

<decoder name="unbound-info">
  <parent>unbound</parent>
  <prematch offset="after_parent">^\p\d+:\d+\p info: </prematch>
  <regex offset="after_prematch">^(\S+) (\S+) \S+ \S+$</regex>
  <order>srcip, url</order>
</decoder>


And this is how it decodes now:
[root@zanovar ossec]# /var/ossec/bin/ossec-logtest
2011/10/26 16:17:16 ossec-testrule: INFO: Reading local decoder file.
2011/10/26 16:17:16 ossec-testrule: INFO: Started (pid: 11810).
ossec-testrule: Type one log per line.

2011-10-26T15:46:15.508083-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 www.ossec.net. A IN


**Phase 1: Completed pre-decoding.
       full event: '2011-10-26T15:46:15.508083-04:00 arrakis unbound: [8113:0] info: 127.0.0.1 www.ossec.net. A IN'
       hostname: 'arrakis'
       program_name: 'unbound'
       log: '[8113:0] info: 127.0.0.1 www.ossec.net. A IN'

**Phase 2: Completed decoding.
       decoder: 'unbound'
       srcip: '127.0.0.1'
       url: 'www.ossec.net.'

We're one step closer to having this all work. The next step is to decide on which domain names you want to alert on. I use a number of sources and a bad python script (seriously bad, you can't have it) to create a list of suspicious domains.
I use lists from Malware Domain List, DNS-BH - Malware Domain Blocklist (please donate!), and abuse.ch Zeus Tracker. I also have a list setup for domains I hear about that may not be on these other lists, and some other lists I don't pull via the script. A quick note about the DNS-BH site: That site is primarily a source for creating a DNS blackhole. This can keep your systems from ever getting to these bad sites (redirect them to a "honeypot" system to see what traffic they try to pass). I definitely recommend doing this, it's almost a free bit of security. It's also easy to setup in both bind and unbound/nsd. I've configured both to do this, so if anyone is interested let me know!
Of course, if you're worried about RAM usage this may not be the best idea:
  PID USERNAME PRI NICE  SIZE   RES STATE     WAIT      TIME    CPU COMMAND
14044 named      2    0 1088M  885M sleep/1   select   34:53  0.00% /usr/sbin/named

  PID USERNAME PRI NICE  SIZE   RES STATE     WAIT      TIME    CPU COMMAND
19753 _nsd       2    0  296M  249M idle      select    0:03  0.00% /usr/sbin/nsd -c /etc/nsd.conf

After collecting a list of possibly malicious domains, you'll want to create a CDB list. CDB is a key-value database format. It's great for lists that are fairly static. There's no way to add or remove items from the database, you have to recompile it from scratch. Recompiling these lists is pretty simple and quick, so it isn't much of an issue
The format is also simple:
key: value

My lists generally look like:
DOMAIN: Suspicious Domain

After this, move the list to your ossec directory, I keep mine in /var/ossec/lists. Next we configure OSSEC to use the list by adding them to the rules section:
<rules>
  ...
    <list>lists/blocked.txt.cdb</list>
    <list>lists/userlist.txt.cdb</list>
    <include>local_rules.xml</include>
</rules>

Compiling the lists is easy, and if the files haven't changed since the last recompile ossec-makelists will notify you that they don't need to be recompiled. When a list is updated you should not have to restart the OSSEC processes, they should pick up the changes automatically.
# /var/ossec/bin/ossec-makelists
 * File lists/blocked.txt.cdb does not need to be compiled
 * File lists/userlist.txt.cdb does not need to be compiled
# rm /var/ossec/lists/userlist.txt.cdb
# /var/ossec/bin/ossec-makelists
 * File lists/blocked.txt.cdb does not need to be compiled
 * File lists/userlist.txt.cdb need to be updated

The last piece will be creating a rule to use the list. These are the rules I have for unbound, but the bind rules look very similar:
   <rule id="40000" level="0" noalert="1">
    <decoded_as>unbound</decoded_as>
    <description>Grouping for unbound.</description>
  </rule>

  <rule id="40001" level="10">
    <if_sid>40000</if_sid>
    <list field="url">lists/blocked.txt</list>
    <description>DNS query on a potentially malicious domain.</description>
  </rule>

The list item compares the decoded url to the keys in blocked.txt.cdb database. If there is a match rule 40001 fires, if that url isn't in the database then it doesn't fire.
Hopefully this post gave you some ideas. There's more you can do with lists, so take a look at the documentation (more here).
Just a little teaser, I'm planning on another documentation post tomorrow or Friday. Stay tuned!

No comments:

Post a Comment