1#!/usr/bin/env python 2""" 3 4Requirements: 5* https://github.com/openalias/dnscrypt-python 6* https://github.com/warner/python-pure25519/blob/master/misc/djbec.py 7* a query file of the form `qname\tqtype`. Example query file https://nominum.com/measurement-tools/ 8 9 10Example usage: 11python dnscrypt-fuzzer.py \ 12 --provider-key XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX \ 13 --port 8443 \ 14 -q queryfile-example-current 15""" 16import argparse 17import codecs 18import os 19import pdb 20import random 21import socket 22import time 23 24import dnscrypt 25 26qtypemap = { 27 'A': 1, 28 'NS': 2, 29 'MD': 3, 30 'MF': 4, 31 'CNAME': 5, 32 'SOA': 6, 33 'MB': 7, 34 'MG': 8, 35 'MR': 9, 36 'NULL': 10, 37 'WKS': 11, 38 'PTR': 12, 39 'HINFO': 13, 40 'MINFO': 14, 41 'MX': 15, 42 'TXT': 16, 43 'RP': 17, 44 'AFSDB': 18, 45 'X25': 19, 46 'ISDN': 20, 47 'RT': 21, 48 'NSAP': 22, 49 'NSAP-PTR': 23, 50 'SIG': 24, 51 'KEY': 25, 52 'PX': 26, 53 'GPOS': 27, 54 'AAAA': 28, 55 'LOC': 29, 56 'NXT': 30, 57 'EID': 31, 58 'NIMLOC': 32, 59 'SRV': 33, 60 'ATMA': 34, 61 'NAPTR': 35, 62 'KX': 36, 63 'CERT': 37, 64 'A6': 38, 65 'DNAME': 39, 66 'SINK': 40, 67 'OPT': 41, 68 'APL': 42, 69 'DS': 43, 70 'SSHFP': 44, 71 'IPSECKEY': 45, 72 'RRSIG': 46, 73 'NSEC': 47, 74 'DNSKEY': 48, 75 'DHCID': 49, 76 'NSEC3': 50, 77 'NSEC3PARAM': 51, 78 'TLSA': 52, 79 'SMIMEA': 53, 80 'Unassigned': 54, 81 'HIP': 55, 82 'NINFO': 56, 83 'RKEY': 57, 84 'TALINK': 58, 85 'CDS': 59, 86 'CDNSKEY': 60, 87 'OPENPGPKEY': 61, 88 'CSYNC': 62, 89 'SPF': 99, 90 'UINFO': 100, 91 'UID': 101, 92 'GID': 102, 93 'UNSPEC': 103, 94 'NID': 104, 95 'L32': 105, 96 'L64': 106, 97 'LP': 107, 98 'EUI48': 108, 99 'EUI64': 109, 100 'TKEY': 249, 101 'TSIG': 250, 102 'IXFR': 251, 103 'AXFR': 252, 104 'MAILB': 253, 105 'MAILA': 254, 106 '*': 255, 107 'URI': 256, 108 'CAA': 257, 109 'AVC': 258, 110 'TA': 32768, 111 'DLV': 32769, 112} 113 114def flipbit(msg, **kwargs): 115 idx = random.randint(0, len(msg)-1) 116 bit_idx = random.randint(0,7) 117 x = msg[:idx] 118 x += chr(ord(msg[idx]) ^ 1<<bit_idx) 119 x += msg[idx+1:] 120 return x 121 122def flipmanybits(msg, **kwargs): 123 124 x = '' 125 for c in msg: 126 if random.randint(0, 50) == 25: 127 bit_idx = random.randint(0,7) 128 x += chr(ord(c) ^ 1<<bit_idx) 129 else: 130 x += c 131 return x 132 133def dropbyte(msg, **kwargs): 134 idx = random.randint(0, len(msg)-1) 135 return msg[:idx] + msg[idx+1:] 136 137def injectbyte(msg, **kwargs): 138 idx = random.randint(0, len(msg)-1) 139 x = msg[:idx] 140 x += chr(random.randint(0, 255)) 141 x += msg[idx:] 142 return x 143 144def truncatepacket(msg, **kwargs): 145 idx = random.randint(kwargs['minint'], len(msg)-1) 146 return msg[idx:] 147 148def noop(msg, **kwargs): 149 return msg 150 151def mkmsg(magic_query, pk, nonce, encoded_message): 152 return magic_query + pk + nonce + encoded_message 153 154def corrupt(magic_query, pk, nonce, nmkey, message): 155 c = random.randint(0, 100) % 7 156 args = {} 157 if c <= 1: 158 f = flipmanybits 159 elif c == 2: 160 f = flipbit 161 elif c == 3: 162 f = dropbyte 163 elif c == 4: 164 f = injectbyte 165 elif c == 5: 166 # 68 is min dnscrypt header size 167 args = {'minint': 68} 168 f = truncatepacket 169 elif c == 6: 170 c2 = random.randint(0, 100) % 4 171 f = flipmanybits 172 if c2 == 0: 173 f = flipbit 174 elif c2 == 1: 175 f = dropbyte 176 elif c2 == 2: 177 f = injectbyte 178 elif c2 == 3: 179 # 12 is min dns header size 180 args = {'minint': 12} 181 f = truncatepacket 182 message = f(message, **args) 183 args = {} 184 f = noop 185 186 encoded_message = dnscrypt.encode_message(message, nonce, nmkey) 187 return f(mkmsg(magic_query, pk, nonce, encoded_message), **args) 188 189class DnsCrypt(): 190 def __init__(self, ip, port, provider_name, provider_key): 191 self.ip = ip 192 self.port = port 193 self.provider_name = provider_name 194 self.provider_key = provider_key.replace(':', '') 195 self.provider_pk, self.magic_query = dnscrypt.get_public_key( 196 self.ip, self.port, self.provider_key, self.provider_name) 197 198 self.pk, self.sk = dnscrypt.generate_keypair() 199 self.nmkey = dnscrypt.create_nmkey(self.provider_pk[:32], self.sk) 200 201 def query(self, qname, qtype, corrupted=False,return_packet=True): 202 # create dns query 203 header = dnscrypt.DnsHeader() 204 205 question = dnscrypt.DnsQuestion() 206 question.labels = qname.split('.') 207 question.qtype = qtype 208 209 packet = dnscrypt.DnsPacket(header) 210 packet.addQuestion(question) 211 212 message = packet.toBinary() + '\x00\x00\x29\x04\xe4' + 6 * '\x00' + '\x80' 213 214 # custom rules for type 48 215 if qtype == 48: 216 url_part = '' 217 for part in question.labels: 218 url_part += chr(len(part)) + part 219 message = '\x124\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01' + url_part + '\x00\x000\x00\x01\x00\x00)\x05\x00\x00\x00\x80\x00\x00\x00\x80' 220 221 nonce = "%x" % int(time.time()) + os.urandom(4).encode('hex')[4:] 222 if corrupted: 223 payload = corrupt(self.magic_query, self.pk, nonce, self.nmkey, message) 224 else: 225 payload = self.magic_query + self.pk + nonce + dnscrypt.encode_message(message, nonce, self.nmkey) 226 227 #poly = poly1305.onetimeauth_poly1305(encoded_message, provider_pk[:32]) not quite sure if that's needed for something... 228 229 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 230 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 231 dest = (self.ip, self.port) 232 233 sock.sendto(payload, dest) 234 235def parse_args(): 236 parser = argparse.ArgumentParser(description='Fuzzer for DNSCrypt') 237 parser.add_argument( 238 '--provider-name', '-p', 239 default='2.dnscrypt-cert.example.com', 240 help='Provider name, default: %(default)s', 241 ) 242 parser.add_argument( 243 '--provider-key', '-k', 244 help='Provider key', 245 required=True, 246 ) 247 parser.add_argument( 248 '--host', '-H', 249 default='127.0.0.1', 250 help='DNS host to fuzz, default: %(default)s', 251 ) 252 parser.add_argument( 253 '--port', '-P', 254 default=443, type=int, 255 help='Port the dnscrypt service is running on, default: %(default)s', 256 ) 257 parser.add_argument( 258 '--queryfile', '-q', 259 required=True, 260 help='Path to the file containing query samples. Format: qname\tqtype. Example file available at https://nominum.com/measurement-tools/' 261 ) 262 parser.add_argument( 263 '--count', '-c', 264 type=int, 265 default=1000000, 266 help='Number of queries to perform, default: %(default)s', 267 ) 268 parser.add_argument( 269 '--non-corrupted', '-C', 270 type=int, 271 default=1000, 272 help='How often to not corrupt a query. 1 query every --non-corrupted query will be sent coorruption free. 0 for always corrupt, default: %(default)s', 273 ) 274 return parser.parse_args() 275 276if __name__ == '__main__': 277 args = parse_args() 278 args.provider_key = args.provider_key.replace(':', '') 279 d = DnsCrypt(args.host, args.port, args.provider_name, args.provider_key) 280 281 queries = [] 282 with open(args.queryfile) as f: 283 for l in f: 284 qname, qtype = l.split() 285 queries.append((codecs.escape_decode(qname)[0], qtypemap[qtype],)) 286 287 for i in xrange(args.count): 288 corrupted = True 289 if args.non_corrupted == 0 or random.randint(0, args.non_corrupted) == 0: 290 corrupted = False 291 q = random.choice(queries) 292 try: 293 r = d.query(q[0], q[1], corrupted=corrupted) 294 except Exception: 295 pdb.set_trace() 296 297