menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 007-papers chevron_right 063-深度调查CVE-2015-5477&CloudFlare Virtual DNS如何保护其用户.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    063-深度调查CVE-2015-5477&CloudFlare Virtual DNS如何保护其用户.md
    10.62 KB / 2021-07-17 00:01:34
        # 深度调查CVE-2015-5477&CloudFlare Virtual DNS如何保护其用户
    
    
    原文:[https://blog.cloudflare.com/a-deep-look-at-cve-2015-5477-and-how-cloudflare-virtual-dns-customers-are-protected/](https://blog.cloudflare.com/a-deep-look-at-cve-2015-5477-and-how-cloudflare-virtual-dns-customers-are-protected/)
    
    上周,ISC[发布](https://kb.isc.org/article/AA-01272)补丁,修复了BIND9 DNS服务器中的一个远程可利用漏洞。这个漏洞会导致服务器在处理某个数据包时发生崩溃。
    
    ![](http://drops.javaweb.org/uploads/images/2b6f32e53569458b5bef7afc6b4e2e6ea7fcde0c.jpg)
    
    公告中指出道,服务器在处理TKEY类型的查询时出现了一个错误,这个错误导致assertion fail,而这个fail又造成了服务器的崩溃。因为assertion是在查询解析的过程出现的,所以这个问题无法避免:服务器在接收到数据包时,首先要做的就是解析这个查询,然后再根据需要做出相应的决定。
    
    [TSIG](https://tools.ietf.org/html/rfc2845)是DNS服务器使用的一个验证彼此的协议。在这个协议的上下文中用到了[TKEY 查询](https://tools.ietf.org/html/rfc2930)。不同于常规的DNS查询,在TKEY查询的信息中,有一个EXTRA/ADDITIONAL节,在这个节中包含有关于TKEY类型的“meta”记录。
    
    ![](http://drops.javaweb.org/uploads/images/b33607cef8a95a9ab700672a53b0a97df52b43ae.jpg)
    
    因为现在利用数据包已经公开了,所以我觉着我们可以研究一下这个漏洞代码。那我们就看看这个崩溃实例的输出结果:
    
    ```
    03-Aug-2015 16:38:55.509 message.c:2352: REQUIRE(*name == ((void*)0)) failed, back trace  
    03-Aug-2015 16:38:55.510 #0 0x10001510d in assertion_failed()+0x5d  
    03-Aug-2015 16:38:55.510 #1 0x1001ee56a in isc_assertion_failed()+0xa  
    03-Aug-2015 16:38:55.510 #2 0x1000bc31d in dns_message_findname()+0x1ad  
    03-Aug-2015 16:38:55.510 #3 0x10017279c in dns_tkey_processquery()+0xfc  
    03-Aug-2015 16:38:55.510 #4 0x100016945 in ns_query_start()+0x695  
    03-Aug-2015 16:38:55.510 #5 0x100008673 in client_request()+0x18d3  
    03-Aug-2015 16:38:55.510 #6 0x1002125fe in run()+0x3ce  
    03-Aug-2015 16:38:55.510 exiting (due to assertion failure)  
    [1]    37363 abort (core dumped)  ./bin/named/named -f -c named.conf
    
    ```
    
    上面的崩溃代码对我们启示很大,它告诉我们这是由assertion失败导致的崩溃,并且告诉我们出现问题的地方在message.c:2352. 下面是漏洞代码摘要:
    
    ```
    // https://source.isc.org/git/bind9.git -- faa3b61 -- lib/dns/message.c    
    
        isc_result_t
        dns_message_findname(dns_message_t *msg, dns_section_t section,
                     dns_name_t *target, dns_rdatatype_t type,
                     dns_rdatatype_t covers, dns_name_t **name,
                     dns_rdataset_t **rdataset)
        {
            dns_name_t *foundname;
            isc_result_t result;    
    
            /*
             * XXX These requirements are probably too intensive, especially
             * where things can be NULL, but as they are they ensure that if
             * something is NON-NULL, indicating that the caller expects it
             * to be filled in, that we can in fact fill it in.
             */
            REQUIRE(msg != NULL);
            REQUIRE(VALID_SECTION(section));
            REQUIRE(target != NULL);
            if (name != NULL)
    ==>         REQUIRE(*name == NULL);    
    
        [...]
    
    ```
    
    这里,我们找到了一个函数"`dns_message_findname`",这个函数的作用是根据message section中给定的名称和类型,查找具有相同名称和类型的RRset。这个函数应用了一个很常见的C API:来获取结果,在结果中填充着caller传递的指针 (`dns_name_t **name, dns_rdataset_t **rdataset`)。
    
    ![](http://drops.javaweb.org/uploads/images/a9e04456884cbe1ea3c183ba00f2f6ec3503d936.jpg)
    
    很讽刺的是,这些指针的验证过程真的非常严格:如果这些指针没有指向(`dns_name_t *)NULL,REQUIRE assertion`就会fail并且服务器就会崩溃,也不会尝试恢复。调用这个函数的代码必须要格外小心地把指针传递到`NULL dns_name_t *`,函数会填充到代码中返回找到的名称。
    
    在非内存安全语言中,当assertion是无效的时候,崩溃就经常出现。因为当出现了异常时,程序很可能就没办法来清理自身的内存了。
    
    所以,在继续调查中,我们通过栈来查找非法调用。接下来就是`dns_tkey_processquery`,下面是简化摘要:
    
    ```
    // https://source.isc.org/git/bind9.git -- faa3b61 -- lib/dns/tkey.c    
    
    isc_result_t  
    dns_tkey_processquery(dns_message_t *msg, dns_tkeyctx_t *tctx,  
                  dns_tsig_keyring_t *ring)
    {
        isc_result_t result = ISC_R_SUCCESS;
        dns_name_t *qname, *name;
        dns_rdataset_t *tkeyset;    
    
        /*
         * Interpret the question section.
         */
        result = dns_message_firstname(msg, DNS_SECTION_QUESTION);
        if (result != ISC_R_SUCCESS)
            return (DNS_R_FORMERR);    
    
        qname = NULL;
        dns_message_currentname(msg, DNS_SECTION_QUESTION, &qname);    
    
        /*
         * Look for a TKEY record that matches the question.
         */
        tkeyset = NULL;
        name = NULL;
        result = dns_message_findname(msg, DNS_SECTION_ADDITIONAL, qname,
                          dns_rdatatype_tkey, 0, &name, &tkeyset);
        if (result != ISC_R_SUCCESS) {
            /*
             * Try the answer section, since that's where Win2000
             * puts it.
             */
            if (dns_message_findname(msg, DNS_SECTION_ANSWER, qname,
                         dns_rdatatype_tkey, 0, &name,
                         &tkeyset) != ISC_R_SUCCESS) {
                result = DNS_R_FORMERR;
                tkey_log("dns_tkey_processquery: couldn't find a TKEY "
                     "matching the question");
                goto failure;
            }
        }    
    
    [...]
    
    ```
    
    这里有`两个dns_message_findname`调用,因为我们寻找的是传递恶意name的一个调用,所以我们可以忽略掉第一个调用了,因为前面写着`name = NULL`;
    
    第二个调用就比较有意思了。在先调用了`dns_message_findname`之后,调用又重新使用了相同的`dns_name_t *name`,而且也没有把它设置成NULL。这可能就是bug出现的地方了。
    
    ![](http://drops.javaweb.org/uploads/images/5a94ee3b4c1d65830edbafd009a22c950500aa5c.jpg)
    
    现在的问题是,什么时候`dns_message_findname`会设置`name`,而不返回ISC_R_SUCCESS呢(这样的话if条件就能满足了)?现在,我们一起看一看完整的函数主体。
    
    ```
    // https://source.isc.org/git/bind9.git -- faa3b61 -- lib/dns/message.c    
    
    isc_result_t  
    dns_message_findname(dns_message_t *msg, dns_section_t section,  
                 dns_name_t *target, dns_rdatatype_t type,
                 dns_rdatatype_t covers, dns_name_t **name,
                 dns_rdataset_t **rdataset)
    {
        dns_name_t *foundname;
        isc_result_t result;    
    
        /*
         * XXX These requirements are probably too intensive, especially
         * where things can be NULL, but as they are they ensure that if
         * something is NON-NULL, indicating that the caller expects it
         * to be filled in, that we can in fact fill it in.
         */
        REQUIRE(msg != NULL);
        REQUIRE(VALID_SECTION(section));
        REQUIRE(target != NULL);
        if (name != NULL)
            REQUIRE(*name == NULL);
        if (type == dns_rdatatype_any) {
            REQUIRE(rdataset == NULL);
        } else {
            if (rdataset != NULL)
                REQUIRE(*rdataset == NULL);
        }    
    
        result = findname(&foundname, target,
                  &msg->sections[section]);    
    
        if (result == ISC_R_NOTFOUND)
            return (DNS_R_NXDOMAIN);
        else if (result != ISC_R_SUCCESS)
            return (result);    
    
        if (name != NULL)
            *name = foundname;    
    
        /*
         * And now look for the type.
         */
        if (type == dns_rdatatype_any)
            return (ISC_R_SUCCESS);    
    
        result = dns_message_findtype(foundname, type, covers, rdataset);
        if (result == ISC_R_NOTFOUND)
            return (DNS_R_NXRRSET);    
    
        return (result);
    }
    
    ```
    
    你能发现,`dns_message_findname`首先使用了`findnamet`来匹配与目标名称一致的记录,然后用`dns_message_findtype`来匹配目标类型。在这两个调用之间...`*name = foundname`!即使`dns_message_findname`在`DNS_SECTION_ADDITIONAL`中找到了`name == qname`的一条记录,但是类型不是`dns_rdatatype_tkey`这个`name`也会被填充并返回失败。 第二个`dns_message_findname`调用会触发恶意`name`,然后就一发不可收拾了。
    
    的确,补丁只是在第二个调用前添加了`name = NULL`(不,我们的出发点不是补丁程序,不然还有什么意思)
    
    ```
    diff --git a/lib/dns/tkey.c b/lib/dns/tkey.c  
    index 66210d5..34ad90b 100644  
    --- a/lib/dns/tkey.c
    +++ b/lib/dns/tkey.c
    @@ -654,6 +654,7 @@ dns_tkey_processquery(dns_message_t *msg, dns_tkeyctx_t *tctx,
              * Try the answer section, since that's where Win2000
              * puts it.
              */
    +        name = NULL;
             if (dns_message_findname(msg, DNS_SECTION_ANSWER, qname,
                          dns_rdatatype_tkey, 0, &name,
                          &tkeyset) != ISC_R_SUCCESS) {
    
    ```
    
    让我们再看一下bug触发的流程:
    
    *   收到一个**TKEY**类型查询,调用**dns_tkey_processquery**来解析这个查询
    *   **在EXTRA节中找到与查询名称相同的记录,**导致填充了`name`,但是这条记录并不是一个TKEY记录,导致`result != ISC_R_SUCCESS`
    *   再次调用`dns_message_findname`在ANS节中查找,现在是通过恶意的`name`参考
    *   assertion ***name != NULL**fail,`BIND崩溃`
    
    [@jfoote_](https://twitter.com/@jfoote_)通过[american fuzzy lop](http://lcamtuf.coredump.cx/afl/)模糊测试工具[发现了](https://twitter.com/ISCdotORG/status/626132833849905152)这个bug。模糊测试工具是一个自动工具,能自动向目标程序不断地提交异常输入,直至程序崩溃。你可以通过TKEY 查询+ 非TKEY EXTRA RR的组合来看看服务器最终是怎样崩溃的,并找到这个bug。
    
    Virtual DNS用户是安全的
    =================
    
    好消息![CloudFlare Virtual DNS](https://www.cloudflare.com/virtual-dns)用户的BIND服务器不会受到这次攻击的影响。如果需要的话,我们的自定义Go DNS服务器-PRDNS会首先解析所有的查询,并“消毒”,然后才会把查询转发回原来的服务器。
    
    因为Virtual DNS并不支持TSIG和TKEY(用于认证服务器到服务器之间的流量,并不是递归查询),所以没有必要在查询中转发EXTRA节的记录,Virtual DNS也没有这样做。这样面临的攻击风险就小了很多,而且也无法通过Virtual DNS来利用这个漏洞。
    
    现在还没有什么办法能防御这个漏洞:PRDNS总是会验证进入的数据包是不是良性的,确保查询是正常的,并且简化成最简单的形式,接着才会转发。
    
    links
    file_download