Didier Stevens

Monday 27 October 2025

Bytes over DNS Tools

Filed under: Hacking,Networking — Didier Stevens @ 9:07

Here are the tools I used to conduct my “Bytes over DNS” tests.

On the server side, I start my dnsresolver.py program with the following custom script:

LOGFILENAME = 'bod-dnsresolver-test.log'

def BoDTest(request, reply, dCommand):
    if request.q.qtype == dnslib.QTYPE.A:
        if len(request.q.qname.label[2]) == 1 and int(request.q.qname.label[1].decode(), 16) == ord(request.q.qname.label[2]):
            with open(LOGFILENAME, 'a') as fOut:
                print(f'BYTE_EQUAL {request.q.qname.label[1]} {request.q.qname.label[2]}', file=fOut)
            qname = request.q.qname
            answer = '. 60 IN A 127.0.0.1'
            for rr in dnslib.RR.fromZone(answer):
                a = copy.copy(rr)
                a.rname = qname
                reply.add_answer(a)
            return False, None
        else:
            with open(LOGFILENAME, 'a') as fOut:
                print(f'BYTE_DIFFERENT {request.q.qname.label[1]} {request.q.qname.label[2]}', file=fOut)
    return True, None

Start it as follows: dnsresolver.py -s bod-dnsresolver-test.py type=resolve,label=bytes,function=BoDTest

And make sure your DNS glue records (e.g., for mydomain.com) point to your server.

Then you can do a small test: nslookup bytes.3D.=.mydomain.com.

This will return 127.0.0.1 when the request arrives unaltered, and NXDOMAIN when it is altered. The BoDTest function will also log the results in text file bod-dnsresolver-test.log.

Then, on your workstation, you can run the following script to test all bytes values in the DNS request via the API of your OS:

#!/usr/bin/env python3

import socket
import sys

DOMAIN = '.mydomain.com.'

def DNSResolveA(char: int):
    hostname_ascii = 'bytes.%02x.%s' % (char, chr(char)) + DOMAIN
    hostname_ascii = hostname_ascii.replace('\\', '\\\\')
    print(hostname_ascii)
    try:
        results = socket.getaddrinfo(hostname_ascii, None, family=socket.AF_INET, type=0, proto=0, flags=socket.AI_CANONNAME)
    except socket.gaierror as e:
        print(f"Resolution failed: {e}")
        return 1
    except UnicodeError as e:
        print(f"Resolution failed: {e}")
        return 1

    if not results:
        print("No results returned by getaddrinfo.")
        return 0

    # Collect canonical name (may be empty) and addresses
    canon_names = set()
    addresses = []
    for res in results:
        family, socktype, proto, canonname, sockaddr = res
        if canonname:
            canon_names.add(canonname)
        # sockaddr is a tuple; for IPv4 it's (addr, port), for IPv6 it's (addr, port, flowinfo, scopeid)
        ip = sockaddr[0]
        addresses.append((family, ip))

    if canon_names:
        print("Canonical name(s):")
        for cn in sorted(canon_names):
            print("  -", cn)
        print()

    # Deduplicate and group by family
    unique_ips = {}
    for fam, ip in addresses:
        fam_name = "IPv4" if fam == socket.AF_INET else ("IPv6" if fam == socket.AF_INET6 else str(fam))
        unique_ips.setdefault(fam_name, set()).add(ip)

    for fam_name in sorted(unique_ips.keys()):
        print(f"{fam_name} addresses ({len(unique_ips[fam_name])}):")
        for ip in sorted(unique_ips[fam_name]):
            print("  -", ip)
    print()

    # Optionally, try reverse DNS for each IP (may be slow / not always available)
    print("Reverse DNS (PTR) lookups:")
    for fam_name, ips in unique_ips.items():
        for ip in sorted(ips):
            try:
                host, aliases, _ = socket.gethostbyaddr(ip)
                print(f"  {ip} -> {host}")
            except Exception as e:
                print(f"  {ip} -> (no PTR)  [{e}]")

    return 0

if __name__ == "__main__":
    for char in range(256):
        DNSResolveA(char)

Use this script to perform the tests via the dnspython/dns.resolver Python module:

import dns.resolver

resolver = dns.resolver.Resolver()
DOMAIN = b'.mydomain.com.'

#resolver.nameservers = ['127.0.0.1']
#resolver.nameservers = ['1.1.1.1']
resolver.nameservers = ['8.8.8.8']

for i in range(256):
    if i == 0x2E:
        continue
    if i == 0x5C:
        byte = b'\\\\'
    else:
        byte = bytes([i])
    try:
        answer = resolver.resolve(((b'bytes.%02x.%s' + DOMAIN) % (i, byte)).decode('latin'), "A")
        for rdata in answer:
            print(i, rdata.to_text())
    except (dns.name.LabelTooLong, dns.resolver.NXDOMAIN) as e:
        print(i, e)

And use this script to perform the tests by crafting your own DNS packets:

import socket

DOMAIN = b'mydomain.com.'
DNS = '1.1.1.1'
DNS = '8.8.8.8'

def send_udp_payload(data: bytes, target_ip: str, port: int = 53) -> None:
    """
    Send raw binary data via UDP to a target IP and port (default 53).
    
    :param data: The binary payload to send (must be bytes).
    :param target_ip: The destination IP address (string).
    :param port: Destination UDP port (default = 53).
    """
    # Create UDP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        sock.sendto(data, (target_ip, port))
        print(f"Sent {len(data)} bytes to {target_ip}:{port}")
    except Exception as e:
        print(f"Error sending data: {e}")
    finally:
        sock.close()

def DNSEncodeDomain(domain):
    labels = domain.split(b'.')
    if labels[-1] != b'':
        labels.append(b'')
    data = bytearray()
    for label in labels:
        data += bytes([len(label)])
        data += label
    return data

data = bytearray([0x88, 0xea, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, 0x79, 0x74, 0x65, 0x73, 0x02, 0x32, 0x65, 0x01, 0x2e]) + DNSEncodeDomain(DOMAIN) + bytearray([0x00, 0x01, 0x00, 0x01])

for i in range(256):
    data[1] = i
    data[22] = i
    hexvalue = b'%02x' % i
    data[19:21] = hexvalue
    print(data)
    send_udp_payload(data, DNS)

Update: dnsresolver.py Version 0.0.4

Filed under: My Software,Update — Didier Stevens @ 0:00

This update brings function= to the definition of a resolve command.

Key-value pair function is optional. If provided, the value will be interpreted as a Python function and called when there is a match. The function must inspect the request and update the reply. Arguments to the function are request, reply and dCommand. The function must return a list with 2 values: first one is True when NXDOMAIN must be returned (and False if there is an answer), second one is an integer with the rcode value, it must be None if there is no rcode set by the function.

An extra Python script (for example with function definitions) can be loaded using option -s.

dnsresolver_V0_0_4.zip (http)
MD5: 7520FB4510E6ED5E5975A8606852F548
SHA256: 1C4BC4D6030A2534600283962890545D428F2BBAFD85D0B3E934B03B07EA1F6A

Saturday 25 October 2025

Quickpost: PEP 515 – Underscores in Numeric Literals

Filed under: Quickpost — Didier Stevens @ 8:07

While attending a great presentation of Kaitai Struct at Hack.lu 2025, I noticed a binary numeric notation during the demo, that I had never seen before. Something like 0b1000_0001.

I’m familiar with notations in Python like 0b10000001, but not with an underscore thrown in to make the number more readable.

Turns out this exists for almost 10 years in Python (since Python 3.6), and that it is known as PEP 515 – Underscores in Numeric Literals.

And it exists in other programming languages too.


Quickpost info

Thursday 2 October 2025

Overview of Content Published in September

Filed under: Announcement — Didier Stevens @ 0:00
Here is an overview of content I published in September:

SANS ISC Diary entries:

Blog at WordPress.com.