[
RFC 1035] defines the DNS message compression scheme that can be used to reduce the size of messages. When it is used, an entire domain name or several name labels are replaced with a (compression) pointer to a prior occurrence of the same name.
The compression pointer is a combination of two octets: the two most significant bits are set to 1, and the remaining 14 bits are the OFFSET field. This field specifies the offset from the beginning of the DNS header, at which another domain name or label is located:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1 1| OFFSET |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
The message compression scheme explicitly allows a domain name to be represented as one of the following: (1) a sequence of unpacked labels ending with a zero octet, (2) a pointer, or (3) a sequence of labels ending with a pointer.
However, [
RFC 1035] does not explicitly state that blindly following compression pointers of any kind can be harmful [
DNS-COMPRESSION], as we could not have had any assumptions about various implementations that would follow.
Yet, any DNS packet parser that attempts to decompress domain names without validating the value of OFFSET is likely susceptible to memory corruption bugs and buffer overruns. These bugs make it easier to perform Denial-of-Service attacks and may result in successful Remote Code Execution attacks.
Pseudocode that illustrates a typical example of a broken domain name parsing implementation is shown below (
Figure 1):
1: decompress_domain_name(*name, *dns_payload) {
2:
3: name_buffer[255];
4: copy_offset = 0;
5:
6: label_len_octet = name;
7: dest_octet = name_buffer;
8:
9: while (*label_len_octet != 0x00) {
10:
11: if (is_compression_pointer(*label_len_octet)) {
12: ptr_offset = get_offset(label_len_octet,
label_len_octet+1);
13: label_len_octet = dns_payload + ptr_offset + 1;
14: }
15:
16: else {
17: length = *label_len_octet;
18: copy(dest_octet + copy_offset,
label_len_octet+1, *length);
19:
20: copy_offset += length;
21: label_len_octet += length + 1;
22: }
23:
24: }
25: }
Such implementations typically have a dedicated function for decompressing domain names (for example, see [
CVE-2020-24338] and [
CVE-2020-27738]). Among other parameters, these functions may accept a pointer to the beginning of the first name label within an RR ("name") and a pointer to the beginning of the DNS payload to be used as a starting point for the compression pointer ("dns_payload"). The destination buffer for the domain name ("name_buffer") is typically limited to 255 bytes as per [
RFC 1035] and can be allocated either in the stack or in the heap memory region.
The code of the function in
Figure 1 reads the domain name label by label from an RR until it reaches the NUL octet ("0x00") that signifies the end of a domain name. If the current label length octet ("label_len_octet") is a compression pointer, the code extracts the value of the compression offset and uses it to "jump" to another label length octet. If the current label length octet is not a compression pointer, the label bytes will be copied into the name buffer, and the number of bytes copied will correspond to the value of the current label length octet. After the copy operation, the code will move on to the next label length octet.
The first issue with this implementation is due to unchecked compression offset values. The second issue is due to the absence of checks that ensure that a pointer will eventually arrive at a decompressed domain label. We describe these issues in more detail below.
[
RFC 1035] states that a compression pointer is "a pointer to a prior occurance [sic] of the same name." Also, according to [
RFC 1035], the maximum size of DNS packets that can be sent over UDP is limited to 512 octets.
The pseudocode in
Figure 1 violates these constraints, as it will accept a compression pointer that forces the code to read outside the bounds of a DNS packet. For instance, a compression pointer set to "0xffff" will produce an offset of 16383 octets, which is most definitely pointing to a label length octet somewhere past the bounds of the original DNS packet. Supplying such offset values will most likely cause memory corruption issues and may lead to Denial-of-Service conditions (e.g., a Null pointer dereference after "label_len_octet" is set to an invalid address in memory). For additional examples, see [
CVE-2020-25767], [
CVE-2020-24339], and [
CVE-2020-24335].
The pseudocode in
Figure 1 allows jumping from a compression pointer to another compression pointer and does not restrict the number of such jumps. That is, if a label length octet that is currently being parsed is a compression pointer, the code will perform a jump to another label, and if that other label is a compression pointer as well, the code will perform another jump, and so forth until it reaches a decompressed label. This may lead to unforeseen side effects that result in security issues.
Consider the DNS packet excerpt illustrated below:
+----+----+----+----+----+----+----+----+----+----+----+----+
+0x00 | ID | FLAGS | QDCOUNT | ANCOUNT | NSCOUNT | ARCOUNT |
+----+----+----+----+----+----+----+----+----+----+----+----+
->+0x0c |0xc0|0x0c| TYPE | CLASS |0x04| t | e | s | t |0x03|
| +----+--|-+----+----+----+----+----+----+----+----+----+----+
| +0x18 | c | o| | m |0x00| TYPE | CLASS | ................ |
| +----+--|-+----+----+----+----+----+----+----+----+----+----+
| |
-----------------
The packet begins with a DNS header at offset +0x00, and its DNS payload contains several RRs. The first RR begins at an offset of 12 octets (+0x0c); its first label length octet is set to the value "0xc0", which indicates that it is a compression pointer. The compression pointer offset is computed from the two octets "0xc00c" and is equal to 12. Since the broken implementation in
Figure 1 follows this offset value blindly, the pointer will jump back to the first octet of the first RR (+0x0c) over and over again. The code in
Figure 1 will enter an infinite-loop state, since it will never leave the "TRUE" branch of the "while" loop.
Apart from achieving infinite loops, the implementation flaws in
Figure 1 make it possible to achieve various pointer loops that have other undesirable effects. For instance, consider the DNS packet excerpt shown below:
+----+----+----+----+----+----+----+----+----+----+----+----+
+0x00 | ID | FLAGS | QDCOUNT | ANCOUNT | NSCOUNT | ARCOUNT |
+----+----+----+----+----+----+----+----+----+----+----+----+
->+0x0c |0x04| t | e | s | t |0xc0|0x0c| ...................... |
| +----+----+----+----+----+----+--|-+----+----+----+----+----+
| |
------------------------------------------
With such a domain name, the implementation in
Figure 1 will first copy the domain label at offset "0xc0" ("test"); it will then fetch the next label length octet, which happens to be a compression pointer ("0xc0"). The compression pointer offset is computed from the two octets "0xc00c" and is equal to 12 octets. The code will jump back to offset "0xc0" where the first label "test" is located. The code will again copy the "test" label and then jump back to it, following the compression pointer, over and over again.
Figure 1 does not contain any logic that restricts multiple jumps from the same compression pointer and does not ensure that no more than 255 octets are copied into the name buffer ("name_buffer"). In fact,
-
the code will continue to write the label "test" into it, overwriting the name buffer and the stack of the heap metadata.
-
attackers would have a significant degree of freedom in constructing shell code, since they can create arbitrary copy chains with various combinations of labels and compression pointers.
Therefore, blindly following compression pointers may lead not only to Denial-of-Service conditions, as pointed out by [
DNS-COMPRESSION], but also to successful Remote Code Execution attacks, as there may be other implementation issues present within the corresponding code.
Some implementations may not follow [
RFC 1035], which states:
The first two bits are ones. This allows a pointer to be distinguishedfrom a label, since the label must begin with two zero bits becauselabels are restricted to 63 octets or less. (The 10 and 01 combinationsare reserved for future use.)
Figures [
2] and [
3] show pseudocode that implements two functions that check whether a given octet is a compression pointer;
Figure 2 shows a correct implementation, and
Figure 3 shows an incorrect (broken) implementation.
1: unsigned char is_compression_pointer(*octet) {
2: if ((*octet & 0xc0) == 0xc0)
3: return true;
4: } else {
5: return false;
6: }
7: }
1: unsigned char is_compression_pointer(*octet) {
2: if (*octet & 0xc0) {
3: return true;
4: } else {
5: return false;
6: }
7: }
The correct implementation (
Figure 2) ensures that the two most significant bits of an octet are both set, while the broken implementation (
Figure 3) would consider an octet with only one of the two bits set to be a compression pointer. This is likely an implementation mistake rather than an intended violation of [
RFC 1035], because there are no benefits in supporting such compression pointer values. The implementations related to [
CVE-2020-24338] and [
CVE-2020-24335] had a broken compression pointer check, similar to the code shown in
Figure 3.
While incorrect implementations alone do not lead to vulnerabilities, they may have unforeseen side effects when combined with other vulnerabilities. For instance, the first octet of the value "0x4130" may be incorrectly interpreted as a label length by a broken implementation. Such a label length (65) is invalid and is larger than 63 (as per [
RFC 1035]); a packet that has this value should be discarded. However, the function shown in
Figure 3 will consider "0x41" to be a valid compression pointer, and the packet may pass the validation steps.
This might give attackers additional leverage for constructing payloads and circumventing the existing DNS packet validation mechanisms.
The first occurrence of a compression pointer in an RR (an octet with the two highest bits set to 1) must resolve to an octet within a DNS record with a value that is greater than 0 (i.e., it must not be a Null-terminator) and less than 64. The offset at which this octet is located must be smaller than the offset at which the compression pointer is located; once an implementation makes sure of that, compression pointer loops can never occur.
In small DNS implementations (e.g., embedded TCP/IP stacks), support for nested compression pointers (pointers that point to a compressed name) should be discouraged: there is very little to be gained in terms of performance versus the high probability of introducing errors such as those discussed above.
The code that implements domain name parsing should check the offset with respect to not only the bounds of a packet but also its position with respect to the compression pointer in question. A compression pointer must not be "followed" more than once. We have seen several implementations using a check that ensures that a compression pointer is not followed more than several times. A better alternative may be to ensure that the target of a compression pointer is always located before the location of the pointer in the packet.