Repository: secdev/scapy

Issue:
DNS answering machines throw `AttributeError` when they receive malformed queries
### Brief description

Malformed DNS queries like
```python
>>> DNS(b'q\xa04\x00\x00\xa0\x01\x00\xf3\x00\x01\x04\x01y').qd
INFO: DNS RR prematured end (ofs=2, len=2)
[<Raw  load=b'\x01y' |>]
```
cause DNS/LLMNR answering machines to throw `AttributeError`.

### Scapy version

a8d2bb7d1ad68b3442533fecb0510b3c02316950

### Python version

Python 3.11.4

### Operating system

Linux version 6.4.7-100.fc37.x86_64

### Additional environment information

_No response_

### How to reproduce

The issue can be reproduced by launching DNS/LLMNR answering machines and sending the malformed queries but it can be reduced to the following unit test:
```python
DNS_am().make_reply(Ether()/IP()/UDP()/DNS(b'q\xa04\x00\x00\xa0\x01\x00\xf3\x00\x01\x04\x01y'))
```

### Actual result

```python
Traceback (most recent call last):                                                                                                                                                
    am()                                                                                                                                                                          
  File "scapy/ansmachine.py", line 211, in __call__                                                                                                         
    self.sniff()                                                                                                                                                                  
  File "scapy/ansmachine.py", line 217, in sniff                                                                                                            
    sniff(**self.optsniff)
  File "scapy/sendrecv.py", line 1311, in sniff 
    sniffer._run(*args, **kwargs)
  File "scapy/sendrecv.py", line 1254, in _run
    session.on_packet_received(p)                                                        
  File "scapy/sessions.py", line 109, in on_packet_received
    result = self.prn(pkt)
             ^^^^^^^^^^^^^                                                               
  File "scapy/ansmachine.py", line 173, in reply
    reply = self.make_reply(pkt)
            ^^^^^^^^^^^^^^^^^^^^
  File "scapy/layers/dns.py", line 1268, in make_reply
    if rq.qtype in [1, 28]:
       ^^^^^^^^
  File "scapy/packet.py", line 469, in __getattr__
    return self.payload.__getattr__(attr)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "scapy/packet.py", line 467, in __getattr__
    fld, v = self.getfield_and_val(attr)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "scapy/packet.py", line 1793, in getfield_and_val
    raise AttributeError(attr)
AttributeError: qtype
```

### Expected result

I think queries like that should probably be ignored.

### Related resources

_No response_


Discussion:

Resolving patch:
diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py
--- a/scapy/layers/dns.py
+++ b/scapy/layers/dns.py
@@ -1520,13 +1520,37 @@ def make_reply(self, req):
         if IPv6 in req:
             resp[IPv6].underlayer.remove_payload()
             resp /= IPv6(dst=req[IPv6].src, src=self.src_ip6 or req[IPv6].dst)
-        else:
+        elif IP in req:
             resp[IP].underlayer.remove_payload()
             resp /= IP(dst=req[IP].src, src=self.src_ip or req[IP].dst)
-        resp /= UDP(sport=req.dport, dport=req.sport)
+        else:
+            warning("No IP or IPv6 layer in %s", req.command())
+            return
+        try:
+            resp /= UDP(sport=req[UDP].dport, dport=req[UDP].sport)
+        except IndexError:
+            warning("No UDP layer in %s", req.command(), exc_info=True)
+            return
         ans = []
-        req = req.getlayer(self.cls)
-        for rq in req.qd:
+        try:
+            req = req[self.cls]
+        except IndexError:
+            warning(
+                "No %s layer in %s",
+                self.cls.__name__,
+                req.command(),
+                exc_info=True,
+            )
+            return
+        try:
+            queries = req.qd
+        except AttributeError:
+            warning("No qd attribute in %s", req.command(), exc_info=True)
+            return
+        for rq in queries:
+            if isinstance(rq, Raw):
+                warning("Cannot parse qd element %s", rq.command(), exc_info=True)
+                continue
             if rq.qtype in [1, 28]:
                 # A or AAAA
                 if rq.qtype == 28:
@@ -1598,6 +1622,9 @@ def make_reply(self, req):
             # Error
             break
         else:
+            if not ans:
+                # No rq was actually answered, as none was valid. Discard.
+                return
             # All rq were answered
             resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans)
             return resp


Test patch:
diff --git a/test/answering_machines.uts b/test/answering_machines.uts
--- a/test/answering_machines.uts
+++ b/test/answering_machines.uts
@@ -119,6 +119,13 @@ test_am(DNS_am,
         match={"google.com": ("127.0.0.1", "::1"), "gaagle.com": "128.0.0.1"},
         joker=False)
 
+assert DNS_am().make_reply(Ether()) is None
+assert DNS_am().make_reply(Ether()/IP()) is None
+assert DNS_am().make_reply(Ether()/IP()/UDP()) is None
+assert DNS_am().make_reply(
+    Ether()/IP()/UDP()/DNS(b'q\xa04\x00\x00\xa0\x01\x00\xf3\x00\x01\x04\x01y')
+) is None
+
 = DHCPv6_am - Basic Instantiaion
 ~ osx netaccess
 a = DHCPv6_am()