robur-coop/udns

Query with a huge question section can lead to crash

Closed this issue · 2 comments

After trying to fuzz the primary server from the example folder with big inputs, I found some packets that could crash main.native. In this example, the culprit is a query of size 3451 that consists of many questions (https://pastebin.com/tnV0JUbR for a hexadecimal and byte representation of that packet) :

2018-07-05 16:21:20 +01:00: INF [tcpip-stack-socket] Manager: connect
2018-07-05 16:21:20 +01:00: INF [tcpip-stack-socket] Manager: configuring
2018-07-05 16:21:20 +01:00: WRN [application] no secondaries keys found (err not found  TTL 300 soa SOA foo._key-management foo._key-management 0 16384 2048 1048576 300)
2018-07-05 16:21:20 +01:00: INF [application] loaded zone: mirage.	2560	SOA	ns.mirage.hostmaster.mirage.	1	10	5	60	2560
mirage.	2560	NS	ns.mirage.
charrua.mirage.	2560	A	10.0.42.3
ns.mirage.	2560	A	10.0.42.2
resolver.mirage.	2560	A	10.0.42.5
router.mirage.	2560	A	10.0.42.1
secondary.mirage.	2560	A	10.0.42.4
www.mirage.	2560	CNAME	router.mirage.

2018-07-05 16:21:20 +01:00: INF [dns_mirage_server] DNS server listening on UDP port 53
2018-07-05 16:21:20 +01:00: INF [dns_mirage_server] DNS server listening on TCP port 53
2018-07-05 16:21:26 +01:00: INF [dns_mirage_server] udp frame from 127.0.0.1:33525
2018-07-05 16:21:26 +01:00: ERR [dns_server] 181 questions foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, bailing
Fatal error: exception (Invalid_argument
  "invalid bounds in Cstruct.BE.set_uint16 [0,450](450) off=449 len=2")
Raised at file "format.ml" (inlined), line 242, characters 35-52
Called from file "format.ml", line 469, characters 8-33
Called from file "format.ml", line 484, characters 6-24

TL;DR : The packet is faulty according to the uDNS primary server handle function because the question section contains more than one question. The server tries to reply with an answer containing the same question section (I think it must be done for security purposes), but it creates a buffer with a shorter length, that's why that error is raised and crashes the application.

Now for a more detailed explanation (took me a really long time to figure out !). If I understood correctly :

  • The server allows any frame which size is under 4096 (otherwise it says ERR [dns_server] partial frame (length 4096)), so the packet goes through
  • UDns_server.Primary.handle is called at one point
  • In that call, UDns_server.Primary.handle_inner is called, which calls itself UDns_server.handle_frame
  • As the frame is a query-type packet, it calls UDns_server.handle_query
  • UDns_server.handle_query checks the question section and sees that there is more than one question, therefore it returns the rcode FormErr to UDns_server.handle_inner
  • UDns_server.handle_inner, seeing that it got an rcode error, calls UDns_server.err, which calls Dns_packet.error to create a reply with the same content but with the rcode set to FormErr
  • Dns_packet.error creates a buffer of size Dns_packet.max_reply_udp (= 450 !!) and tries to copy the initial packet into that buffer through Dns_packet.encode_query
  • Dns_packet.encode_query calls List.fold_left using Dns_packet.encode_question as the folding function, which shifts the offset value in the reply buffer
  • As 3451 is greater than 450, the final error invalid bounds is raised.

Maybe a solution would be to rise max_udp_size to 4096 ?

thanks for the report :)

I added a regression test (which decodes the frame and afterwards expects error to produce something sensible (and esp. not crash).

the fix I applied is if 450 byte are not sufficient to encode out only the first question. max_udp_size is my sanity guard to not act as an amplifier (which maybe should be tweaked to be a ratio between incoming and outgoing packet) -- given that I don't support any frames with multiple questions, I don't think I need to send the entire question back (please let me know if there's a requirement in some RFC to always send back the entire question section).

Sending the entire question back isn't a compulsory requirement but RFC5452, a Proposed Standard RFC, advises that the question section in the answer should match the question section from the query to help against spoofed packets. There may be some resolvers implementing this RFC, so that may be an issue, although I don't really know if there are many packets asking for multiples questions in a single query.

I will continue fuzzing in that direction, there may be other cases in which it could happen. The parsing function looks really solid though, I haven't found any bugs here 🤔