CVE-2020-8620
An assertion failure exists within the Internet Systems Consortium’s BIND server versions 9.16.1 through 9.17.1 when processing TCP traffic via the libuv library. Due to a length specified within a callback for the library, flooding the server’s TCP port used for larger DNS requests (AXFR) can cause the libuv library to pass a length to the server which will violate an assertion check in the server’s verifications. This assertion check will terminate the service resulting in a denial of service condition. An attacker can flood the port with unauthenticated packets in order to trigger this vulnerability.
Internet Systems Consortium BIND 9.16.1
Internet Systems Consortium BIND 9.16.2
Internet Systems Consortium BIND 9.16.3
Internet Systems Consortium BIND 9.17.0
Internet Systems Consortium BIND 9.17.1
7.5 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-617 - Reachable Assertion
The BIND nameserver is considered the reference implementation of the Domain Name System of the Internet. It is capable of being an authoritative name server as well as a recursive cache for domain name queries on a network.
The BIND nameserver is based on a custom event queueing system that wraps around the libuv
library (http://libuv.org) for performing asynchronous I/O as needed by the server. The libuv
library was introduced as a new network manager during the release of version 9.16 in order to allow the server to be used on both the Posix and Windows environments and to simplify the management of processing network I/O distributed amongst a configurable number of threads within the server.
The BIND nameserver combines its own queue for scheduling with the libuv
library in order to process requests and queries asynchronously for both the UDP and TCP protocols. In order to accomplish this, the server must first initialize the libuv
library by allocating and initializing the uv_loop_t
which is a handle directly used by the library to manage the event loop used by the server. After the server has initialized its core memory handling functions in the setup phase of the daemon, the isc_nm_start
function will be used to construct a uv_loop_t
for a number of workers using the uv_loop_init
function at [1]. After initializing the loop that is to be used for each worker, the server will then allocate space for a receive buffer at [2] and then assign it into the context of the worker. This loop is then used to bind to the configured UDP and TCP ports as used by the server.
lib/isc/netmgr/netmgr.c:142
isc_nm_t *
isc_nm_start(isc_mem_t *mctx, uint32_t workers) {
isc_nm_t *mgr = NULL;
char name[32];
mgr = isc_mem_get(mctx, sizeof(*mgr));
*mgr = (isc_nm_t){ .nworkers = workers };
isc_mem_attach(mctx, &mgr->mctx);
...
r = uv_loop_init(&worker->loop); // [1] Initialize a uv_loop_t for each worker
RUNTIME_CHECK(r == 0);
...
r = uv_async_init(&worker->loop, &worker->async, async_cb);
RUNTIME_CHECK(r == 0);
...
worker->ievents = isc_queue_new(mgr->mctx, 128);
worker->ievents_prio = isc_queue_new(mgr->mctx, 128);
worker->recvbuf = isc_mem_get(mctx, ISC_NETMGR_RECVBUF_SIZE); // [2] Allocate a receive buffer of the size ISC_NETMGR_RECVBUF_SIZE
...
After the server has initialized each worker and bound to the configured ports, the server must use libuv
to assign a callback to dispatch to when receiving a connection on a port. The callback that is used for processing TCP is the following dnslisten_acceptcb
function. After performing a few validations, at [3] the function will call the isc_nm_read
function with a callback, dnslisten_readcb
, as its second parameter. This callback will be stored into a structure and then later passed to libuv
in order to inform the library what to call when the server needs to read data from a connected TCP client.
lib/isc/netmgr/tcpdns.c:98
/*
* Accept callback for TCP-DNS connection.
*/
static void
dnslisten_acceptcb(isc_nmhandle_t *handle, isc_result_t result, void *cbarg) {
isc_nmsocket_t *dnslistensock = (isc_nmsocket_t *)cbarg;
isc_nmsocket_t *dnssock = NULL;
REQUIRE(VALID_NMSOCK(dnslistensock));
REQUIRE(dnslistensock->type == isc_nm_tcpdnslistener);
/* If accept() was unnsuccessful we can't do anything */
if (result != ISC_R_SUCCESS) {
return;
}
...
isc_nm_read(handle, dnslisten_readcb, dnssock); // [3] Pass the dnslisten_readcb callback for reading as a parameter
Inside the isc_nm_read
function, the server will then assign the dnslisten_readcb
callback that was passed as its second parameter into a isc_nmsocket_t
structure at [4] as sock->rcb.recv
. After preparing the isc_nmsocket_t
structure, the server will fetch a new event and then assign the isc_nmsocket_t
into it. Eventually at [5], the function will pass the event as the second parameter to the isc__nm_async_startread
function. The isc__nm_async_startread
function is directly responsible for calling into the libuv
library with the necessary callbacks in order for the server to process any received DNS packets.
lib/isc/netmgr/tcp.c:521
isc_result_t
isc_nm_read(isc_nmhandle_t *handle, isc_nm_recv_cb_t cb, void *cbarg) {
isc_nmsocket_t *sock = NULL;
isc__netievent_startread_t *ievent = NULL;
...
sock = handle->sock;
sock->rcb.recv = cb; // [4] Store callback into sock
sock->rcbarg = cbarg;
...
ievent = isc__nm_get_ievent(sock->mgr, netievent_tcpstartread);
ievent->sock = sock; // Assign sock into the event
if (sock->tid == isc_nm_tid()) {
isc__nm_async_startread(&sock->mgr->workers[sock->tid], // [5] Pass the event containing the callback as the second parameter
(isc__netievent_t *)ievent);
isc__nm_put_ievent(sock->mgr, ievent);
} else {
...
return (ISC_R_SUCCESS);
}
Once inside the isc__nm_async_startread
function, the isc_nmsocket_t
will then be extracted from a field belonging to the event. After starting a timer with libuv
in order to determine when to timeout the connection, the server will execute the uv_read_start
function at [6]. The uv_read_start
function belongs to libuv
and is used in order to inform the library which callbacks to use when allocating space for the receive buffer during processing of a TCP stream and which callback to actually use for processing the data from the buffer that was received. The vulnerability referred to by this document is specifically due to the way these two callbacks are implemented by the server.
lib/isc/netmgr/tcp.c:548
void
isc__nm_async_startread(isc__networker_t *worker, isc__netievent_t *ev0) {
isc__netievent_startread_t *ievent = (isc__netievent_startread_t *)ev0;
isc_nmsocket_t *sock = ievent->sock;
int r;
...
r = uv_read_start(&sock->uv_handle.stream, isc__nm_alloc_cb, read_cb); // [6] Use libuv to assign callbacks for reading
...
}
When the libuv
library needs the server to allocate a buffer to receive packet data into, it will call the following function. This function’s responsibility is to allocate a buffer to receive packet data into, and then write the buffer along with its length into one of the function’s parameters. The libuv
library provides the uv_buf_t
object to modify and a suggested size in its arguments. The implementation chosen by the server was to preallocate the read buffer for each worker during the setup process of the worker. Therefore in this function, the server will only need to assign the preallocated buffer and its size at [7] which prevents needing to allocate during the receiving of a packet.
lib/isc/netmgr/netmgr.c:972
void
isc__nm_alloc_cb(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
isc_nmsocket_t *sock = uv_handle_get_data(handle);
isc__networker_t *worker = NULL;
REQUIRE(VALID_NMSOCK(sock));
REQUIRE(isc__nm_in_netthread());
REQUIRE(size <= ISC_NETMGR_RECVBUF_SIZE);
worker = &sock->mgr->workers[sock->tid];
INSIST(!worker->recvbuf_inuse);
buf->base = worker->recvbuf; // [7] Assign worker's receive buffer into buf->base
worker->recvbuf_inuse = true;
buf->len = ISC_NETMGR_RECVBUF_SIZE; // [7] Assign the length for the worker's receive buffer.
}
The size that was used for the allocation of the receive buffer and is assigned to the buffer length for libuv
to use is defined in the following file. As described in the comments, this length is taken from the libuv
source and contains the maximum size of a message on Posix platforms. Due to a smaller buffer size being used for the Windows platforms, the vulnerability described by this document does not affect that class of particular platforms.
lib/isc/netmgr/netmgr-int.h:38
#if !defined(WIN32)
/*
* New versions of libuv support recvmmsg on unices.
* Since recvbuf is only allocated per worker allocating a bigger one is not
* that wasteful.
* 20 here is UV__MMSG_MAXWIDTH taken from the current libuv source, nothing
* will break if the original value changes.
*/
#define ISC_NETMGR_RECVBUF_SIZE (20 * 65536)
#else
#define ISC_NETMGR_RECVBUF_SIZE (65536)
#endif
After the libuv
library has dispatched to the allocation callback in order to allocate a buffer to read packet data into, the library can now execute the callback that is responsible for processing the actual data from the packet. The server implements this with the following read_cb
function. This function will simply take the uv_buf_t
and length that is passed as parameters to assign them into an isc_region_t
at [8]. After initializing the isc_region_t
, the server will then dispatch to the dnslisten_readcb
callback at [9] that was previously assigned in the isc_nm_read
function.
lib/isc/netmgr/tcp.c:639
static void
read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
isc_nmsocket_t *sock = uv_handle_get_data((uv_handle_t *)stream);
REQUIRE(VALID_NMSOCK(sock));
REQUIRE(buf != NULL);
if (nread >= 0) {
isc_region_t region = { .base = (unsigned char *)buf->base, // [8] Initialize the isc_region_t with the buffer and size from libuv
.length = nread };
if (sock->rcb.recv != NULL) {
sock->rcb.recv(sock->tcphandle, ®ion, sock->rcbarg); // [9] Pass the region to the worker's callback
}
...
The dnslisten_readcb
function has the responsibility for taking the packet data that was dispatched as a parameter by libuv
, and aggregating it into a buffer containing a full DNS packet packet confirming to RFC1035. This is done by taking the packet data and its length from the region
parameter of the type isc_region_t
which was initialized by the calling function and using it to grow a buffer that will later be processed. At [10], the packet data and its length are extracted from the isc_region_t
and assigned into local variables. Once determining the length, it is then used to check if the current packet buffer size that the server will process is large enough to fit the newly read data from the TCP socket. If the sum of the current buffer length and the number of bytes read from the packet is larger than the buffer size, then at [11] the server will use the alloc_dnsbuf
function to reallocate the buffer to fit the calculated size. After performing the resize, at [12] the server will copy the new packet data from the isc_region_t
directly into the current packet buffer and then process it at [13].
lib/isc/netmgr/tcpdns.c:198
static void
dnslisten_readcb(isc_nmhandle_t *handle, isc_region_t *region, void *arg) {
isc_nmsocket_t *dnssock = (isc_nmsocket_t *)arg;
unsigned char *base = NULL;
bool done = false;
size_t len;
...
base = region->base; // [10] Extract the libuv buffer and its length from the region
len = region->length;
if (dnssock->buf_len + len > dnssock->buf_size) {
alloc_dnsbuf(dnssock, dnssock->buf_len + len); // [11] Allocate the DNS buffer if it is too small
}
memmove(dnssock->buf + dnssock->buf_len, base, len); // [12] Copy new packet data to the end of the current packet buffer
dnssock->buf_len += len;
...
do {
isc_result_t result;
isc_nmhandle_t *dnshandle = NULL;
result = processbuffer(dnssock, &dnshandle); // [13] Process the contents off the packet data
...
When reallocating the packet buffer, the following alloc_dnsbuf
function is used. The comment in front of this function indicates that the buffer size being defined for NM_BIG_BUF
is for two full DNS packet lengths as this should be enough. However, due to the way that libuv
works the length that was allocated for the worker receive buffer that was assigned in isc__nm_alloc_cb
is used instead. This length is 20 * 64k
and is passed to the read
system call by the libuv
library. This results in the value of the len
field that was passed to this function capable of being up to 0x140000 bytes. At [14], an assertion is used to validate that the length is less than the NM_BIG_BUF
definition. If the length does not validate, then the assertion will log itself and follow up by calling the abort
library function. This will directly terminate the server resulting in a denial of service condition.
lib/isc/netmgr/tcpdns.c:58
/*
* Two full DNS packets with lengths.
* netmgr receives 64k at most so there's no risk
* of overrun.
*/
#define NM_BIG_BUF (65535 + 2) * 2
static inline void
alloc_dnsbuf(isc_nmsocket_t *sock, size_t len) {
REQUIRE(len <= NM_BIG_BUF); // [14] Assertion
if (sock->buf == NULL) {
/* We don't have the buffer at all */
size_t alloc_len = len < NM_REG_BUF ? NM_REG_BUF : NM_BIG_BUF;
sock->buf = isc_mem_allocate(sock->mgr->mctx, alloc_len);
sock->buf_size = alloc_len;
} else {
/* We have the buffer but it's too small */
sock->buf = isc_mem_reallocate(sock->mgr->mctx, sock->buf,
NM_BIG_BUF);
sock->buf_size = NM_BIG_BUF;
}
}
First we attach to the process, and then resume its execution.
$ gdb -p `pgrep named`
(gdb) c
Continuing.
After running the provided proof-of-concept, gdb will break due to the SIGABRT
signal that was raised by the assertion.
(gdb)
Thread 5 "isc-net-0003" received signal SIGABRT, Aborted.
[Switching to Thread 0x7f95a443a700 (LWP 7)]
0x00007f95a822a18b in raise () from target:/lib/x86_64-linux-gnu/libc.so.6
The following backtrace is at the time of the signal being dispatched to the process.
(gdb) bt
#0 0x00007f95a822a18b in raise () from target:/lib/x86_64-linux-gnu/libc.so.6
#1 0x00007f95a8209859 in abort () from target:/lib/x86_64-linux-gnu/libc.so.6
#2 0x0000563409bc75c6 in assertion_failed (file=0x563409f56eff "tcpdns.c", line=66, type=isc_assertiontype_require,
cond=0x563409f56ee8 "len <= (65535 + 2) * 2") at ./main.c:260
#3 0x0000563409e83070 in isc_assertion_failed (file=0x563409f56eff "tcpdns.c", line=66, type=isc_assertiontype_require,
cond=0x563409f56ee8 "len <= (65535 + 2) * 2") at assertions.c:46
#4 0x0000563409ea9453 in alloc_dnsbuf (sock=0x7f9598ce6be0, len=1310749) at tcpdns.c:66
#5 0x0000563409ea9d2a in dnslisten_readcb (handle=0x7f957c003180, region=0x7f95a44369b0, arg=0x7f9598ce6be0) at tcpdns.c:223
#6 0x0000563409ea696a in read_cb (stream=0x7f9598ce6920, nread=1310720, buf=0x7f95a4436a20) at tcp.c:651
#7 0x00007f95a841bad1 in ?? () from target:/lib/x86_64-linux-gnu/libuv.so.1
#8 0x00007f95a841c608 in ?? () from target:/lib/x86_64-linux-gnu/libuv.so.1
#9 0x00007f95a8421ab0 in uv.io_poll () from target:/lib/x86_64-linux-gnu/libuv.so.1
#10 0x00007f95a84117ac in uv_run () from target:/lib/x86_64-linux-gnu/libuv.so.1
#11 0x0000563409ea0bc2 in nm_thread (worker0=0x56340b6dd048) at netmgr.c:481
#12 0x00007f95a83e5609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#13 0x00007f95a8306103 in clone () from target:/lib/x86_64-linux-gnu/libc.so.6
In frame 5 belonging to the dnslisten_readcb
function, we can see that the region length corresponds directly to the value defined for ISC_NETMGR_RECVBUF_SIZE
.
(gdb) frame 5
#5 0x0000563409ea9d2a in dnslisten_readcb (handle=0x7f957c003180, region=0x7f95a44369b0, arg=0x7f9598ce6be0) at tcpdns.c:223
223 alloc_dnsbuf(dnssock, dnssock->buf_len + len);
(gdb) p *regio
$3 = {base = 0x7f95a443b010 "l", length = 1310720}
The current buffer size that is to be grown is only 0x20002 bytes.
(gdb) p dnssock.buf_size
$7 = 131074
(gdb) p dnssock.buf_len
$8 = 29
To use the provided proof-of-concept, it must first be modified. Change both the DST_IP and DST_PORT variables to point to the host the BIND daemon is listening on, and then run it with Python 2.x.
Flood protection could mitigate this denial of service if configured properly.
2020-07-02 - Initial contact
2020-07-06 - Vendor assigned CVE-2020-8620
2020-08-04 - Vendor patched
2020-08-20 - Public Release
Discovered by Emanuel Almeida of Cisco Systems, Inc.