v11.1: DNS Blackhole with iRules

Back in October, I attended a Security B-Sides event in Jefferson City (review here). One of the presenters (@bethayoung) talked about poisoning the internal DNS intentionally for known purveyors of all things bad. I indicated in my write-up that I’d be detailing an F5-based solution, and whereas a few weeks has turned into a couple months, well, here we are. As much as I had hoped to get it all together on my own, F5er Hugh O’Donnell beat me to it, and did a fantastic job. F5er Lee Orrick also contributed to the solution and I’ll have more from him in a future article.

Conceptual Overview

Before jumping into the nuts and bolts, I’d like to describe the solution. First, consider normal operation: Joe Anonymous is surfing and hits a popular page that has been compromised. He hits a link for a cute video about puppies and rainbows and NOT SO FAST MY FRIEND! Instead of said cute puppies and rainbows video, he ends up with a nasty case of malware and his friendly neighborhood IT staff gets to spend some time remediating the damage—if it’s caught at all. See, DNS is if not the backbone of the internet, at least several of the vertebrae.  And it does its job very well. Asked and answered. Done. If you hit a link with a malicious domain, there’s a very very good chance your DNS server will have no safeguards in place, it’ll answer away. This is what a blackhole DNS solution is configured to overcome. The networking folks in the audience will be familiar with blackhole routing, and this is really no different a concept. When a user makes a query, the service inspects the destination, and if it matches a list of well known badness, it returns an address of an internal site where remediation or at least notification can take place. In either event, the request is not hitting the malicious destination, which protects user and organization. See Figure 1 for the flow detail.

Building the Datagroup

As with iFiles in v11.1, datagroups can also be imported via the GUI and then referenced similarly. To import your blacklisted domains (there’s a big list here: mirror1.malwaredomains.com), make sure your text editor is set for line feed terminator only (CR-LF won’t work) and use this format for each entry:

“.abbcp.cn” := “harmful”,

“.3dglases-panasonic-tv.com” := “zeusv2”,

The first field is the domain, and the second field is a type description. The first will match your traffic, the second is strictly for classification purposes and can be edited as necessary.

Intercepting the DNS Requests

This solution can be implemented with LTM or GTM, though if the latter, the iRule will still need to be attached to the virtual server associated with the wideIP instead of the wideIP itself. In this article, I’ll implement the LTM-based solution. As I’ll be utilizing the new DNS:: commands, a DNS profile will need to be attached to the virtual server as well as the iRule below. Note that the blackhole class (named appropriately Blackhole_Class in the iRule below) should be present on the system for this solution to work.

 

 

# Author:  Hugh O'Donnell, F5 Consulting
when RULE_INIT {
  # Set IPV4 address that is returned for Blackhole matches for A records
  set static::blackhole_reply_IPV4 "10.10.20.50"
  # Set IPV6 address that is returned for Blackhole matches for AAAA records
  set static::blackhole_reply_IPV6 "2001:19b8:101:2::f5f5:1d"
  # Set TTL used for all Blackhole replies
  set static::blackhole_ttl "300"
}
when DNS_REQUEST {
  # debugging statement see all questions and request details
  # log -noname local0. "Client: [IP::client_addr] Question:[DNS::question name] Type:[DNS::question type] Class:[DNS::question class] Origin:[DNS::origin]"
  # Blackhole_Match is used to track when a Query matches the blackhole list
  # Ensure it is always set to 0 or false at beginning of the DNS request
  set Blackhole_Match 0
  # Blackhole_Type is used to track why this FQDN was added to the Blackhole_Class
  set Blackhole_Type ""
  # When the FQDN from the DNS Query is checked against the Blackhole class, the FQDN must start with a
  # period.  This ensures we match a FQDN and all names to the left of it.  This prevents against
  # malware that dynamically prepends characters to the domain name in order to bypass exact matches
  if {!([DNS::question name] == ".")} {
    set fqdn_name .[DNS::question name]
  }
  if { [class match $fqdn_name ends_with Blackhole_Class] } {
    # Client made a DNS request for a Blackhole site.
    set Blackhole_Match 1
    set Blackhole_Type [class match -value $fqdn_name ends_with Blackhole_Class ]
    # Prevent processing by GTM, DNS Express, BIND and GTM Listener's pool. 
    # Want to ensure we don't request a prohibited site and allow their server to identify or track the GTM source IP.
    DNS::return
  }    
}
when DNS_RESPONSE {
  # debugging statement to see all questions and request details
  # log -noname local0. "Request: $fqdn_name Answer: [DNS::answer] Origin:[DNS::origin] Status: [DNS::header rcode] Flags: RD [DNS::header rd] RA [DNS::header ra]"
  if { $Blackhole_Match } {
    # This DNS request was for a Blackhole FQDN. Take different actions based on the request type.
    switch [DNS::question type] {
      "A" {
        # Clear out any DNS responses and insert the custom response.  RA header = recursive answer
        DNS::answer clear
        DNS::answer insert "[DNS::question name]. $static::blackhole_ttl [DNS::question class] [DNS::question type] $static::blackhole_reply_IPV4"
        DNS::header ra "1"
        # log example:  Apr  3 14:54:23 local/tmm info tmm[4694]:
        # Blackhole: 10.1.1.148#4902 requested foo.com query type: A class IN A-response: 10.1.1.60
        log -noname local0. "Blackhole: [IP::client_addr]#[UDP::client_port] requested [DNS::question name] query type: [DNS::question type] class [DNS::question class] A-response: $static::blackhole_reply_IPV4 BH type: $Blackhole_Type"
      }
      "AAAA" {
        # Clear out any DNS responses and insert the custom response.  RA header = recursive answer
        DNS::answer clear
        DNS::answer insert "[DNS::question name]. $static::blackhole_ttl km[DNS::question class] [DNS::question type] $static::blackhole_reply_IPV6"
        DNS::header ra "1"
        # log example:  Apr  3 14:54:23 local/tmm info tmm[4694]:
        # Blackhole: 10.1.1.148#4902 requested foo.com query type: A class IN AAAA-response: 2001:19b8:101:2::f5f5:1d
        log -noname local0. "Blackhole: [IP::client_addr]#[UDP::client_port] requested [DNS::question name] query type: [DNS::question type] class [DNS::question class] AAAA-response: $static::blackhole_reply_IPV6 BH type: $Blackhole_Type"
      }
      default {
        # For other record types, e.g. MX, NS, TXT, etc, provide a blank NOERROR response
        DNS::last_act reject
        # log example:  Apr  3 14:54:23 local/tmm info tmm[4694]:
        # Blackhole: 10.1.1.148#4902 requested foo.com query type: A class IN unable to respond
        log -noname local0. "Blackhole: [IP::client_addr]#[UDP::client_port] requested [DNS::question name] query type: [DNS::question type] class [DNS::question class] unable to respond  BH type: $Blackhole_Type"
      }
    }
  }
} 

 

 

This iRule handles the DNS request, responding on behalf of GTM or any DNS servers being load balanced by LTM. And since we’re handling the blackhole site, we can serve that up as well from an iRule on an HTTP virtual server.

Serving the Remediation Page

The remediation page can be as simple as a text message indicating malware, or it can be a little more complex to show the category of the problem site as well as provide some contact information. The iRule below is an example of the latter.

# Author: Hugh O’Donnell, F5 Consulting
when HTTP_REQUEST {
  # the static HTML pages include the logo that is referenced in HTML as corp-logo.gif
  # intercept requests for this and reply with the image that is stored in an iFile defined in RULE_INIT below
  if {[HTTP::uri] ends_with "/_maintenance-page/corp-logo.png" } {
    # Present
    HTTP::respond 200 content $static::corp_logo
  } else {
    # Request for Blackhole webpage.  Identify what type of block was in place
    switch -glob [class match -value ".[HTTP::host]" ends_with Blackhole_Class ] {
      "virus" { set block_reason "Virus site" }
      "phishing" { set block_reason "Phishing site" }
      "generic" { set block_reason "Unacceptable Usage" }
      default { set block_reason "Denied Per Policy - Other Sites" }
    }
    # Log details about the blackhole request to the remote syslog server
    log -noname local0. "Blackhole: From [IP::client_addr]:[TCP::client_port] \
      to [IP::local_addr]:[TCP::local_port], [HTTP::request_num], \
      [HTTP::method],[HTTP::uri],[HTTP::version], [HTTP::host],  [HTTP::header value Referer], \
      [HTTP::header User-Agent], [HTTP::header names],[HTTP::cookie names], BH category: $block_reason,"
    # Send an HTML page to the user.  The page is defined in the RULE_INIT event below
    HTTP::respond 200 content "$static::block_page [HTTP::host][HTTP::uri] $static::after_url $block_reason $static::after_block_reason "
  }   
}
when RULE_INIT {
  # load the logo that was stored as an iFile
  set static::corp_logo [ifile get "/Common/f5ball"]
  # Beginning of the block page
  set static::block_page "
    <html lang=\"en_US\">
    <head>
    <title>Web Access Denied - Enterprise Network Operations Center</title>
    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">
    <meta http-equiv=\"CACHE-CONTROL\" content=\"NO-CACHE\">
    <meta http-equiv=\"PRAGMA\" content=\"NO-CACHE\">
    <meta http-equiv=\"EXPIRES\" content=\"Mon, 22 Jul 2002 11:12:01 GMT\">
    <style>
    <!--
      .mainbody {
        background-color: #C0C0C0;
        color: #000000;
        font-family: Verdana, Geneva, sans-serif;
        font-size: 12px;
        margin: 0px;
        padding: 20px 0px 20px 0px;
        position: relative;
        text-align: center;
        width: 100%;
      }
      .bdywrpr {
        width:996px;
        height:auto;
        text-align:left;
        margin:0 auto; 
        z-index:1;
        position: relative;
      }
      #banner-wrapper {
        width: 950px;
        padding: 0px;
        margin: 0px;
        overflow:hidden;
        background-color: #FFFFFF;
        background-repeat: no-repeat;
      }
      #banner-image {
        float: left;
        margin-left: auto;
        margin-right: auto;
        padding: 3px 0px 2px 7px;
        width: 950px;
      }
      #textbody {
        background-color: #FFFFFF;
        color: #000000;
        font-family: Verdana, Geneva, sans-serif;
        font-size: 13px;
        width: 950px;
        padding:0px;
        text-align:justify;
        margin: 0px;
      }
    -->
    </style>
    </head>
    <body class=\"mainbody\">
    <div class=\"bdywrpr\">
    <div id=\"banner-wrapper\">
    <!-- BANNER -->
    <div id=\"banner-image\">
    <center><img src=\"/_maintenance-page/corp-logo.png\" alt=\"Enterprise Network Operations Center\"></center>
    </div>
    </div>
    <div id=\"textbody\">
    <table border=\"0\" cellpadding=\"40\"><tr><td>
    <center><p style=\"font-size:18px\"><b>Access has been denied.<br><br> URL: "
  set static::after_url "</p></center></b></font> <br>
    Your request was denied because it is blacklisted in DNS. <br><br>
    Blacklist category: "
  set static::after_block_reason "<br>
    <p>
    The Internet Gateways are for official use only. Misuse violates policy.
    If you believe that this site is categorized incorrectly, and that you have a valid business
    reason for access to this site please contact your manager for approval
    and the Enterprise Network Operations Center via
    <br><br>
    E-mail: <a href=\"mailto:enoc@example.com\">enoc@example.com</a> <br><br>
    Please use the Web Access Request Form and include a business justification.
    &nbsp;
    Only e-mail that originates from valid internal e-mail addresses will be processed. 
    If you do not have a valid e-mail address, your manager will need to submit a request on your behalf.
    </center>
    <p>
    <font size=-1><i>Generated by bigip1.f5.com.</i></font>
    </td></tr></table>
    </div>
    </div>
    </body> </html>
    "
}

Note that the remediation page references an iFile for a logo. For details on configuring iFiles, please reference my article on iFiles. Also note that in addition to the client getting a heads-up notification of malfeasance, the visit is logged so other processes, individuals can act on the information.

The Results

First, our DNS query and response. Rather than test out a real well-known bad site, I added espn.com to my blacklist so if I forgot a step and leaked through to the real site I wouldn’t compromise anything. The response from my DNS virtual server is shown in Figure 2 below.

You can see that the address matches the address set in the iRule as our blackhole IPv4 address. Also, the log information from that DNS query:

Dec 28 15:35:08 tmm info tmm[6883]: Blackhole: 10.10.20.251#57714 requested espn.com query type: A class IN A-response: 10.10.20.50 BH type: sports

Next, the resulting remediation page in my browser (Figure 3):

And finally, the log entry from the HTTP request:

Dec 28 15:35:08 tmm info tmm[6883]: Blackhole: From 10.10.20.251:32447 to 10.10.20.50:80, 1, GET,/,1.1, espn.com, , Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7, Host Connection User-Agent Accept Accept-Encoding Accept-Language Accept-Charset,, BH category: Denied Per Policy - Other Sites,

Conclusion

This is a wicked application of iRules with new DNS and file handling features delivered in v11.1. If you wanted to take it even further, you could use sideband connections and reference an external list instead of a datagroup that will need constant refreshing. The GTM version of this solution is documented in CrowdSRC. If you’re curious about the DNS commands used in the iRule above, I’ll be discussing them in my next tech tip, so check back soon!

 

Note: For the LTM solution presented above, the DNS Services module or the GTM module is required to be licensed.

 
Updated Mar 16, 2022
Version 2.0
  • Nice iRule!

     

     

    You could save a little bit of CPU time by changing the == to eq to do a string comparison without polymorphism:

     

     

    if {!([DNS::question name] == ".")} {

     

    ->

     

    if {!([DNS::question name] eq ".")} {

     

     

    Aaron
  • This is very cool. But my setup is different. My DNS server sits physically in-line behind a GTM. The DNS IP address is being advertised from the server via OSPF. In that instance, would I be able to apply the iRule to the WideIP?
  • I did look for that but i don't have a virtual servers section under "local traffic" on my GTM. I am running 10.2, maybe that is why? I do realize I'll need to upgrade for this to work, by the way :)
  • With GTM license these abilities will be in place when you upgrade. I will update the article to note that at a minimum the DNS Services module is required for LTM-only solution to work.
  • Hi, I've used this blackhole iRule, sometimes it blocked the genuine page as well, and i changed few option like instead of ends_with, used eq or contains, then i saw CPU usage is very high. If possible, can we modify the iRule that control the CPU usage and it work properly. Thanks.....Jay
  • Hi Jason, we had a discussion on the board where a peer has complained about performance problems using this iRule and I've tried to optimize his code. (Link: https://devcentral.f5.com/s/feed/0D51T00006n6oY4SAI) Could you provide additional datails why the enforcement of the $Blackhole_Match happens in the DNS_RESPONSE event and not directly in the DNS_REQUEST event? Cheers, Kai