/* Parses DNS response information The scanner sends a CHAOS TXT query for "version.bind". This module parses DNS in order to find the response string. */ #include "proto-udp.h" #include "proto-dns.h" #include "proto-dns-parse.h" #include "proto-preprocess.h" #include "syn-cookie.h" #include "util-logger.h" #include "output.h" #include "masscan-app.h" #include "proto-banner1.h" #include "massip-port.h" #include "masscan.h" #include "unusedparm.h" #define VERIFY_REMAINING(n) if (offset+(n) > length) return; /**************************************************************************** * This skips over a name field while parsing the packet. If the name * is just a two-byte compression field like 0xc0 0x1a, then it'll skip * those two bytes. However, when it does the skip, it does validate * the name. Thus, if it's a compressed name, it'll follow the compression * links to validate things like long names and infinite recursion. ****************************************************************************/ static unsigned dns_name_skip_validate(const unsigned char *px, unsigned offset, unsigned length, unsigned name_length) { unsigned ERROR = length + 1; unsigned result = offset + 2; unsigned recursion = 0; /* 'for all labels' */ for (;;) { unsigned len; /* validate: the eventual uncompressed name will be less than 255 */ if (name_length >= 255) return ERROR; /* validate: haven't gone off end of packet */ if (offset >= length) return ERROR; /* grab length of next label */ len = px[offset]; /* Do two types of processing, either a compression code or a * original label. Note that we can alternate back and forth * between these two states. */ if (len & 0xC0) { /* validate: top 2 bits are 11*/ if ((len & 0xC0) != 0xC0) return ERROR; /* validate: enough bytes left for 2 byte compression field */ if (offset + 1 >= length) return ERROR; /* follow the compression pointer to the next location */ offset = (px[offset]&0x3F)<<8 | px[offset+1]; /* validate: follow a max of 4 links */ if (++recursion > 4) return ERROR; } else { /* we have a normal label */ recursion = 0; /* If the label-length is zero, then that means we've reached * the end of the name */ if (len == 0) { return result; /* end of domain name */ } /* There are more labels to come, therefore skip this and go * to the next one */ name_length += len + 1; offset += len + 1; } } } /**************************************************************************** * Just skip the name, without validating whether it's valid or not. This * is for re-parsing the packet usually, after we've validated that all * the names are OK. ****************************************************************************/ unsigned dns_name_skip(const unsigned char px[], unsigned offset, unsigned max) { unsigned name_length = 0; /* Loop through all labels * NOTE: the only way this loops around is in the case of a normal * label. All other conditions cause a 'return' from this function */ for (;;) { if (name_length >= 255) return max + 1; if (offset >= max) return max + 1; switch (px[offset]>>6) { case 0: /* uncompressed label */ if (px[offset] == 0) { return offset+1; /* end of domain name */ } else { name_length += px[offset] + 1; offset += px[offset] + 1; continue; } break; case 3: /* 0xc0 = compressed name */ return dns_name_skip_validate(px, offset, max, name_length); case 2: /* 0x40 - ENDS0 extended label type * rfc2671 section 3.1 * I have no idea how to parse this */ return max + 1; /* totally clueless how to parse it */ case 1: return max + 1; } } } /**************************************************************************** ****************************************************************************/ static void dns_extract_name(const unsigned char px[], unsigned offset, unsigned max, struct DomainPointer *name) { name->length = 0; for (;;) { unsigned len; if (offset >= max) return; len = px[offset]; if (len & 0xC0) { if ((len & 0xC0) != 0xC0) return; else if (offset + 1 >= max) return; else { offset = (px[offset]&0x3F)<<8 | px[offset+1]; } } else { if (len == 0) { return; /* end of domain name */ } else { memcpy((unsigned char*)name->name+name->length, px+offset, len+1); name->length = (unsigned char)(name->length + len + 1); offset += len + 1; } } } } /**************************************************************************** ****************************************************************************/ void proto_dns_parse(struct DNS_Incoming *dns, const unsigned char px[], unsigned offset, unsigned max) { static const unsigned MAX_RRs = sizeof(dns->rr_offset)/sizeof(dns->rr_offset[0]); unsigned i; dns->is_valid = 0; /* not valid yet until we've successfully parsed*/ dns->req = px; dns->req_length = max-offset; dns->edns0.payload_size = 512; /* RFC 1035 4.2.1 */ /* 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ID | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QDCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ANCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | NSCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | ARCOUNT | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ if (max - offset < 12) return; dns->id = px[offset+0]<<8 | px[offset+1]; dns->qr = (px[offset+2]>>7)&1; dns->aa = (px[offset+2]>>2)&1; dns->tc = (px[offset+2]>>1)&1; dns->rd = (px[offset+2]>>0)&1; dns->ra = (px[offset+3]>>7)&1; dns->z = (px[offset+3]>>4)&7; dns->opcode = (px[offset+2]>>3)&0xf; dns->rcode = (px[offset+3]>>0)&0xf; dns->qdcount = px[offset+4]<<8 | px[offset+5]; dns->ancount = px[offset+6]<<8 | px[offset+7]; dns->nscount = px[offset+8]<<8 | px[offset+9]; dns->arcount = px[offset+10]<<8 | px[offset+11]; dns->rr_count = 0; /* so far */ offset += 12; dns->is_valid = 1; dns->is_formerr = 1; /* is "formate-error" until we've finished parsing */ /* 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / QNAME / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QTYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | QCLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ for (i=0; iqdcount; i++) { unsigned xclass; unsigned xtype; if (dns->rr_count >= MAX_RRs) return; dns->rr_offset[dns->rr_count++] = (unsigned short)offset; offset = dns_name_skip(px, offset, max); offset += 4; /* length of type and class */ if (offset > max) return; xclass = px[offset-2]<<8 | px[offset-1]; if (xclass != 1 && xclass != 255 && xclass != 3) return; xtype = px[offset-4]<<8 | px[offset-3]; dns->query_type = xtype; } /* 1 1 1 1 1 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | | / / / NAME / | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TYPE | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | CLASS | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | TTL | | | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ | RDLENGTH | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| / RDATA / / / +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ for (i=0; iancount + dns->nscount; i++) { unsigned rdlength; if (dns->rr_count >= sizeof(dns->rr_offset)/sizeof(dns->rr_offset[0])) return; dns->rr_offset[dns->rr_count++] = (unsigned short)offset; offset = dns_name_skip(px, offset, max); offset += 10; if (offset > max) return; rdlength = px[offset-2]<<8 | px[offset-1]; offset += rdlength; if (offset > max) return; } for (i=0; iarcount; i++) { unsigned rdlength; if (dns->rr_count >= sizeof(dns->rr_offset)/sizeof(dns->rr_offset[0])) return; dns->rr_offset[dns->rr_count++] = (unsigned short)offset; /* ENDS0 OPT parsing */ if (offset + 11 <= max && px[offset] == 0 && px[offset+1] == 0 && px[offset+2] == 0x29) { dns->edns0.payload_size = px[offset+3]<<8 | px[offset+4]; if (dns->edns0.payload_size < 512) return; dns->rcode |= px[offset+5]<<4; dns->edns0.version = px[offset+6]; dns->is_edns0 = 1; } offset = dns_name_skip(px, offset, max); offset += 10; if (offset > max) return; rdlength = px[offset-2]<<8 | px[offset-1]; offset += rdlength; if (offset > max) return; } dns->query_name.name = dns->query_name_buffer; dns_extract_name(px, dns->rr_offset[0], max, &dns->query_name); dns->is_formerr = 0; return; } /*************************************************************************** * Set the "syn-cookie" style information so that we can validate replies * match a valid request. We don't hold "state" on the requests, so this * becomes a hash of the port/IP information. * DNS has a two-byte "transaction id" field, so we can't use the full * cookie, just the lower two bytes of it. * Below in "handle_dns", we validate that the cookie is correct. ***************************************************************************/ unsigned dns_set_cookie(unsigned char *px, size_t length, uint64_t cookie) { if (length > 2) { px[0] = (unsigned char)(cookie >> 8); px[1] = (unsigned char)(cookie >> 0); return cookie & 0xFFFF; } else return 0; } /*************************************************************************** * Process a DNS packet received in response to UDP probes to port 53. * This function has three main tasks: * - parse the DNS protocol, and make sure it's valid DNS. * - make sure that the DNS response matches a valid request using * the "syn-cookie" approach. * - parse the "version.bind" response and report it as the version * string for the banner. ***************************************************************************/ unsigned handle_dns(struct Output *out, time_t timestamp, const unsigned char *px, unsigned length, struct PreprocessedInfo *parsed, uint64_t entropy) { ipaddress ip_them = parsed->src_ip; ipaddress ip_me = parsed->dst_ip; unsigned port_them = parsed->port_src; unsigned port_me = parsed->port_dst; struct DNS_Incoming dns[1]; unsigned offset; uint64_t seqno; const char *reason = 0; seqno = (unsigned)syn_cookie(ip_them, port_them | Templ_UDP, ip_me, port_me, entropy); proto_dns_parse(dns, px, parsed->app_offset, parsed->app_offset + parsed->app_length); if ((seqno & 0xFFFF) != dns->id) return 1; /* * In practice, DNS queries always have the query count set to 1, * though in theory servers could support multiple queries in a * single request, almost none of them do */ if (dns->qr != 1) return 0; /* * If we get back NOERROR, we drop through and extract the strings in * the packet. Otherwise, we report the error here. */ switch (dns->rcode) { case 0: reason = 0; break; /* NOERROR */ case 1: reason = "1:FORMERR"; break; case 2: reason = "2:SERVFAIL"; break; case 3: reason = "3:NXDOMAIN"; break; case 4: reason = "4:NOTIMP"; break; case 5: reason = "5:REFUSED"; break; case 6: reason = "6:YXDOMAIN"; break; case 7: reason = "7:XRRSET"; break; case 8: reason = "8:NOTAUTH"; break; case 9: reason = "9:NOTZONE"; break; } if (reason != 0) { output_report_banner( out, timestamp, ip_them, 17, port_them, PROTO_DNS_VERSIONBIND, parsed->ip_ttl, (const unsigned char*)reason, (unsigned)strlen(reason)); return 0; } /*if (dns->qdcount != 1) return 0; if (dns->ancount < 1) return 0; if (dns->rr_count < 2) return 0;*/ offset = dns->rr_offset[1]; offset = dns_name_skip(px, offset, length); if (offset + 10 >= length) return 0; { unsigned type = px[offset+0]<<8 | px[offset+1]; unsigned xclass = px[offset+2]<<8 | px[offset+3]; unsigned rrlen = px[offset+8]<<8 | px[offset+9]; unsigned txtlen = px[offset+10]; offset += 11; /* Make sure can't exceed bounds of RR */ if (txtlen > length - offset) txtlen = length - offset; if (rrlen == 0 || txtlen > rrlen-1) return 0; if (type != 0x10 || xclass != 3) return 0; output_report_banner( out, timestamp, ip_them, 17, port_them, PROTO_DNS_VERSIONBIND, parsed->ip_ttl, px + offset, txtlen); return 1; } }