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