Skip to content

Commit ba51704

Browse files
committed
Improve answering machines, dns_resolve, renames
1 parent 363d376 commit ba51704

File tree

9 files changed

+154
-81
lines changed

9 files changed

+154
-81
lines changed

doc/scapy/routing.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Scapy maintains its own network stack, which is independent from the one of your
66
It possesses its own *interfaces list*, *routing table*, *ARP cache*, *IPv6 neighbour* cache, *nameservers* config... and so on, all of which is configurable.
77

88
Here are a few examples of where this is used::
9+
910
- When you use ``sr()/send()``, Scapy will use internally its own routing table (``conf.route``) in order to find which interface to use, and eventually send an ARP request.
1011
- When using ``dns_resolve()``, Scapy uses its own nameservers list (``conf.nameservers``) to perform the request
1112
- etc.

doc/scapy/usage.rst

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,28 +1434,32 @@ Visualizing the results in a list::
14341434
>>> res.nsummary(prn=lambda s,r: r.src, lfilter=lambda s,r: r.haslayer(ISAKMP) )
14351435

14361436

1437-
DNS spoof
1438-
---------
1437+
DNS server
1438+
----------
14391439

1440-
See :class:`~scapy.layers.dns.DNS_am`::
1440+
By default, ``dnsd`` uses a joker (IPv4 only): it answers to all unknown servers with the joker. See :class:`~scapy.layers.dns.DNS_am`::
14411441

1442-
>>> dns_spoof(iface="tap0", joker="192.168.1.1")
1442+
>>> dnsd(iface="tap0", match={"google.com": "1.1.1.1"}, joker="192.168.1.1")
14431443

1444-
LLMNR spoof
1445-
-----------
1444+
You can also use ``relay=True`` to replace the joker behavior with a forward to a server included in ``conf.nameservers``.
1445+
1446+
LLMNR server
1447+
------------
14461448

14471449
See :class:`~scapy.layers.llmnr.LLMNR_am`::
14481450

14491451
>>> conf.iface = "tap0"
1450-
>>> llmnr_spoof(iface="tap0", from_ip=Net("10.0.0.1/24"))
1452+
>>> llmnrd(iface="tap0", from_ip=Net("10.0.0.1/24"))
14511453

1452-
Netbios spoof
1453-
-------------
1454+
Note that ``llmnrd`` extends the ``dnsd`` API.
1455+
1456+
Netbios server
1457+
--------------
14541458

14551459
See :class:`~scapy.layers.netbios.NBNS_am`::
14561460

1457-
>>> nbns_spoof(iface="eth0") # With local IP
1458-
>>> nbns_spoof(iface="eth0", ip="192.168.122.17") # With some other IP
1461+
>>> nbnsd(iface="eth0") # With local IP
1462+
>>> nbnsd(iface="eth0", ip="192.168.122.17") # With some other IP
14591463

14601464
Node status request (get NetbiosName from IP)
14611465
---------------------------------------------

scapy/ansmachine.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import abc
1515
import functools
16+
import threading
1617
import socket
1718
import warnings
1819

@@ -225,12 +226,14 @@ def sniff_bg(self):
225226
class AnsweringMachineTCP(AnsweringMachine[Packet]):
226227
"""
227228
An answering machine that use the classic socket.socket to
228-
answer multiple clients
229+
answer multiple TCP clients
229230
"""
231+
TYPE = socket.SOCK_STREAM
232+
230233
def parse_options(self, port=80, cls=conf.raw_layer):
231234
# type: (int, Type[Packet]) -> None
232235
self.port = port
233-
self.cls = conf.raw_layer
236+
self.cls = cls
234237

235238
def close(self):
236239
# type: () -> None
@@ -239,7 +242,7 @@ def close(self):
239242
def sniff(self):
240243
# type: () -> None
241244
from scapy.supersocket import StreamSocket
242-
ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
245+
ssock = socket.socket(socket.AF_INET, self.TYPE)
243246
ssock.bind(
244247
(get_if_addr(self.optsniff.get("iface", conf.iface)), self.port))
245248
ssock.listen()
@@ -267,6 +270,19 @@ def sniff(self):
267270
self.close()
268271
ssock.close()
269272

273+
def sniff_bg(self):
274+
# type: () -> None
275+
self.sniffer = threading.Thread(target=self.sniff) # type: ignore
276+
self.sniffer.start()
277+
270278
def make_reply(self, req, address=None):
271279
# type: (Packet, Optional[Any]) -> Packet
272280
return req
281+
282+
283+
class AnsweringMachineUDP(AnsweringMachineTCP):
284+
"""
285+
An answering machine that use the classic socket.socket to
286+
answer multiple UDP clients
287+
"""
288+
TYPE = socket.SOCK_DGRAM

scapy/layers/dhcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ def parse_options(self,
593593
network="192.168.1.0/24",
594594
gw="192.168.1.1",
595595
nameserver=None,
596-
domain="localnet",
596+
domain=None,
597597
renewal_time=60,
598598
lease_time=1800):
599599
"""

scapy/layers/dns.py

Lines changed: 90 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,13 +1072,13 @@ def mysummary(self):
10721072
name = ""
10731073
if self.qr:
10741074
type = "Ans"
1075-
if self.an and isinstance(self.an, DNSRR):
1076-
name = ' "%s"' % self.an[0].rdata
1075+
if self.an and isinstance(self.an[0], DNSRR):
1076+
name = ' %s' % self.an[0].rdata
10771077
else:
10781078
type = "Qry"
1079-
if self.qd and isinstance(self.qd, DNSQR):
1080-
name = ' "%s"' % self.qd[0].qname
1081-
return 'DNS %s%s ' % (type, name)
1079+
if self.qd and isinstance(self.qd[0], DNSQR):
1080+
name = ' %s' % self.qd[0].qname
1081+
return 'DNS %s%s' % (type, name)
10821082

10831083
def post_build(self, pkt, pay):
10841084
if isinstance(self.underlayer, TCP) and self.length is None:
@@ -1129,13 +1129,16 @@ def pre_dissect(self, s):
11291129

11301130

11311131
@conf.commands.register
1132-
def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs):
1132+
def dns_resolve(qname, qtype="A", raw=False, verbose=1, timeout=3, **kwargs):
11331133
"""
11341134
Perform a simple DNS resolution using conf.nameservers with caching
11351135
11361136
:param qname: the name to query
11371137
:param qtype: the type to query (default A)
11381138
:param raw: return the whole DNS packet (default False)
1139+
:param verbose: show verbose errors
1140+
:param timeout: seconds until timeout (per server)
1141+
:raise TimeoutError: if no DNS servers were reached in time.
11391142
"""
11401143
# Unify types
11411144
qtype = DNSQR.qtype.any2i_one(None, qtype)
@@ -1149,7 +1152,7 @@ def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs):
11491152
if answer:
11501153
return answer
11511154

1152-
kwargs.setdefault("timeout", 3)
1155+
kwargs.setdefault("timeout", timeout)
11531156
kwargs.setdefault("verbose", 0)
11541157
res = None
11551158
for nameserver in conf.nameservers:
@@ -1203,7 +1206,8 @@ def dns_resolve(qname, qtype="A", raw=False, verbose=1, **kwargs):
12031206
# Cache it
12041207
_dns_cache[cache_ident] = answer
12051208
return answer
1206-
return None
1209+
else:
1210+
raise TimeoutError
12071211

12081212

12091213
@conf.commands.register
@@ -1247,14 +1251,15 @@ def dyndns_del(nameserver, name, type="ALL", ttl=10):
12471251

12481252

12491253
class DNS_am(AnsweringMachine):
1250-
function_name = "dns_spoof"
1254+
function_name = "dnsd"
12511255
filter = "udp port 53"
1252-
cls = DNS # We use this automaton for llmnr_spoof
1256+
cls = DNS # We also use this automaton for llmnrd
12531257

12541258
def parse_options(self, joker=None,
12551259
match=None,
12561260
srvmatch=None,
12571261
joker6=False,
1262+
relay=False,
12581263
from_ip=None,
12591264
from_ip6=None,
12601265
src_ip=None,
@@ -1265,40 +1270,50 @@ def parse_options(self, joker=None,
12651270
Set to False to disable, None to mirror the interface's IP.
12661271
:param joker6: default IPv6 for unresolved domains (Default: False)
12671272
set to False to disable, None to mirror the interface's IPv6.
1273+
:param relay: relay unresolved domains to conf.nameservers (Default: False).
12681274
:param match: a dictionary of {name: val} where name is a string representing
12691275
a domain name (A, AAAA) and val is a tuple of 2 elements, each
1270-
representing an IP or a list of IPs
1276+
representing an IP or a list of IPs. If val is a single element,
1277+
(A, None) is assumed.
12711278
:param srvmatch: a dictionary of {name: (port, target)} used for SRV
12721279
:param from_ip: an source IP to filter. Can contain a netmask
12731280
:param from_ip6: an source IPv6 to filter. Can contain a netmask
12741281
:param ttl: the DNS time to live (in seconds)
1275-
:param src_ip:
1282+
:param src_ip: override the source IP
12761283
:param src_ip6:
12771284
12781285
Example:
12791286
1280-
>>> dns_spoof(joker="192.168.0.2", iface="eth0")
1281-
>>> dns_spoof(match={
1287+
$ sudo iptables -I OUTPUT -p icmp --icmp-type 3/3 -j DROP
1288+
>>> dnsd(match={"google.com": "1.1.1.1"}, joker="192.168.0.2", iface="eth0")
1289+
>>> dnsd(srvmatch={
12821290
... "_ldap._tcp.dc._msdcs.DOMAIN.LOCAL.": (389, "srv1.domain.local")
12831291
... })
12841292
"""
1293+
def normv(v):
1294+
if isinstance(v, (tuple, list)) and len(v) == 2:
1295+
return v
1296+
elif isinstance(v, str):
1297+
return (v, None)
1298+
else:
1299+
raise ValueError("Bad match value: '%s'" % repr(v))
1300+
1301+
def normk(k):
1302+
k = bytes_encode(k).lower()
1303+
if not k.endswith(b"."):
1304+
k += b"."
1305+
return k
12851306
if match is None:
12861307
self.match = {}
12871308
else:
1288-
assert all(isinstance(x, (tuple, list)) for x in match.values()), (
1289-
"'match' values must be a tuple of 2 elements: ('<ipv4>', '<ipv6>')"
1290-
". They can be None"
1291-
)
1292-
self.match = {bytes_encode(k): v for k, v in match.items()}
1309+
self.match = {normk(k): normv(v) for k, v in match.items()}
12931310
if srvmatch is None:
12941311
self.srvmatch = {}
12951312
else:
1296-
assert all(isinstance(x, (tuple, list)) for x in srvmatch.values()), (
1297-
"'srvmatch' values must be a tuple of 2 elements: (port, 'target')"
1298-
)
1299-
self.srvmatch = {bytes_encode(k): v for k, v in srvmatch.items()}
1313+
self.srvmatch = {normk(k): normv(v) for k, v in srvmatch.items()}
13001314
self.joker = joker
13011315
self.joker6 = joker6
1316+
self.relay = relay
13021317
if isinstance(from_ip, str):
13031318
self.from_ip = Net(from_ip)
13041319
else:
@@ -1341,51 +1356,65 @@ def make_reply(self, req):
13411356
if rq.qtype == 28:
13421357
# AAAA
13431358
try:
1344-
rdata = self.match[rq.qname][1]
1359+
rdata = self.match[rq.qname.lower()][1]
13451360
except KeyError:
1346-
if self.joker6 is False:
1347-
return
1348-
rdata = self.joker6 or get_if_addr6(
1349-
self.optsniff.get("iface", conf.iface)
1350-
)
1361+
if self.relay or self.joker6 is False:
1362+
rdata = None
1363+
else:
1364+
rdata = self.joker6 or get_if_addr6(
1365+
self.optsniff.get("iface", conf.iface)
1366+
)
13511367
elif rq.qtype == 1:
13521368
# A
13531369
try:
1354-
rdata = self.match[rq.qname][0]
1370+
rdata = self.match[rq.qname.lower()][0]
13551371
except KeyError:
1356-
if self.joker is False:
1357-
return
1358-
rdata = self.joker or get_if_addr(
1359-
self.optsniff.get("iface", conf.iface)
1360-
)
1361-
if rdata is None:
1362-
# Ignore None
1363-
return
1364-
# Common A and AAAA
1365-
if not isinstance(rdata, list):
1366-
rdata = [rdata]
1367-
ans.extend([
1368-
DNSRR(rrname=rq.qname, ttl=self.ttl, rdata=x, type=rq.qtype)
1369-
for x in rdata
1370-
])
1372+
if self.relay or self.joker is False:
1373+
rdata = None
1374+
else:
1375+
rdata = self.joker or get_if_addr(
1376+
self.optsniff.get("iface", conf.iface)
1377+
)
1378+
if rdata is not None:
1379+
# Common A and AAAA
1380+
if not isinstance(rdata, list):
1381+
rdata = [rdata]
1382+
ans.extend([
1383+
DNSRR(rrname=rq.qname, ttl=self.ttl, rdata=x, type=rq.qtype)
1384+
for x in rdata
1385+
])
1386+
continue # next
13711387
elif rq.qtype == 33:
13721388
# SRV
13731389
try:
1374-
port, target = self.srvmatch[rq.qname]
1390+
port, target = self.srvmatch[rq.qname.lower()]
1391+
ans.append(DNSRRSRV(
1392+
rrname=rq.qname,
1393+
port=port,
1394+
target=target,
1395+
weight=100,
1396+
ttl=self.ttl
1397+
))
1398+
continue # next
13751399
except KeyError:
1376-
return
1377-
ans.append(DNSRRSRV(
1378-
rrname=rq.qname,
1379-
port=port,
1380-
target=target,
1381-
weight=100,
1382-
ttl=self.ttl
1383-
))
1384-
else:
1385-
# Not handled
1386-
continue
1387-
# Common: All
1388-
if not ans:
1389-
return
1390-
resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans)
1400+
# No result
1401+
pass
1402+
# It it arrives here, there is currently no answer
1403+
if self.relay:
1404+
# Relay mode ?
1405+
try:
1406+
_rslv = dns_resolve(rq.qname, qtype=rq.qtype)
1407+
if _rslv is not None:
1408+
ans.append(_rslv)
1409+
continue # next
1410+
except TimeoutError:
1411+
pass
1412+
# Error
1413+
break
1414+
else:
1415+
# All rq were answered
1416+
resp /= self.cls(id=req.id, qr=1, qd=req.qd, an=ans)
1417+
return resp
1418+
# An error happened
1419+
resp /= self.cls(id=req.id, qr=1, qd=req.qd, rcode=3)
13911420
return resp

scapy/layers/llmnr.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414

1515
import struct
1616

17-
from scapy.fields import BitEnumField, BitField, ShortField
17+
from scapy.fields import (
18+
BitEnumField,
19+
BitField,
20+
DestField,
21+
DestIP6Field,
22+
ShortField,
23+
)
1824
from scapy.packet import Packet, bind_layers, bind_bottom_up
1925
from scapy.compat import orb
2026
from scapy.layers.inet import UDP
@@ -88,9 +94,14 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs):
8894
bind_bottom_up(UDP, _LLMNR, sport=5355)
8995
bind_layers(UDP, _LLMNR, sport=5355, dport=5355)
9096

97+
DestField.bind_addr(LLMNRQuery, _LLMNR_IPv4_mcast_addr, dport=5355)
98+
DestField.bind_addr(LLMNRResponse, _LLMNR_IPv4_mcast_addr, dport=5355)
99+
DestIP6Field.bind_addr(LLMNRQuery, _LLMNR_IPv6_mcast_Addr, dport=5355)
100+
DestIP6Field.bind_addr(LLMNRResponse, _LLMNR_IPv6_mcast_Addr, dport=5355)
101+
91102

92103
class LLMNR_am(DNS_am):
93-
function_name = "llmnr_spoof"
104+
function_name = "llmnrd"
94105
filter = "udp port 5355"
95106
cls = LLMNRQuery
96107

scapy/layers/netbios.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ def post_build(self, pkt, pay):
355355

356356

357357
class NBNS_am(AnsweringMachine):
358-
function_name = "nbns_spoof"
358+
function_name = "nbnsd"
359359
filter = "udp port 137"
360360
sniff_options = {"store": 0}
361361

0 commit comments

Comments
 (0)