0

深度调查CVE-2015-5477&CloudFlare Virtual DNS如何保护其用户 | WooYun知识库

 6 years ago
source link:
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

深度调查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/

上周,ISC 发布补丁,修复了BIND9 DNS服务器中的一个远程可利用漏洞。这个漏洞会导致服务器在处理某个数据包时发生崩溃。

公告中指出道,服务器在处理TKEY类型的查询时出现了一个错误,这个错误导致assertion fail,而这个fail又造成了服务器的崩溃。因为assertion是在查询解析的过程出现的,所以这个问题无法避免:服务器在接收到数据包时,首先要做的就是解析这个查询,然后再根据需要做出相应的决定。

TSIG是DNS服务器使用的一个验证彼此的协议。在这个协议的上下文中用到了TKEY 查询。不同于常规的DNS查询,在TKEY查询的信息中,有一个EXTRA/ADDITIONAL节,在这个节中包含有关于TKEY类型的“meta”记录。

因为现在利用数据包已经公开了,所以我觉着我们可以研究一下这个漏洞代码。那我们就看看这个崩溃实例的输出结果:

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)。

很讽刺的是,这些指针的验证过程真的非常严格:如果这些指针没有指向(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出现的地方了。

现在的问题是,什么时候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_findnameDNS_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_通过 american fuzzy lop 模糊测试工具发现了这个bug。模糊测试工具是一个自动工具,能自动向目标程序不断地提交异常输入,直至程序崩溃。你可以通过TKEY 查询+ 非TKEY EXTRA RR的组合来看看服务器最终是怎样崩溃的,并找到这个bug。

Virtual DNS用户是安全的

好消息! CloudFlare Virtual DNS用户的BIND服务器不会受到这次攻击的影响。如果需要的话,我们的自定义Go DNS服务器-PRDNS会首先解析所有的查询,并“消毒”,然后才会把查询转发回原来的服务器。

因为Virtual DNS并不支持TSIG和TKEY(用于认证服务器到服务器之间的流量,并不是递归查询),所以没有必要在查询中转发EXTRA节的记录,Virtual DNS也没有这样做。这样面临的攻击风险就小了很多,而且也无法通过Virtual DNS来利用这个漏洞。

现在还没有什么办法能防御这个漏洞:PRDNS总是会验证进入的数据包是不是良性的,确保查询是正常的,并且简化成最简单的形式,接着才会转发。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK