#!/usr/bin/python # $Id: pmtud4dns.py,v 1.2 2013/05/31 11:28:06 willem Exp $ # Copyright (c) 2013, NLnet Labs. All rights reserved. # # This software is open source. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # Neither the name of the NLNET LABS nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. from sys import argv, exit from socket import socket, inet_ntop \ , AF_PACKET, SOCK_RAW, AF_INET6, IPPROTO_RAW import dpkt from time import time ## --------------------------------------------------------------------------- ## Process arguments progname = argv[0].split('/')[-1] def print_usage(): print("""Usage: %s [-h | --help] [-v | --verbose] WARNING! This is a Proof-Of-Concept program with the single purpose of experimentation only. It is not intented of operational practice! %s assumes a nameserver is running on the host and listens on for certain ICMPv6 messages that indicate that an DNS answer originating from the nameserver has not arrived: - ICMPv6 Packet-Too-Big containing a DNS answer. i.e. The packet did not fit the path to the requestor. - ICMPv6 Administratively Prohibited containing a fragmented DNS answer. i.e. The requestor is behind a firewall that does not allow fragments. %s then reconstructs the original query from the payload of those ICMPv6 messages, making sure (by setting EDNS0 udp size) that the answer will not be fragmented and will not be larger than the ICMPv6 messages. The answer will then fit through the path to the requestor, without risk of amplification. Note that besides the udp size, also the DNSSEC OK (DO) bit is set using EDNS0. The underlying assumption is that a big DNS answers are likely to be DNSSEC. -h | --help print help -v | --verbose print a line for each reinjected query """ % (progname, progname, progname)) verbose = False while '-v' in argv or '--verbose' in argv: argv = [a for a in argv if a not in ('-v', '--verbose')] verbose = True if '-h' in argv or '--help' in argv: print_usage() exit(0) if len(argv) != 2: print_usage() exit(1) ## --------------------------------------------------------------------------- ## Initialize interface = argv[1] EtherType_IPv6 = 0x86DD Protocol_TCP = 6 Protocol_UDP = 17 Protocol_IPv6_Frag = 44 Protocol_ICMPv6 = 58 ICMPv6Type_Unreach = 1 ICMPv6Type_PTB = 2 ICMPv6Code_Prohibit = 1 DNSFlags_RD = 1 s = socket(AF_PACKET, SOCK_RAW, EtherType_IPv6) # Open raw IPv6 socket s.bind((interface, EtherType_IPv6)) # on interface to sniff l = socket(AF_INET6 , SOCK_RAW, IPPROTO_RAW) # And one to inject # locally while True: ## -------------------------------------------------------------------- ## Listen on interface for ICMPv6 Packet-Too-Big and Administratively- ## Prohibited messages with a DNS answer in the payload. ## frame = s.recv(1500 + 14) # max packet size + # ethernet header size eth = dpkt.ethernet.Ethernet(frame) # We asked for IPv6! assert eth.type == EtherType_IPv6, "Non IPv6!" # Expect IPv6! ipv6 = eth.data if ipv6.nxt != Protocol_ICMPv6: # Next when not ICMPv6 continue icmpv6 = ipv6.data max_dns_msg_size = len(eth.data) # Never give a larger # answer than the ICMP # packet size. # (i.e. do not amplify) if icmpv6.type == ICMPv6Type_PTB: # Packet Too Big max_dns_msg_size = min( max_dns_msg_size , icmpv6.data.mtu ) elif icmpv6.type != ICMPv6Type_Unreach \ or icmpv6.code != ICMPv6Code_Prohibit \ or icmpv6.data.data.nxt != Protocol_IPv6_Frag:# admin prohibited only continue # for fragments... max_dns_msg_size -= 48 # Turn packet size in # DNS message size by # substracting sizes of # IPv6 and UDP headers payload = icmpv6.data.data # payload = the # original non-fitting # response packet if payload.nxt == Protocol_IPv6_Frag: # In case of fragments if ord(payload.data[0]) != Protocol_UDP:# Next when fragment continue # does not contain UDP if ( ord(payload.data[2]) << 8 | ord(payload.data[3]) & 0xf8) != 0: # Next when not the continue # first fragment udp = dpkt.udp.UDP(payload.data[8:]) elif payload.nxt == Protocol_UDP: udp = payload.data else: # Next when no fragment continue # or UDP payload. if udp.sport != 53: # Next when not a DNS continue # answer ## -------------------------------------------------------------------- ## Recreate query udp.data = ( udp.data[:2] # Clear all but RD + chr(ord(udp.data[2]) & DNSFlags_RD)# (recursion desired) + '\x00' # Clear RA and RCODE + udp.data[4:6] # Question count (1) + '\x00\x00' # Answer count (0) + '\x00\x00' # Authority count (0) + '\x00\x00' # Additional count (0) + udp.data[12:] ) try: udp.data = str(dpkt.dns.DNS(udp.data)) # Parse DNS to clean # so there is only the # question... except IndexError: # Sometimes we don't print( "%f Parse error, DNS packet: %s" # even have enough % ( time() # remaining data to , str(udp.data).encode('hex') # reconstruct only )) # the question... continue udp.data = ( udp.data[:10] # Set additional + '\x00\x01' # section counter to 1 + udp.data[12:] # and append EDNS0. + '\x00' + '\x00\x29' # . OPT + chr(max_dns_msg_size >> 8) # Message size in + chr(max_dns_msg_size & 0xff) # network byte order + '\x00' # extended RCODE + '\x00' # version (always 0) + '\x80\x00' # set DO flag (likely) + '\x00\x00' # rdlen + '' # rdata ) ## -------------------------------------------------------------------- ## Reinject udp.ulen = len(udp.data) + 8 # Including header size udp.sport, udp.dport = udp.dport, udp.sport # Back to the server udp.sum = 0 # Reset to recalculate payload.nxt = Protocol_UDP # Reuse original ipv6 payload.data = udp # header for msg back payload.plen = len(payload.data) # to the server. payload.src, payload.dst = payload.dst, payload.src l.sendto( str(payload) # Reinject locally , 0 # It works on Linux, , ( inet_ntop(AF_INET6, payload.dst) # Not tested on other , 0, 0, 0 ) # systems. ) ## -------------------------------------------------------------------- ## Logging if not verbose: continue dns = dpkt.dns.DNS(udp.data) print( "%f Reinjected %s. TYPE%d from %s to %s with EDNS0 udp size %d" % ( time() , dns.qd[0].name, dns.qd[0].type , inet_ntop(AF_INET6, payload.src) , inet_ntop(AF_INET6, payload.dst) , max_dns_msg_size ))