The Domain Name System (DNS) is a hierarchical distributed database that implements a global naming scheme for resources available on the Internet. It provides the infrastructure for mapping domain names to IP addresses as well as key data used to interpret email addresses. When people access resources on the Internet, they typically do so by using names such as www.google.com and abuse@comcast.net. Their computers use DNS to translate these names into the IP addresses suitable for use with Internet protocols. Obviously, text names are far easier for people to work with than numbers. There's a reason you don't hear people say "Man, 66.35.250.151 has really gone downhill lately." Domain Names and Resource RecordsThe DNS database is organized as a tree data structure, with a single root node at the top (see Figure 16-15 for a very simple example of such a tree). For the sake of clarity, this diagram omits some domains that would be necessary to make the database functional. Every node (and leaf) in the tree is called a domain, and a domain's child nodes are called its subdomains. Each domain has a label, which is a short text name such as com, mail, www, or food. A domain name is a series of labels, separated by dots, that uniquely identifies a node in the tree by tracing the full path from the specified domain to the root domain. For example, the domain name www.google.com specifies a domain labeled www that's a subdomain of google.com. The google.com domain is a subdomain of the com domain, and com is a subdomain of the root domain. The root domain has an empty label, which is usually omitted in casual discussion. In configuration files and technical discussions, however, it's usually represented by a trailing dotwww.google.com., for example. Figure 16-15. DNS tree data structureEach domain owns a set of zero or more resource records, which describe attributes of that domain. In general, you work with DNS by asking about a domain name. The response you get is a set of resource records owned by that domain name. Every resource record has five elements, described in Table 16-2.
Name Servers and ResolversBefore you can understand how resource records are used in practice, you need a brief review of name servers and resolvers. The DNS database is distributed among thousands of systems around the world, which are called name servers. The responsibility for maintaining this vast database is divided among the thousands of administrators of these systems; each administrator is responsible for a small piece of the global namespace. To facilitate this division of labor, the domain namespace is split up into sections called zones. The code responsible for querying DNS on behalf of user applications is called resolver code. It takes a request from a user, tough function such as gethostbyname(), and begins asking name servers it knows about to try to hunt down an authoritative resource record with the answer. There are two basic kinds of name servers: recursive and nonrecursive. Nonrecursive name servers are the most straightforward. They answer questions only about the zones they are responsible for. They have all this information in memory, so they don't need to query the DNS infrastructure for further information. (Note that they also have some delegation and glue information memorized, which you learn about through the rest of this chapter). Nonrecursive name servers give you an authoritative answer or tell you to go ask someone else. Recursive name servers are a different animal. If they don't know the answer to a query offhand, they take it upon themselves to go find the answer. If they are successful, they consolidate all the intermediate findings into a nice concise answer for the client. There are also two kinds of resolvers. A fully functional resolver can interrogate DNS to hunt down answers to user questions. It knows what to do when a nonrecursive name server doesn't have the answer. A stub resolver, on the other hand, is quite comfortable letting a recursive name server do all the work. It just needs the IP address of a local friendly recursive name server, and it relies on that server to handle interrogating the world's name servers. The process of querying DNS for a piece of information often involves making multiple queries to different name servers. To speed up this process, both name servers and resolvers can implement a domain name cache, which stores results of queries locally for limited time frames. In fact, quite a bit of the information stored in DNS is instructions on how caches should manage information. ZonesWhen you take responsibility for a zone, you're expected to set up two or more authoritative name servers. These servers are the ultimate authority for your zone, and DNS servers and resolvers ask your servers when they need resource records from your zone. When a name server or resolver receives a resource record originating from an authoritative name server, it usually caches the resource record for a predetermined length of time. Over time, your zone information gets distributed and cached across the global DNS infrastructure. You control the details of how your zone's information should be cached and refreshed. Zones are created by delegating subdomains. For every zone, there's a single domain that's the closest to the root node, which is the top node of the zone. Figure 16-16 shows an example of a namespace with zone partitions overlaid in gray. (Again, this simplified view omits some necessary details.) Look at the zone with a top node of neohapsis.com. At some point, the administrator of the com. zone delegated control of the neohapsis.com. subdomain to the neohapsis administrator. This means requests for any subdomain of neohapsis.com. are under the authoritative purview of the neohapsis.com. zone. You can see that the neohapsis administrator delegated lab.neohapsis.com. to another zone, which might be managed by the lab administrator. Figure 16-16. Example DNS tree with zonesResource Record ConventionsThere are several different types of resource records, distinguished by their type codes. The most important types, and the general format of their associated RDATA elements, are listed in Table 16-3.
The top node of any zone is a special node containing meta-information about that zone. It has two key sets of information: the SOA resource record for the zone and authoritative NS resource records for the zone. The SOA record contains information about caching parameters used by all the zone's resource records. The NS records authoritatively state the name servers in charge of the zone. The A resource records are used liberally to assign IP addresses to domain names and can appear in any domain in the zone. CNAME records are used for aliases. If the domain name sol.lab.neohapsis.com is an alias to jm.lab.neohapsis.com, there's a CNAME resource record owned by sol.lab.neohapsis.com. That resource record contains sol's canonical (ultimate) name, which is jm.lab.neohapsis.com. An authoritative name server typically knows all the information necessary to delegate requests to children zones. It conveys this information to other systems, even though it isn't technically authoritative for that information. For example, the name server responsible for the neohapsis.com. zone has NS records for lab.neohapsis.com. They should be identical to the authoritative NS records that the lab.neohapsis.com name server has for its top domain. The NS record points to a domain name, such as sol.lab.neohapsis.com., and the neohapsis.com. zone's server needs to provide a glue resource record that tells a client the IP address for the NS record. So the neohapsis.com. zone's server sends these additional resource records: lab.neohapsis.com. NS sol.lab.neohapsis.com. sol.lab.neohapsis.com. A 7.6.5.23 Basic Use CaseMost operating systems have a simple stub resolver that relies on an external recursive name server. The resolver library translates user requests into a DNS query packet that's sent to the preconfigured local recursive name server. This friendly name server attempts to answer the question by referring to its authoritative data and cache and by querying other name servers for information. This process usually takes a series of requests. Figure 16-17 shows how a typical DNS request is handled. Figure 16-17. DNS request trafficThe resolver creates an A query for the domain name www.google.com. and sends the query to its local recursive name server. First, the name server looks at its zones for anything in the domain name that it can answer for authoritatively, but it can't help with this query. Then it looks in its cache for any useful information; for the sake of discussion, assume it comes up empty. The name server is preloaded with a list of root name servers, and it starts sending iterative queries to them. It asks several root name servers for the A record for www.google.com and eventually gets a response. The response doesn't have the answer, however. Instead, it has multiple authority NS resource records that give the domain names for all com. name servers. The response also contains additional A resource records that give the numeric IP addresses for each specified name server. The name server asks a com. name server for the A record for www.google.com. The response still doesn't have an answer, but this time, the authority section has four NS records for google.com. The additional section has four corresponding A records for the numeric IP addresses of these name servers. Next, the name server asks a google.com. name server for the A record for www.google.com. In the real world, you learn that www.google.com. is an alias because you get an authoritative answer telling you that it's a CNAME for www.l.google.com. However, for this use case, pretend it returns an A record instead. The name server finally gets its A record for www.google.com., and the IP address is 1.2.3.4. The name server then constructs an answer for the resolver code and sends it as a response to the initial recursive query. The resolver code extracts the IP address from the A record and hands it to the user application. DNS Protocol Structure PrimerDNS is a binary protocol, so you know that integer issues are going to be involved. A DNS packet is essentially composed of a header followed by four variable-length fields: a questions section, an answer section, an authority section, and an additional section. This basic packet layout is shown in Figure 16-18. Figure 16-18. DNS packet structure
The header provides information about how the packet should be interpreted. Figure 16-19 shows how it's structured. Figure 16-19. DNS header structure
The DNS header contains a number of status bit fields and a series of record counts, indicating the number of resource records in the packet. These fields are described in the following list:
The questions section contains a series of question records, and the other sections contain resource records (RRs). The format of a question is shown in Figure 16-20. Figure 16-20. DNS question structure
The fields for a question entry in a query are as follows:
The format of a resource record structure is shown in Figure 16-21. The following list describes the fields for an RR:
Figure 16-21. DNS resource record data structure
DNS NamesNames are communicated in many places in DNS packets. These domain names aren't transmitted in a pure text format. Instead, they are transmitted as a series of labels. Each label contains a single-byte length value followed by the data bytes that make up this part of the name. Going back to the previous example of www.google.com, the name would look like Figure 16-22 in the packet. Figure 16-22. DNS names
Each label length byte is followed by the data bytes that make up each domain label. The name ends at the root of the tree, which has an empty label with a length byte of zero. A simple compression scheme using pointers can be used in domain names. If the top two bits are set in a label length byte, the remaining bits of the byte are combined with the next 8 bits from the packet (the next byte). They are used as an offset inside the DNS packet the pointer appears in, beginning at the start of the DNS header. This offset points to domain name information for the rest of the domain name. Using this simple scheme, multiple resource records using the same owner name (or sharing a common suffix) can write the shared name in the packet just one time. They can then refer to this shared name for all other subsequent resource records that refer to the same name. Although this naming scheme is simple and can save valuable space in some places, it certainly complicates the DNS name-decoding scheme. Take a look at a simple (buggy) implementation of name parsing, and the following sections discuss potential problems with it. int parse_dns_name(char *msg, char *name, int namelen, char *dest0, int destlen) { int label_length, offset, bytes_read = 0; char *ptr, *dest = dest0; for(ptr = name; *ptr; ){ label_length = *ptr++; /* check for pointers */ if((label_length & 0xC0) == 0xC0){ offset = ((label_length & 0x3F) << 8) | *ptr; ptr = msg + offset; continue; } if(bytes_read + label_length > destlen) return 1; memcpy(dest, ptr, label_length); ptr += label_length; dest += label_length; bytes_read += label_length; *dest++ = '.'; } if(dest != dest0) dest--; *dest = '\0'; return 0; } This simple implementation of the specification has numerous problems, explained in the following sections, that demonstrate what can go wrong when parsing DNS names. Failure to Deal with Invalid Label LengthsThe maximum size for a label is 63 bytes because setting the top 2 bits indicates that the byte is the first in a two-byte pointer, leaving 6 bits to represent a label length. That means any label in which one of the top bits is set but the other one isn't is an invalid length value. The preceding code doesn't adequately deal with this situation, resulting in larger domain labels than the specification allows. In this implementation, this problem carries additional consequences. Consider this line: label_length = *ptr++; Because ptr is signed, you know from Chapter 6 that this assignment sign-extends the value, so label_length can have a negative value. Later a size check is carried out: if(bytes_read + label_length > destlen) return 1; Can you see why this check isn't adequate? In this check, label_length is a negative value, so bytes_read + label_length can be made a negative value. Hence, this length check doesn't catch the problem, and subsequently a large negative memcpy() occurs. Insufficient Destination Length ChecksIt's easy to overlook the space required for bytes that are appended manually when performing length checks. In the sample code, a period (.) is appended manually after each label. These periods simply aren't checked for in the length check; only label_length bytes are accounted for. In addition, the trailing NUL byte isn't accounted for in much the same way. Insufficient Source Length ChecksJust as pointers aren't correctly verified to be in the packet, the code has no verification that source bytes being read are within the packet boundaries. If no NUL byte exists in the name section, this code keeps processing data until it runs past the end of the packetagain resulting in a potential information leak or denial of service. Even when the code does check that source bytes are within bounds, it omits this check when reading the second byte of a pointer or the amount of bytes specified in the label length. Pointer Values Not Verified In PacketWhen pointers are found, the ptr variable is set to point to the new location to continue reading the domain name. In this sample code, the new pointer is simply set to msg (the beginning of the DNS message) plus the supplied offset. The code never verifies that this new location is actually inside the packet, so it begins reading random memory from the program. This error might result in an information leak or a denial of serviceat any rate, it's not desirable behavior! Special Pointer ValuesWhen pointer compression methods are used, you can find a few more oddities. For example, a malicious user might create a loop. Say a pointer is 20 bytes into a DNS message and points to offset 20. If the sample code shown previously processes this pointer, it gets stuck in an infinite loop. This loop would probably end up causing a denial of service by not dealing with other DNS requests (especially if several resolutions were taking place in parallel with corrupt DNS pointers, such as this example). Also, be aware that the code has no real verification that pointers are actually pointing to name data in a DNS message. They might be pointing to a TTL field, a length field, or a pointer byte (such as having a pointer at offset 20 that points to offset 21 in the packet). Generally, this oversight doesn't cause too many security problems, but it might serve as part of an evasion technique to bypass IDSs. Length VariablesThere are no 32-bit integers to specify data lengths in the DNS protocol; everything is 8 or 16 bits. Therefore, this section focuses on the issues with 16-bit length fields discussed at the beginning of the chapter. The first issue is sign extensions of 16-bit values. You probably won't see this problem often, although when you do, it's likely a bug is present. Here's a simple example: struct rrecord { char *name; int ttl; short length, type, class; char *data; } #define ROUNDUP(x) ((x + 7) & 0xFFFFFFF8) void *mymalloc(size_t length) { return malloc(ROUNDUP(length)); } int parse_rrecord(char *data, int length, struct rrecord *rr) { if(length < 2 + 2 + 2 + 4) return 1; rr->name = parse_name(data, &data); if(!rr->name) return 1; rr->type = get_short(data); data += 2; rr->class = get_short(data); data += 2; rr->ttl = get_long(data); data += 4; rr->length = get_short(data); data += 2; length -= (4 + 2 + 2 + 2); if(rr->length > length) return 1; rr->data = (char *)mymalloc(rr->length); if(!rr->data) return 1; memcpy(rr->data, data, rr->length); ... } This code shows a typical malloc() implementation that's potentially vulnerable to an integer overflow. Because you're dealing with a protocol containing 16-bit length fields, allocation functions such as malloc() normally aren't dangerous because you can supply only 16-bit lengths, which aren't big enough to cause an integer wrap on a 32-bit integer size parameter. However, in this code, the 16-bit length value is sign-extended, so if the top bit is set, the high 16 bits of the value passed to mymalloc() are also set, allowing users to specify a size big enough to cause an integer wrap. Note This code wouldn't be vulnerable if the length parameter to parse_rrecord() was unsigned because the comparison of rr->length against length would cause rr->length to be sign-extended and then converted to unsigned, which is no doubt larger than length. In addition to sign-extension issues, there are other complications when the program decides to make extensive use of 16-bit variables for sizes or holding length values. Specifically, if 16-bit values are used carelessly, the risk of integer overflows is present (in the same way programs dealing with protocols that have 32-bit lengths are vulnerable to integer overflows). In the context of DNS, any addition or multiplication on a 16-bit variable presents a potential danger if users can specify large 16-bit values. To understand this problem, take a look at a bug that was in Microsoft's DNS-parsing code. To understand the bug, you must first examine the allocation routine used to allocate records. The following code shows the Dns_AllocateRecord() function: .text:76F239EC ; __stdcall Dns_AllocateRecord(x) .text:76F239EC _Dns_AllocateRecord@4 proc near .text:76F239EC .text:76F239EC .text:76F239EC arg_4 = word ptr 8 .text:76F239EC .text:76F239EC mov edi, edi .text:76F239EE push ebp .text:76F239EF mov ebp, esp .text:76F239F1 push esi .text:76F239F2 mov si, [ebp+arg_4] .text:76F239F6 movzx eax, si .text:76F239F9 add eax, 18h .text:76F239FC push eax .text:76F239FD call _Dns_AllocZero@4 ; Dns_AllocZero(x) .text:76F23A02 mov edx, eax .text:76F23A04 test edx, edx .text:76F23A06 jz loc_76F2DCB5 .text:76F23A0C push edi .text:76F23A0D push 6 .text:76F23A0F pop ecx .text:76F23A10 xor eax, eax .text:76F23A12 mov edi, edx .text:76F23A14 rep stosd .text:76F23A16 mov [edx+0Ah], si .text:76F23A1A mov eax, edx .text:76F23A1C pop edi .text:76F23A1D .text:76F23A1D loc_76F23A1D: ; CODE XREF: .text:76F2DCBF .text:76F23A1D pop esi .text:76F23A1E pop ebp .text:76F23A1F retn 4 .text:76F23A1F_Dns_AllocateRecord@4 endp This assembly code roughly translates to the following C code: /* sizeof DnsRecord structure is 24 (0x18) bytes */ struct DnsRecord { unsigned short size; /* offset 0x0A */ unsigned char data[0]; /* offset 0x18 */ } struct DnsRecord *Dns_AllocateRecord(unsigned short size) { struct DnsRecord *record; record = (struct DnsRecord *)Dns_AllocZero(size + sizeof(struct DnsRecord)); if(record == NULL){ SetLastError(8); return NULL; } memset((void *)record, 0, sizeof(struct DnsRecord)); record->size = size; return record; } You might be wondering why a SetLastError() function is in the C code but not in the assembly. The assembly output shows that the code tests the return value of Dns_AllocZero() and then jumps if it returns zero (which happens at location 76F23A06). The code it jumps to isn't shown, but it calls SetLastError(). Interested readers can refer to this function in dnsapi.dll on Windows XP or dnsrslvr.dll on Windows 2000. As you can see, this allocation routine could be dangerous. It takes a 16-bit size parameter, so if this function can ever be called with an allocation size of more than 65,535 bytes (the maximum representable 16-bit value), the high 16-bits are ignored, and a small data block not large enough to hold all the data will be allocated. It turns out that DNS packets are limited elsewhere in the code to a maximum of 16,384 bytes for TCP and 1,472 bytes for UDP, so you can't specify a big enough record to trigger an overflow under normal circumstances. However, take a closer look at how text records are processed. The following code is translated into C from the TxTRecordRead() function, which is used to parse records containing text fields. These records are composed of multiple text fields, each one consisting of a single-byte length field followed by text data. struct DnsRecord *TxtRecordRead(int to_unicode, unsigned char *src, unsigned char *end) { unsigned short length; int count, bytes_needed; struct DnsRecord *record; for(count = 0, bytes_needed = 0; src < end; count++){ length = *src++; bytes_needed += ((to_unicode) ? 2*length + 2 : length + 1); src += length; } if(src != end){ SetLastError(0x0D); return NULL; } record = Dns_AllocateRecord( ((count + 1) * sizeof(char *)) + bytes_needed); ... copy data and pointers ... } For every text field in the record, four bytes are allocated (for a pointer value to point to the text field), and two bytes are allocated for every byte appearing in the text data. The reason is that the data is converted in the text field from UTF-8 encoding to Unicode wide characters. Also, the code adds two bytes for the trailing NUL to appear after the text string it copies. When you have a zero-length record, it consists of a single byte: the length field, which has the value 0. For every zero-length record encountered, six bytes are added to the allocation size passed to Dns_AllocateRecord(): four bytes for the pointer, and two bytes for a NUL value. Six bytes for every one byte appearing in the record allows reaching the 16-bit boundary of 65,535 bytes with a record of around 10,922 bytes, which can be supplied in a TCP packet. Therefore, a buffer overflow can be triggered. DNS SpoofingDNS is a protocol for retrieving information from a large-scale distributed database, and it's used by clients of the service and servers that maintain the entire database. Because the system requires a large degree of trust, what can happen if attackers abuse this trust to feed bad information to those who request DNS information? The implications of this attack can be quite severe, depending on how clients use the false information. In the past, hostnames were commonly used for verification of a user's identity. For example, the UNIX rlogin service consulted a file with combinations of usernames and hostnames to authenticate incoming connections, instead of the username/password authentication most other services used at the time. Therefore, if attackers could forge DNS responses to make their IP addresses appear to be one of the hosts in this file, they could bypass authentication and log in to the target machine. These days, DNS names are rarely used to authenticate parties in such a direct manner; however, being able to forge DNS responses is still a serious issue. The most serious current risk is impersonation of a legitimate site. Malicious nodes can pose as legitimate destinations and collect authentication details or other sensitive data. For example, attackers could pose as a retailer that clients usually visit (such as www.amazon.com/). By posing as the legitimate site and fooling certain clients, the malicious users might be able to collect Amazon login credentials and credit card information from clients browsing the site. These attackers would have to pull a few tricks to make the spoofed site seem authentic, but they can usually fool most users. Cache PoisoningThe original resolver algorithm specified in DNS RFCs was vulnerable to a poisoning attack that enabled attackers to provide malicious IP addresses for arbitrary domain names. Assume that attackers have control of the zone at badperson.com. A victim asks the attackers' name server for the A records of www.badperson.com. They can respond by delegating authority for the www subdomain to the hostname they want to poison. For example, they could include an authority section in the response with these NS resource records: www.badperson.com. NS ns1.google.com. www.badperson.com. NS ns2.google.com. www.badperson.com. NS ns3.google.com. www.badperson.com. NS ns4.google.com. Basically, the attackers are telling the victim that the subdomain www.badperson.com is handled by four authoritative name servers, which happen to be Google's name servers. The death blow comes in the additional section in the response, where attackers place the A resource records for the Google name servers: ns1.google.com A 10.20.30.40 ns2.google.com A 10.20.30.40 ns3.google.com A 10.20.30.40 ns4.google.com A 10.20.30.40 RFC 1034 says the resolution code should check that the delegation is to a "better" name server then the one used in the current query. In this example, the query for www.badperson.com. was made to the badperson.com. name server. This request is delegated to the Google name servers, but the packet is saying is the name servers are authoritative for the www.badperson.com. subdomain. This is good enough to pass the algorithm's "better" check. The real problem is the algorithm suggests that code blindly honor the supplemental A records that purport to be helpful glue. Vulnerable implementations of BIND circa 1997 would enter these A records into the cache. Any future requests by victims for a google.com. host would end up contacting the attackers' evil name server at 10.20.30.40. Windows Resolver BugWindows resolvers also have a bug that allows attackers to hijack popular Web sites for specific targets. Say attackers have control of the zone at badperson.com. A victim asks their name server for the A records of www.badperson.com. This time, attackers can respond by delegating the authority for the com. domain to an evil name server under their control. The authority section might contain this NS resource record: com. NS evil.reallybad.org. There's no reason the victim's resolver should honor this response, as it's completely illogical. However, Windows cached this NS record because of an implementation bug. This means that later, when the resolver needs to contact a name server for the .com zone, it contacts evil.reallybad.org instead. Windows NT and Windows 2000 SP1 and SP2 were vulnerable by default to this problem, and it also affected various Symantec products. Spoofing ResponsesMost communications between DNS clients and servers occur over UDP, an unreliable and unauthenticated transport. ("Unauthenticated" means there's no way to verify that sender are who they say they are.) TCP is also an unauthenticated transport but to a much lesser extent. (For more information, refer to Chapter 14.) Therefore, how does a client or server know a request is from a legitimate source? The answer is simple: They don't, in a lot of circumstances! The traditional way of validating DNS responses is using the DNS ID field in the header. When a DNS client generates a question, it assigns an (ostensibly) random number for the ID field. When it receives responses, it checks that the DNS ID field matches the request. This check is done by verifying that the response packet has the same value in the DNS ID field as the query packet the client originally sent. With this information, a couple of attacks could be launched. One of the most obvious is a man-in-the-middle attack by someone in a position to observe DNS traffic. This attack is fairly easy to achieve, so chalk it up as a known risk and focus your attention on blind spoofing. DNS spoofing issues affect both DNS server and client implementations because servers make requests on behalf of clients and usually cache results (if they are configured for recursion). When a server issues a DNS request to recursively resolve a remote host on behalf of a client, remote responses to servers could be forged and subsequently cached. Basically, most attacks of this nature revolve around how predictable an implementation's DNS ID generation algorithm is. The simplest implementations have fixed increments (usually of 1) for each question they generate. In the past, BIND (one of the premier name servers on the Internet) was vulnerable to this problem, as pointed out by Secure Networks Inc. and CORE (documented at http://attrition.org/security/advisory/nai/SNI-12.BIND.advisory). The advisory walks through the steps required to cache poison name servers by forging responses from a remote DNS server. Note In some ways, this attack is not unlike the TCP sequence number spoofing mentioned in Chapter 14, except DNS IDs need to be exact. Injecting TCP data just requires a sequence number within the TCP window. Dan Bernstein gives a great summary of the current risks of blind forgery at http://cr.yp.to/djbdns/forgery.html:
The lack of authentication in this protocol is a recognized problem, and steps have been taken to help secure it. Specifically, DNS messages can be cryptographically verified by using the TKEY and TSIG record types, but this method isn't yet used extensively (even though most implementations support it). For this reason, you can't assume that cryptographic verification protects an implementation from DNS ID prediction vulnerabilities unless the implementation you're reviewing mandates the use of the DNS cryptographic features. DNS ID generation algorithms based on known values also might not be very secure. For example, a DNS ID based on the time returned from the time() functions might be quite easy to guess. |