CVE-2020-6077
An exploitable denial-of-service vulnerability exists in the message-parsing functionality of Videolabs libmicrodns 0.1.0. When parsing mDNS messages, the implementation does not properly keep track of the available data in the message, possibly leading to an out-of-bounds read that would result in a denial of service. An attacker can send an mDNS message to trigger this vulnerability.
Videolabs libmicrodns 0.1.0
https://github.com/videolabs/libmicrodns
7.5 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer
The libmicrodns library is an mDNS resolver that aims to be simple and compatible cross-platform.
The function mdns_recv
reads and parses an mDNS message:
static int
mdns_recv(const struct mdns_conn* conn, struct mdns_hdr *hdr, struct rr_entry **entries)
{
uint8_t buf[MDNS_PKT_MAXSZ];
size_t num_entry, n;
ssize_t length;
struct rr_entry *entry;
*entries = NULL;
if ((length = recv(conn->sock, (char *) buf, sizeof(buf), 0)) < 0) // [1]
return (MDNS_NETERR);
const uint8_t *ptr = mdns_read_header(buf, length, hdr); // [2]
n = length;
num_entry = hdr->num_qn + hdr->num_ans_rr + hdr->num_add_rr;
for (size_t i = 0; i < num_entry; ++i) {
entry = calloc(1, sizeof(struct rr_entry));
if (!entry)
goto err;
ptr = rr_read(ptr, &n, buf, entry, i >= hdr->num_qn); // [3]
if (!ptr) {
free(entry);
errno = ENOSPC;
goto err;
}
entry->next = *entries;
*entries = entry;
}
...
}
At [1], a message is read from the network. The 12-bytes mDNS header is then parsed at [2]. Based on the header info, the loop parses each resource record (“RR”) using the function rr_read
[3], which in turn calls rr_read_RR
and then rr_decode
.
#define advance(x) ptr += x; *n -= x
/*
* Decodes a DN compressed format (RFC 1035)
* e.g "\x03foo\x03bar\x00" gives "foo.bar"
*/
static const uint8_t *
rr_decode(const uint8_t *ptr, size_t *n, const uint8_t *root, char **ss)
{
char *s;
s = *ss = malloc(MDNS_DN_MAXSZ);
if (!s)
return (NULL);
if (*ptr == 0) { // [9]
*s = '\0';
advance(1);
return (ptr);
}
while (*ptr) { // [4]
size_t free_space;
uint16_t len;
free_space = *ss + MDNS_DN_MAXSZ - s;
len = *ptr;
advance(1);
/* resolve the offset of the pointer (RFC 1035-4.1.4) */
if ((len & 0xC0) == 0xC0) {
const uint8_t *p;
char *buf;
size_t m;
if (*n < sizeof(len)) // [5]
goto err;
len &= ~0xC0;
len = (len << 8) | *ptr;
advance(1);
p = root + len; // [8]
m = ptr - p + *n; // [6]
rr_decode(p, &m, root, &buf);
if (free_space <= strlen(buf)) {
free(buf);
goto err;
}
(void) strcpy(s, buf);
free(buf);
return (ptr);
}
if (*n <= len || free_space <= len) // [7]
goto err;
strncpy(s, (const char *) ptr, len);
advance(len);
s += len;
*s++ = (*ptr) ? '.' : '\0'; // [10]
}
advance(1);
return (ptr);
err:
free(*ss);
return (NULL);
}
The function rr_decode
expects 4 parameters:
ptr
: the pointer to the start of the label to parsen
: the number of remaining bytes in the message, starting from ptrroot
: the pointer to the start of the mDNS messagess
: buffer used to build the domain nameThe task of this function is to parse a domain name in a given resource record, according to RFC 1035.
To walk through the message, the advance
macro is used to move ptr
forward and to decrement *n
(the number of bytes left in the message) accordingly.
Also note how the code relies on the value of *n
to make decisions [5] [6] [7] about the loop [4] termination.
In the code above, the comparison at [5] is performed incorrectly. Because of the sizeof
, the code checks if *n
is either 0
or 1
. This means that the pointer p
at [8] can point from root
up to root + 16383
. The function would then call recursively, reading out of bounds and possibly crashing at [9] or [10] when dereferencing ptr
.
Additionally, before rr_decode
is called, the function mdns_read_header
[2] parses the mDNS header:
static const uint8_t *
mdns_read_header(const uint8_t *ptr, size_t n, struct mdns_hdr *hdr)
{
if (n <= sizeof(struct mdns_hdr)) {
errno = ENOSPC;
return NULL;
}
ptr = read_u16(ptr, &n, &hdr->id);
ptr = read_u16(ptr, &n, &hdr->flags);
ptr = read_u16(ptr, &n, &hdr->num_qn);
ptr = read_u16(ptr, &n, &hdr->num_ans_rr);
ptr = read_u16(ptr, &n, &hdr->num_auth_rr);
ptr = read_u16(ptr, &n, &hdr->num_add_rr);
return ptr;
}
The function read_u16
takes care of reading a 2-bytes unsigned integer, and decrementing n
accordingly [11]:
static inline const uint8_t *read_u16(const uint8_t *p, size_t *s, uint16_t *v)
{
*v = 0;
*v |= *p++ << 8;
*v |= *p++ << 0;
*s -= 2; // [11]
return (p);
}
However, as we can see from the definition of mdns_read_header
and the way it’s called [12], n
is passed to mdns_read_header
by value rather than by reference, losing any modification performed by read_u16
.
static int
mdns_recv(const struct mdns_conn* conn, struct mdns_hdr *hdr, struct rr_entry **entries)
{
uint8_t buf[MDNS_PKT_MAXSZ];
size_t num_entry, n;
ssize_t length;
struct rr_entry *entry;
*entries = NULL;
if ((length = recv(conn->sock, (char *) buf, sizeof(buf), 0)) < 0)
return (MDNS_NETERR);
const uint8_t *ptr = mdns_read_header(buf, length, hdr); // [12]
n = length;
This makes the *n
value to be 0xC bytes (the header size) bigger than it should. While, in absence of other bugs, this last issue alone may not cause a direct impact on the service, it may be used together with the first bug presented in this advisory in order to evade detection.
Finally, while this bug alone would result in a simple denial-of-service, because of other bugs in the code, it may be used to trigger the same double-free that was reported in TALOS-2020-0995.
2020-01-30 - Vendor Disclosure
2020-03-20 - Vendor Patched
2020-03-23 - Public Release
Discovered by Claudio Bozzato of Cisco Talos