86890704fd
todo: add documentation & wireshark dissector
1944 lines
70 KiB
Python
Executable file
1944 lines
70 KiB
Python
Executable file
# This file is part of Scapy
|
|
# See http://www.secdev.org/projects/scapy for more information
|
|
# Copyright (C) Philippe Biondi <phil@secdev.org>
|
|
# This program is published under a GPLv2 license
|
|
|
|
"""
|
|
IPv4 (Internet Protocol v4).
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import print_function
|
|
import time
|
|
import struct
|
|
import re
|
|
import random
|
|
import select
|
|
import socket
|
|
from collections import defaultdict
|
|
|
|
from scapy.utils import checksum, do_graph, incremental_label, \
|
|
linehexdump, strxor, whois, colgen
|
|
from scapy.base_classes import Gen, Net
|
|
from scapy.data import ETH_P_IP, ETH_P_ALL, DLT_RAW, DLT_RAW_ALT, DLT_IPV4, \
|
|
IP_PROTOS, TCP_SERVICES, UDP_SERVICES
|
|
from scapy.layers.l2 import Ether, Dot3, getmacbyip, CookedLinux, GRE, SNAP, \
|
|
Loopback
|
|
from scapy.compat import raw, chb, orb, bytes_encode
|
|
from scapy.config import conf
|
|
from scapy.extlib import plt, MATPLOTLIB, MATPLOTLIB_INLINED, \
|
|
MATPLOTLIB_DEFAULT_PLOT_KARGS
|
|
from scapy.fields import ConditionalField, IPField, BitField, BitEnumField, \
|
|
FieldLenField, StrLenField, ByteField, ShortField, ByteEnumField, \
|
|
DestField, FieldListField, FlagsField, IntField, MultiEnumField, \
|
|
PacketListField, ShortEnumField, SourceIPField, StrField, \
|
|
StrFixedLenField, XByteField, XShortField, Emph
|
|
from scapy.packet import Packet, bind_layers, bind_bottom_up, NoPayload
|
|
from scapy.volatile import RandShort, RandInt, RandBin, RandNum, VolatileValue
|
|
from scapy.sendrecv import sr, sr1
|
|
from scapy.plist import PacketList, SndRcvList
|
|
from scapy.automaton import Automaton, ATMT
|
|
from scapy.error import warning
|
|
from scapy.pton_ntop import inet_pton
|
|
|
|
import scapy.as_resolvers
|
|
|
|
import scapy.modules.six as six
|
|
from scapy.modules.six.moves import range
|
|
|
|
####################
|
|
# IP Tools class #
|
|
####################
|
|
|
|
|
|
class IPTools(object):
|
|
"""Add more powers to a class with an "src" attribute."""
|
|
__slots__ = []
|
|
|
|
def whois(self):
|
|
"""whois the source and print the output"""
|
|
print(whois(self.src).decode("utf8", "ignore"))
|
|
|
|
def _ttl(self):
|
|
"""Returns ttl or hlim, depending on the IP version"""
|
|
return self.hlim if isinstance(self, scapy.layers.inet6.IPv6) else self.ttl # noqa: E501
|
|
|
|
def ottl(self):
|
|
t = sorted([32, 64, 128, 255] + [self._ttl()])
|
|
return t[t.index(self._ttl()) + 1]
|
|
|
|
def hops(self):
|
|
return self.ottl() - self._ttl()
|
|
|
|
|
|
_ip_options_names = {0: "end_of_list",
|
|
1: "nop",
|
|
2: "security",
|
|
3: "loose_source_route",
|
|
4: "timestamp",
|
|
5: "extended_security",
|
|
6: "commercial_security",
|
|
7: "record_route",
|
|
8: "stream_id",
|
|
9: "strict_source_route",
|
|
10: "experimental_measurement",
|
|
11: "mtu_probe",
|
|
12: "mtu_reply",
|
|
13: "flow_control",
|
|
14: "access_control",
|
|
15: "encode",
|
|
16: "imi_traffic_descriptor",
|
|
17: "extended_IP",
|
|
18: "traceroute",
|
|
19: "address_extension",
|
|
20: "router_alert",
|
|
21: "selective_directed_broadcast_mode",
|
|
23: "dynamic_packet_state",
|
|
24: "upstream_multicast_packet",
|
|
25: "quick_start",
|
|
30: "rfc4727_experiment",
|
|
}
|
|
|
|
|
|
class _IPOption_HDR(Packet):
|
|
fields_desc = [BitField("copy_flag", 0, 1),
|
|
BitEnumField("optclass", 0, 2, {0: "control", 2: "debug"}),
|
|
BitEnumField("option", 0, 5, _ip_options_names)]
|
|
|
|
|
|
class IPOption(Packet):
|
|
name = "IP Option"
|
|
fields_desc = [_IPOption_HDR,
|
|
FieldLenField("length", None, fmt="B", # Only option 0 and 1 have no length and value # noqa: E501
|
|
length_of="value", adjust=lambda pkt, l:l + 2), # noqa: E501
|
|
StrLenField("value", "", length_from=lambda pkt:pkt.length - 2)] # noqa: E501
|
|
|
|
def extract_padding(self, p):
|
|
return b"", p
|
|
|
|
registered_ip_options = {}
|
|
|
|
@classmethod
|
|
def register_variant(cls):
|
|
cls.registered_ip_options[cls.option.default] = cls
|
|
|
|
@classmethod
|
|
def dispatch_hook(cls, pkt=None, *args, **kargs):
|
|
if pkt:
|
|
opt = orb(pkt[0]) & 0x1f
|
|
if opt in cls.registered_ip_options:
|
|
return cls.registered_ip_options[opt]
|
|
return cls
|
|
|
|
|
|
class IPOption_EOL(IPOption):
|
|
name = "IP Option End of Options List"
|
|
option = 0
|
|
fields_desc = [_IPOption_HDR]
|
|
|
|
|
|
class IPOption_NOP(IPOption):
|
|
name = "IP Option No Operation"
|
|
option = 1
|
|
fields_desc = [_IPOption_HDR]
|
|
|
|
|
|
class IPOption_Security(IPOption):
|
|
name = "IP Option Security"
|
|
copy_flag = 1
|
|
option = 2
|
|
fields_desc = [_IPOption_HDR,
|
|
ByteField("length", 11),
|
|
ShortField("security", 0),
|
|
ShortField("compartment", 0),
|
|
ShortField("handling_restrictions", 0),
|
|
StrFixedLenField("transmission_control_code", "xxx", 3),
|
|
]
|
|
|
|
|
|
class IPOption_RR(IPOption):
|
|
name = "IP Option Record Route"
|
|
option = 7
|
|
fields_desc = [_IPOption_HDR,
|
|
FieldLenField("length", None, fmt="B",
|
|
length_of="routers", adjust=lambda pkt, l:l + 3), # noqa: E501
|
|
ByteField("pointer", 4), # 4 is first IP
|
|
FieldListField("routers", [], IPField("", "0.0.0.0"),
|
|
length_from=lambda pkt:pkt.length - 3)
|
|
]
|
|
|
|
def get_current_router(self):
|
|
return self.routers[self.pointer // 4 - 1]
|
|
|
|
|
|
class IPOption_LSRR(IPOption_RR):
|
|
name = "IP Option Loose Source and Record Route"
|
|
copy_flag = 1
|
|
option = 3
|
|
|
|
|
|
class IPOption_SSRR(IPOption_RR):
|
|
name = "IP Option Strict Source and Record Route"
|
|
copy_flag = 1
|
|
option = 9
|
|
|
|
|
|
class IPOption_Stream_Id(IPOption):
|
|
name = "IP Option Stream ID"
|
|
copy_flag = 1
|
|
option = 8
|
|
fields_desc = [_IPOption_HDR,
|
|
ByteField("length", 4),
|
|
ShortField("security", 0), ]
|
|
|
|
|
|
class IPOption_MTU_Probe(IPOption):
|
|
name = "IP Option MTU Probe"
|
|
option = 11
|
|
fields_desc = [_IPOption_HDR,
|
|
ByteField("length", 4),
|
|
ShortField("mtu", 0), ]
|
|
|
|
|
|
class IPOption_MTU_Reply(IPOption_MTU_Probe):
|
|
name = "IP Option MTU Reply"
|
|
option = 12
|
|
|
|
|
|
class IPOption_Traceroute(IPOption):
|
|
name = "IP Option Traceroute"
|
|
option = 18
|
|
fields_desc = [_IPOption_HDR,
|
|
ByteField("length", 12),
|
|
ShortField("id", 0),
|
|
ShortField("outbound_hops", 0),
|
|
ShortField("return_hops", 0),
|
|
IPField("originator_ip", "0.0.0.0")]
|
|
|
|
|
|
class IPOption_Address_Extension(IPOption):
|
|
name = "IP Option Address Extension"
|
|
copy_flag = 1
|
|
option = 19
|
|
fields_desc = [_IPOption_HDR,
|
|
ByteField("length", 10),
|
|
IPField("src_ext", "0.0.0.0"),
|
|
IPField("dst_ext", "0.0.0.0")]
|
|
|
|
|
|
class IPOption_Router_Alert(IPOption):
|
|
name = "IP Option Router Alert"
|
|
copy_flag = 1
|
|
option = 20
|
|
fields_desc = [_IPOption_HDR,
|
|
ByteField("length", 4),
|
|
ShortEnumField("alert", 0, {0: "router_shall_examine_packet"}), ] # noqa: E501
|
|
|
|
|
|
class IPOption_SDBM(IPOption):
|
|
name = "IP Option Selective Directed Broadcast Mode"
|
|
copy_flag = 1
|
|
option = 21
|
|
fields_desc = [_IPOption_HDR,
|
|
FieldLenField("length", None, fmt="B",
|
|
length_of="addresses", adjust=lambda pkt, l:l + 2), # noqa: E501
|
|
FieldListField("addresses", [], IPField("", "0.0.0.0"),
|
|
length_from=lambda pkt:pkt.length - 2)
|
|
]
|
|
|
|
|
|
TCPOptions = (
|
|
{0: ("EOL", None),
|
|
1: ("NOP", None),
|
|
2: ("MSS", "!H"),
|
|
3: ("WScale", "!B"),
|
|
4: ("SAckOK", None),
|
|
5: ("SAck", "!"),
|
|
8: ("Timestamp", "!II"),
|
|
14: ("AltChkSum", "!BH"),
|
|
15: ("AltChkSumOpt", None),
|
|
25: ("Mood", "!p"),
|
|
28: ("UTO", "!H"),
|
|
34: ("TFO", "!II"),
|
|
# RFC 3692
|
|
# 253: ("Experiment", "!HHHH"),
|
|
# 254: ("Experiment", "!HHHH"),
|
|
},
|
|
{"EOL": 0,
|
|
"NOP": 1,
|
|
"MSS": 2,
|
|
"WScale": 3,
|
|
"SAckOK": 4,
|
|
"SAck": 5,
|
|
"Timestamp": 8,
|
|
"AltChkSum": 14,
|
|
"AltChkSumOpt": 15,
|
|
"Mood": 25,
|
|
"UTO": 28,
|
|
"TFO": 34,
|
|
})
|
|
|
|
|
|
class RandTCPOptions(VolatileValue):
|
|
def __init__(self, size=None):
|
|
if size is None:
|
|
size = RandNum(1, 5)
|
|
self.size = size
|
|
|
|
def _fix(self):
|
|
# Pseudo-Random amount of options
|
|
# Random ("NAME", fmt)
|
|
rand_patterns = [
|
|
random.choice(list(
|
|
(opt, fmt) for opt, fmt in six.itervalues(TCPOptions[0])
|
|
if opt != 'EOL'
|
|
))
|
|
for _ in range(self.size)
|
|
]
|
|
rand_vals = []
|
|
for oname, fmt in rand_patterns:
|
|
if fmt is None:
|
|
rand_vals.append((oname, b''))
|
|
else:
|
|
# Process the fmt arguments 1 by 1
|
|
structs = fmt[1:] if fmt[0] == "!" else fmt
|
|
rval = []
|
|
for stru in structs:
|
|
stru = "!" + stru
|
|
if "s" in stru or "p" in stru: # str / chr
|
|
v = bytes(RandBin(struct.calcsize(stru)))
|
|
else: # int
|
|
_size = struct.calcsize(stru)
|
|
v = random.randint(0, 2 ** (8 * _size) - 1)
|
|
rval.append(v)
|
|
rand_vals.append((oname, tuple(rval)))
|
|
return rand_vals
|
|
|
|
def __bytes__(self):
|
|
return TCPOptionsField.i2m(None, None, self._fix())
|
|
|
|
|
|
class TCPOptionsField(StrField):
|
|
islist = 1
|
|
|
|
def getfield(self, pkt, s):
|
|
opsz = (pkt.dataofs - 5) * 4
|
|
if opsz < 0:
|
|
warning("bad dataofs (%i). Assuming dataofs=5" % pkt.dataofs)
|
|
opsz = 0
|
|
return s[opsz:], self.m2i(pkt, s[:opsz])
|
|
|
|
def m2i(self, pkt, x):
|
|
opt = []
|
|
while x:
|
|
onum = orb(x[0])
|
|
if onum == 0:
|
|
opt.append(("EOL", None))
|
|
break
|
|
if onum == 1:
|
|
opt.append(("NOP", None))
|
|
x = x[1:]
|
|
continue
|
|
try:
|
|
olen = orb(x[1])
|
|
except IndexError:
|
|
olen = 0
|
|
if olen < 2:
|
|
warning("Malformed TCP option (announced length is %i)" % olen)
|
|
olen = 2
|
|
oval = x[2:olen]
|
|
if onum in TCPOptions[0]:
|
|
oname, ofmt = TCPOptions[0][onum]
|
|
if onum == 5: # SAck
|
|
ofmt += "%iI" % (len(oval) // 4)
|
|
if ofmt and struct.calcsize(ofmt) == len(oval):
|
|
oval = struct.unpack(ofmt, oval)
|
|
if len(oval) == 1:
|
|
oval = oval[0]
|
|
opt.append((oname, oval))
|
|
else:
|
|
opt.append((onum, oval))
|
|
x = x[olen:]
|
|
return opt
|
|
|
|
def i2h(self, pkt, x):
|
|
if not x:
|
|
return []
|
|
return x
|
|
|
|
def i2m(self, pkt, x):
|
|
opt = b""
|
|
for oname, oval in x:
|
|
# We check for a (0, b'') or (1, b'') option first
|
|
oname = {0: "EOL", 1: "NOP"}.get(oname, oname)
|
|
if isinstance(oname, str):
|
|
if oname == "NOP":
|
|
opt += b"\x01"
|
|
continue
|
|
elif oname == "EOL":
|
|
opt += b"\x00"
|
|
continue
|
|
elif oname in TCPOptions[1]:
|
|
onum = TCPOptions[1][oname]
|
|
ofmt = TCPOptions[0][onum][1]
|
|
if onum == 5: # SAck
|
|
ofmt += "%iI" % len(oval)
|
|
_test_isinstance = not isinstance(oval, (bytes, str))
|
|
if ofmt is not None and (_test_isinstance or "s" in ofmt):
|
|
if not isinstance(oval, tuple):
|
|
oval = (oval,)
|
|
oval = struct.pack(ofmt, *oval)
|
|
else:
|
|
warning("option [%s] unknown. Skipped.", oname)
|
|
continue
|
|
else:
|
|
onum = oname
|
|
if not isinstance(onum, int):
|
|
warning("Invalid option number [%i]" % onum)
|
|
continue
|
|
if not isinstance(oval, (bytes, str)):
|
|
warning("option [%i] is not bytes." % onum)
|
|
continue
|
|
if isinstance(oval, str):
|
|
oval = bytes_encode(oval)
|
|
opt += chb(onum) + chb(2 + len(oval)) + oval
|
|
return opt + b"\x00" * (3 - ((len(opt) + 3) % 4)) # Padding
|
|
|
|
def randval(self):
|
|
return RandTCPOptions()
|
|
|
|
|
|
class ICMPTimeStampField(IntField):
|
|
re_hmsm = re.compile("([0-2]?[0-9])[Hh:](([0-5]?[0-9])([Mm:]([0-5]?[0-9])([sS:.]([0-9]{0,3}))?)?)?$") # noqa: E501
|
|
|
|
def i2repr(self, pkt, val):
|
|
if val is None:
|
|
return "--"
|
|
else:
|
|
sec, milli = divmod(val, 1000)
|
|
min, sec = divmod(sec, 60)
|
|
hour, min = divmod(min, 60)
|
|
return "%d:%d:%d.%d" % (hour, min, sec, int(milli))
|
|
|
|
def any2i(self, pkt, val):
|
|
if isinstance(val, str):
|
|
hmsms = self.re_hmsm.match(val)
|
|
if hmsms:
|
|
h, _, m, _, s, _, ms = hmsms.groups()
|
|
ms = int(((ms or "") + "000")[:3])
|
|
val = ((int(h) * 60 + int(m or 0)) * 60 + int(s or 0)) * 1000 + ms # noqa: E501
|
|
else:
|
|
val = 0
|
|
elif val is None:
|
|
val = int((time.time() % (24 * 60 * 60)) * 1000)
|
|
return val
|
|
|
|
|
|
class DestIPField(IPField, DestField):
|
|
bindings = {}
|
|
|
|
def __init__(self, name, default):
|
|
IPField.__init__(self, name, None)
|
|
DestField.__init__(self, name, default)
|
|
|
|
def i2m(self, pkt, x):
|
|
if x is None:
|
|
x = self.dst_from_pkt(pkt)
|
|
return IPField.i2m(self, pkt, x)
|
|
|
|
def i2h(self, pkt, x):
|
|
if x is None:
|
|
x = self.dst_from_pkt(pkt)
|
|
return IPField.i2h(self, pkt, x)
|
|
|
|
|
|
class IP(Packet, IPTools):
|
|
__slots__ = ["_defrag_pos"]
|
|
name = "IP"
|
|
fields_desc = [BitField("version", 4, 4),
|
|
BitField("ihl", None, 4),
|
|
XByteField("tos", 0),
|
|
ShortField("len", None),
|
|
ShortField("id", 1),
|
|
FlagsField("flags", 0, 3, ["MF", "DF", "evil"]),
|
|
BitField("frag", 0, 13),
|
|
ByteField("ttl", 64),
|
|
ByteEnumField("proto", 0, IP_PROTOS),
|
|
XShortField("chksum", None),
|
|
# IPField("src", "127.0.0.1"),
|
|
Emph(SourceIPField("src", "dst")),
|
|
Emph(DestIPField("dst", "127.0.0.1")),
|
|
PacketListField("options", [], IPOption, length_from=lambda p:p.ihl * 4 - 20)] # noqa: E501
|
|
|
|
def post_build(self, p, pay):
|
|
ihl = self.ihl
|
|
p += b"\0" * ((-len(p)) % 4) # pad IP options if needed
|
|
if ihl is None:
|
|
ihl = len(p) // 4
|
|
p = chb(((self.version & 0xf) << 4) | ihl & 0x0f) + p[1:]
|
|
if self.len is None:
|
|
tmp_len = len(p) + len(pay)
|
|
p = p[:2] + struct.pack("!H", tmp_len) + p[4:]
|
|
if self.chksum is None:
|
|
ck = checksum(p)
|
|
p = p[:10] + chb(ck >> 8) + chb(ck & 0xff) + p[12:]
|
|
return p + pay
|
|
|
|
def extract_padding(self, s):
|
|
tmp_len = self.len - (self.ihl << 2)
|
|
if tmp_len < 0:
|
|
return s, b""
|
|
return s[:tmp_len], s[tmp_len:]
|
|
|
|
def route(self):
|
|
dst = self.dst
|
|
if isinstance(dst, Gen):
|
|
dst = next(iter(dst))
|
|
if conf.route is None:
|
|
# unused import, only to initialize conf.route
|
|
import scapy.route # noqa: F401
|
|
return conf.route.route(dst)
|
|
|
|
def hashret(self):
|
|
if ((self.proto == socket.IPPROTO_ICMP) and
|
|
(isinstance(self.payload, ICMP)) and
|
|
(self.payload.type in [3, 4, 5, 11, 12])):
|
|
return self.payload.payload.hashret()
|
|
if not conf.checkIPinIP and self.proto in [4, 41]: # IP, IPv6
|
|
return self.payload.hashret()
|
|
if self.dst == "224.0.0.251": # mDNS
|
|
return struct.pack("B", self.proto) + self.payload.hashret()
|
|
if conf.checkIPsrc and conf.checkIPaddr:
|
|
return (strxor(inet_pton(socket.AF_INET, self.src),
|
|
inet_pton(socket.AF_INET, self.dst)) +
|
|
struct.pack("B", self.proto) + self.payload.hashret())
|
|
return struct.pack("B", self.proto) + self.payload.hashret()
|
|
|
|
def answers(self, other):
|
|
if not conf.checkIPinIP: # skip IP in IP and IPv6 in IP
|
|
if self.proto in [4, 41]:
|
|
return self.payload.answers(other)
|
|
if isinstance(other, IP) and other.proto in [4, 41]:
|
|
return self.answers(other.payload)
|
|
if conf.ipv6_enabled \
|
|
and isinstance(other, scapy.layers.inet6.IPv6) \
|
|
and other.nh in [4, 41]:
|
|
return self.answers(other.payload)
|
|
if not isinstance(other, IP):
|
|
return 0
|
|
if conf.checkIPaddr:
|
|
if other.dst == "224.0.0.251" and self.dst == "224.0.0.251": # mDNS # noqa: E501
|
|
return self.payload.answers(other.payload)
|
|
elif (self.dst != other.src):
|
|
return 0
|
|
if ((self.proto == socket.IPPROTO_ICMP) and
|
|
(isinstance(self.payload, ICMP)) and
|
|
(self.payload.type in [3, 4, 5, 11, 12])):
|
|
# ICMP error message
|
|
return self.payload.payload.answers(other)
|
|
|
|
else:
|
|
if ((conf.checkIPaddr and (self.src != other.dst)) or
|
|
(self.proto != other.proto)):
|
|
return 0
|
|
return self.payload.answers(other.payload)
|
|
|
|
def mysummary(self):
|
|
s = self.sprintf("%IP.src% > %IP.dst% %IP.proto%")
|
|
if self.frag:
|
|
s += " frag:%i" % self.frag
|
|
return s
|
|
|
|
def fragment(self, fragsize=1480):
|
|
"""Fragment IP datagrams"""
|
|
fragsize = (fragsize + 7) // 8 * 8
|
|
lst = []
|
|
fnb = 0
|
|
fl = self
|
|
while fl.underlayer is not None:
|
|
fnb += 1
|
|
fl = fl.underlayer
|
|
|
|
for p in fl:
|
|
s = raw(p[fnb].payload)
|
|
nb = (len(s) + fragsize - 1) // fragsize
|
|
for i in range(nb):
|
|
q = p.copy()
|
|
del(q[fnb].payload)
|
|
del(q[fnb].chksum)
|
|
del(q[fnb].len)
|
|
if i != nb - 1:
|
|
q[fnb].flags |= 1
|
|
q[fnb].frag += i * fragsize // 8
|
|
r = conf.raw_layer(load=s[i * fragsize:(i + 1) * fragsize])
|
|
r.overload_fields = p[fnb].payload.overload_fields.copy()
|
|
q.add_payload(r)
|
|
lst.append(q)
|
|
return lst
|
|
|
|
|
|
def in4_chksum(proto, u, p):
|
|
"""
|
|
As Specified in RFC 2460 - 8.1 Upper-Layer Checksums
|
|
|
|
Performs IPv4 Upper Layer checksum computation. Provided parameters are:
|
|
- 'proto' : value of upper layer protocol
|
|
- 'u' : IP upper layer instance
|
|
- 'p' : the payload of the upper layer provided as a string
|
|
"""
|
|
if not isinstance(u, IP):
|
|
warning("No IP underlayer to compute checksum. Leaving null.")
|
|
return 0
|
|
if u.len is not None:
|
|
if u.ihl is None:
|
|
olen = sum(len(x) for x in u.options)
|
|
ihl = 5 + olen // 4 + (1 if olen % 4 else 0)
|
|
else:
|
|
ihl = u.ihl
|
|
ln = max(u.len - 4 * ihl, 0)
|
|
else:
|
|
ln = len(p)
|
|
psdhdr = struct.pack("!4s4sHH",
|
|
inet_pton(socket.AF_INET, u.src),
|
|
inet_pton(socket.AF_INET, u.dst),
|
|
proto,
|
|
ln)
|
|
return checksum(psdhdr + p)
|
|
|
|
|
|
class TCP(Packet):
|
|
name = "TCP"
|
|
fields_desc = [ShortEnumField("sport", 20, TCP_SERVICES),
|
|
ShortEnumField("dport", 80, TCP_SERVICES),
|
|
IntField("seq", 0),
|
|
IntField("ack", 0),
|
|
BitField("dataofs", None, 4),
|
|
BitField("reserved", 0, 3),
|
|
FlagsField("flags", 0x2, 9, "FSRPAUECN"),
|
|
ShortField("window", 8192),
|
|
XShortField("chksum", None),
|
|
ShortField("urgptr", 0),
|
|
TCPOptionsField("options", "")]
|
|
|
|
def post_build(self, p, pay):
|
|
p += pay
|
|
dataofs = self.dataofs
|
|
if dataofs is None:
|
|
opt_len = len(self.get_field("options").i2m(self, self.options))
|
|
dataofs = 5 + ((opt_len + 3) // 4)
|
|
dataofs = (dataofs << 4) | orb(p[12]) & 0x0f
|
|
p = p[:12] + chb(dataofs & 0xff) + p[13:]
|
|
if self.chksum is None:
|
|
if isinstance(self.underlayer, IP):
|
|
ck = in4_chksum(socket.IPPROTO_TCP, self.underlayer, p)
|
|
p = p[:16] + struct.pack("!H", ck) + p[18:]
|
|
elif conf.ipv6_enabled and isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501
|
|
ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_TCP, self.underlayer, p) # noqa: E501
|
|
p = p[:16] + struct.pack("!H", ck) + p[18:]
|
|
else:
|
|
warning("No IP underlayer to compute checksum. Leaving null.")
|
|
return p
|
|
|
|
def hashret(self):
|
|
if conf.checkIPsrc:
|
|
return struct.pack("H", self.sport ^ self.dport) + self.payload.hashret() # noqa: E501
|
|
else:
|
|
return self.payload.hashret()
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, TCP):
|
|
return 0
|
|
# RST packets don't get answers
|
|
if other.flags.R:
|
|
return 0
|
|
# We do not support the four-way handshakes with the SYN+ACK
|
|
# answer split in two packets (one ACK and one SYN): in that
|
|
# case the ACK will be seen as an answer, but not the SYN.
|
|
if self.flags.S:
|
|
# SYN packets without ACK are not answers
|
|
if not self.flags.A:
|
|
return 0
|
|
# SYN+ACK packets answer SYN packets
|
|
if not other.flags.S:
|
|
return 0
|
|
if conf.checkIPsrc:
|
|
if not ((self.sport == other.dport) and
|
|
(self.dport == other.sport)):
|
|
return 0
|
|
# Do not check ack value for SYN packets without ACK
|
|
if not (other.flags.S and not other.flags.A) \
|
|
and abs(other.ack - self.seq) > 2:
|
|
return 0
|
|
# Do not check ack value for RST packets without ACK
|
|
if self.flags.R and not self.flags.A:
|
|
return 1
|
|
if abs(other.seq - self.ack) > 2 + len(other.payload):
|
|
return 0
|
|
return 1
|
|
|
|
def mysummary(self):
|
|
if isinstance(self.underlayer, IP):
|
|
return self.underlayer.sprintf("TCP %IP.src%:%TCP.sport% > %IP.dst%:%TCP.dport% %TCP.flags%") # noqa: E501
|
|
elif conf.ipv6_enabled and isinstance(self.underlayer, scapy.layers.inet6.IPv6): # noqa: E501
|
|
return self.underlayer.sprintf("TCP %IPv6.src%:%TCP.sport% > %IPv6.dst%:%TCP.dport% %TCP.flags%") # noqa: E501
|
|
else:
|
|
return self.sprintf("TCP %TCP.sport% > %TCP.dport% %TCP.flags%")
|
|
|
|
|
|
class UDP(Packet):
|
|
name = "UDP"
|
|
fields_desc = [ShortEnumField("sport", 53, UDP_SERVICES),
|
|
ShortEnumField("dport", 53, UDP_SERVICES),
|
|
ShortField("len", None),
|
|
XShortField("chksum", None), ]
|
|
|
|
def post_build(self, p, pay):
|
|
p += pay
|
|
tmp_len = self.len
|
|
if tmp_len is None:
|
|
tmp_len = len(p)
|
|
p = p[:4] + struct.pack("!H", tmp_len) + p[6:]
|
|
if self.chksum is None:
|
|
if isinstance(self.underlayer, IP):
|
|
ck = in4_chksum(socket.IPPROTO_UDP, self.underlayer, p)
|
|
# According to RFC768 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501
|
|
if ck == 0:
|
|
ck = 0xFFFF
|
|
p = p[:6] + struct.pack("!H", ck) + p[8:]
|
|
elif isinstance(self.underlayer, scapy.layers.inet6.IPv6) or isinstance(self.underlayer, scapy.layers.inet6._IPv6ExtHdr): # noqa: E501
|
|
ck = scapy.layers.inet6.in6_chksum(socket.IPPROTO_UDP, self.underlayer, p) # noqa: E501
|
|
# According to RFC2460 if the result checksum is 0, it should be set to 0xFFFF # noqa: E501
|
|
if ck == 0:
|
|
ck = 0xFFFF
|
|
p = p[:6] + struct.pack("!H", ck) + p[8:]
|
|
else:
|
|
warning("No IP underlayer to compute checksum. Leaving null.")
|
|
return p
|
|
|
|
def extract_padding(self, s):
|
|
tmp_len = self.len - 8
|
|
return s[:tmp_len], s[tmp_len:]
|
|
|
|
def hashret(self):
|
|
return self.payload.hashret()
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, UDP):
|
|
return 0
|
|
if conf.checkIPsrc:
|
|
if self.dport != other.sport:
|
|
return 0
|
|
return self.payload.answers(other.payload)
|
|
|
|
def mysummary(self):
|
|
if isinstance(self.underlayer, IP):
|
|
return self.underlayer.sprintf("UDP %IP.src%:%UDP.sport% > %IP.dst%:%UDP.dport%") # noqa: E501
|
|
elif isinstance(self.underlayer, scapy.layers.inet6.IPv6):
|
|
return self.underlayer.sprintf("UDP %IPv6.src%:%UDP.sport% > %IPv6.dst%:%UDP.dport%") # noqa: E501
|
|
else:
|
|
return self.sprintf("UDP %UDP.sport% > %UDP.dport%")
|
|
|
|
|
|
icmptypes = {0: "echo-reply",
|
|
3: "dest-unreach",
|
|
4: "source-quench",
|
|
5: "redirect",
|
|
8: "echo-request",
|
|
9: "router-advertisement",
|
|
10: "router-solicitation",
|
|
11: "time-exceeded",
|
|
12: "parameter-problem",
|
|
13: "timestamp-request",
|
|
14: "timestamp-reply",
|
|
15: "information-request",
|
|
16: "information-response",
|
|
17: "address-mask-request",
|
|
18: "address-mask-reply",
|
|
30: "traceroute",
|
|
31: "datagram-conversion-error",
|
|
32: "mobile-host-redirect",
|
|
33: "ipv6-where-are-you",
|
|
34: "ipv6-i-am-here",
|
|
35: "mobile-registration-request",
|
|
36: "mobile-registration-reply",
|
|
37: "domain-name-request",
|
|
38: "domain-name-reply",
|
|
39: "skip",
|
|
40: "photuris"}
|
|
|
|
|
|
icmpcodes = {3: {0: "network-unreachable",
|
|
1: "host-unreachable",
|
|
2: "protocol-unreachable",
|
|
3: "port-unreachable",
|
|
4: "fragmentation-needed",
|
|
5: "source-route-failed",
|
|
6: "network-unknown",
|
|
7: "host-unknown",
|
|
9: "network-prohibited",
|
|
10: "host-prohibited",
|
|
11: "TOS-network-unreachable",
|
|
12: "TOS-host-unreachable",
|
|
13: "communication-prohibited",
|
|
14: "host-precedence-violation",
|
|
15: "precedence-cutoff", },
|
|
5: {0: "network-redirect",
|
|
1: "host-redirect",
|
|
2: "TOS-network-redirect",
|
|
3: "TOS-host-redirect", },
|
|
11: {0: "ttl-zero-during-transit",
|
|
1: "ttl-zero-during-reassembly", },
|
|
12: {0: "ip-header-bad",
|
|
1: "required-option-missing", },
|
|
40: {0: "bad-spi",
|
|
1: "authentication-failed",
|
|
2: "decompression-failed",
|
|
3: "decryption-failed",
|
|
4: "need-authentification",
|
|
5: "need-authorization", }, }
|
|
|
|
|
|
class ICMP(Packet):
|
|
name = "ICMP"
|
|
fields_desc = [ByteEnumField("type", 8, icmptypes),
|
|
MultiEnumField("code", 0, icmpcodes, depends_on=lambda pkt:pkt.type, fmt="B"), # noqa: E501
|
|
XShortField("chksum", None),
|
|
ConditionalField(XShortField("id", 0), lambda pkt:pkt.type in [0, 8, 13, 14, 15, 16, 17, 18]), # noqa: E501
|
|
ConditionalField(XShortField("seq", 0), lambda pkt:pkt.type in [0, 8, 13, 14, 15, 16, 17, 18]), # noqa: E501
|
|
ConditionalField(ICMPTimeStampField("ts_ori", None), lambda pkt:pkt.type in [13, 14]), # noqa: E501
|
|
ConditionalField(ICMPTimeStampField("ts_rx", None), lambda pkt:pkt.type in [13, 14]), # noqa: E501
|
|
ConditionalField(ICMPTimeStampField("ts_tx", None), lambda pkt:pkt.type in [13, 14]), # noqa: E501
|
|
ConditionalField(IPField("gw", "0.0.0.0"), lambda pkt:pkt.type == 5), # noqa: E501
|
|
ConditionalField(ByteField("ptr", 0), lambda pkt:pkt.type == 12), # noqa: E501
|
|
ConditionalField(ByteField("reserved", 0), lambda pkt:pkt.type in [3, 11]), # noqa: E501
|
|
ConditionalField(ByteField("length", 0), lambda pkt:pkt.type in [3, 11, 12]), # noqa: E501
|
|
ConditionalField(IPField("addr_mask", "0.0.0.0"), lambda pkt:pkt.type in [17, 18]), # noqa: E501
|
|
ConditionalField(ShortField("nexthopmtu", 0), lambda pkt:pkt.type == 3), # noqa: E501
|
|
ConditionalField(ShortField("unused", 0), lambda pkt:pkt.type in [11, 12]), # noqa: E501
|
|
ConditionalField(IntField("unused", 0), lambda pkt:pkt.type not in [0, 3, 5, 8, 11, 12, 13, 14, 15, 16, 17, 18]) # noqa: E501
|
|
]
|
|
|
|
def post_build(self, p, pay):
|
|
p += pay
|
|
if self.chksum is None:
|
|
ck = checksum(p)
|
|
p = p[:2] + chb(ck >> 8) + chb(ck & 0xff) + p[4:]
|
|
return p
|
|
|
|
def hashret(self):
|
|
if self.type in [0, 8, 13, 14, 15, 16, 17, 18, 33, 34, 35, 36, 37, 38]:
|
|
return struct.pack("HH", self.id, self.seq) + self.payload.hashret() # noqa: E501
|
|
return self.payload.hashret()
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, ICMP):
|
|
return 0
|
|
if ((other.type, self.type) in [(8, 0), (13, 14), (15, 16), (17, 18), (33, 34), (35, 36), (37, 38)] and # noqa: E501
|
|
self.id == other.id and
|
|
self.seq == other.seq):
|
|
return 1
|
|
return 0
|
|
|
|
def guess_payload_class(self, payload):
|
|
if self.type in [3, 4, 5, 11, 12]:
|
|
return IPerror
|
|
else:
|
|
return None
|
|
|
|
def mysummary(self):
|
|
if isinstance(self.underlayer, IP):
|
|
return self.underlayer.sprintf("ICMP %IP.src% > %IP.dst% %ICMP.type% %ICMP.code%") # noqa: E501
|
|
else:
|
|
return self.sprintf("ICMP %ICMP.type% %ICMP.code%")
|
|
|
|
|
|
class IPerror(IP):
|
|
name = "IP in ICMP"
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, IP):
|
|
return 0
|
|
|
|
# Check if IP addresses match
|
|
test_IPsrc = not conf.checkIPsrc or self.src == other.src
|
|
test_IPdst = self.dst == other.dst
|
|
|
|
# Check if IP ids match
|
|
test_IPid = not conf.checkIPID or self.id == other.id
|
|
test_IPid |= conf.checkIPID and self.id == socket.htons(other.id)
|
|
|
|
# Check if IP protocols match
|
|
test_IPproto = self.proto == other.proto
|
|
|
|
if not (test_IPsrc and test_IPdst and test_IPid and test_IPproto):
|
|
return 0
|
|
|
|
return self.payload.answers(other.payload)
|
|
|
|
def mysummary(self):
|
|
return Packet.mysummary(self)
|
|
|
|
|
|
class TCPerror(TCP):
|
|
name = "TCP in ICMP"
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, TCP):
|
|
return 0
|
|
if conf.checkIPsrc:
|
|
if not ((self.sport == other.sport) and
|
|
(self.dport == other.dport)):
|
|
return 0
|
|
if conf.check_TCPerror_seqack:
|
|
if self.seq is not None:
|
|
if self.seq != other.seq:
|
|
return 0
|
|
if self.ack is not None:
|
|
if self.ack != other.ack:
|
|
return 0
|
|
return 1
|
|
|
|
def mysummary(self):
|
|
return Packet.mysummary(self)
|
|
|
|
|
|
class UDPerror(UDP):
|
|
name = "UDP in ICMP"
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, UDP):
|
|
return 0
|
|
if conf.checkIPsrc:
|
|
if not ((self.sport == other.sport) and
|
|
(self.dport == other.dport)):
|
|
return 0
|
|
return 1
|
|
|
|
def mysummary(self):
|
|
return Packet.mysummary(self)
|
|
|
|
|
|
class ICMPerror(ICMP):
|
|
name = "ICMP in ICMP"
|
|
|
|
def answers(self, other):
|
|
if not isinstance(other, ICMP):
|
|
return 0
|
|
if not ((self.type == other.type) and
|
|
(self.code == other.code)):
|
|
return 0
|
|
if self.code in [0, 8, 13, 14, 17, 18]:
|
|
if (self.id == other.id and
|
|
self.seq == other.seq):
|
|
return 1
|
|
else:
|
|
return 0
|
|
else:
|
|
return 1
|
|
|
|
def mysummary(self):
|
|
return Packet.mysummary(self)
|
|
|
|
|
|
bind_layers(Ether, IP, type=2048)
|
|
bind_layers(CookedLinux, IP, proto=2048)
|
|
bind_layers(GRE, IP, proto=2048)
|
|
bind_layers(SNAP, IP, code=2048)
|
|
bind_bottom_up(Loopback, IP, type=0)
|
|
bind_layers(Loopback, IP, type=socket.AF_INET)
|
|
bind_layers(IPerror, IPerror, frag=0, proto=4)
|
|
bind_layers(IPerror, ICMPerror, frag=0, proto=1)
|
|
bind_layers(IPerror, TCPerror, frag=0, proto=6)
|
|
bind_layers(IPerror, UDPerror, frag=0, proto=17)
|
|
bind_layers(IP, IP, frag=0, proto=4)
|
|
bind_layers(IP, ICMP, frag=0, proto=1)
|
|
bind_layers(IP, TCP, frag=0, proto=6)
|
|
bind_layers(IP, UDP, frag=0, proto=17)
|
|
bind_layers(IP, GRE, frag=0, proto=47)
|
|
|
|
conf.l2types.register(DLT_RAW, IP)
|
|
conf.l2types.register_num2layer(DLT_RAW_ALT, IP)
|
|
conf.l2types.register(DLT_IPV4, IP)
|
|
|
|
conf.l3types.register(ETH_P_IP, IP)
|
|
conf.l3types.register_num2layer(ETH_P_ALL, IP)
|
|
|
|
|
|
def inet_register_l3(l2, l3):
|
|
return getmacbyip(l3.dst)
|
|
|
|
|
|
conf.neighbor.register_l3(Ether, IP, inet_register_l3)
|
|
conf.neighbor.register_l3(Dot3, IP, inet_register_l3)
|
|
|
|
|
|
###################
|
|
# Fragmentation #
|
|
###################
|
|
|
|
@conf.commands.register
|
|
def fragment(pkt, fragsize=1480):
|
|
"""Fragment a big IP datagram"""
|
|
fragsize = (fragsize + 7) // 8 * 8
|
|
lst = []
|
|
for p in pkt:
|
|
s = raw(p[IP].payload)
|
|
nb = (len(s) + fragsize - 1) // fragsize
|
|
for i in range(nb):
|
|
q = p.copy()
|
|
del(q[IP].payload)
|
|
del(q[IP].chksum)
|
|
del(q[IP].len)
|
|
if i != nb - 1:
|
|
q[IP].flags |= 1
|
|
q[IP].frag += i * fragsize // 8
|
|
r = conf.raw_layer(load=s[i * fragsize:(i + 1) * fragsize])
|
|
r.overload_fields = p[IP].payload.overload_fields.copy()
|
|
q.add_payload(r)
|
|
lst.append(q)
|
|
return lst
|
|
|
|
|
|
@conf.commands.register
|
|
def overlap_frag(p, overlap, fragsize=8, overlap_fragsize=None):
|
|
"""Build overlapping fragments to bypass NIPS
|
|
|
|
p: the original packet
|
|
overlap: the overlapping data
|
|
fragsize: the fragment size of the packet
|
|
overlap_fragsize: the fragment size of the overlapping packet"""
|
|
|
|
if overlap_fragsize is None:
|
|
overlap_fragsize = fragsize
|
|
q = p.copy()
|
|
del(q[IP].payload)
|
|
q[IP].add_payload(overlap)
|
|
|
|
qfrag = fragment(q, overlap_fragsize)
|
|
qfrag[-1][IP].flags |= 1
|
|
return qfrag + fragment(p, fragsize)
|
|
|
|
|
|
def _defrag_list(lst, defrag, missfrag):
|
|
"""Internal usage only. Part of the _defrag_logic"""
|
|
p = lst[0]
|
|
lastp = lst[-1]
|
|
if p.frag > 0 or lastp.flags.MF: # first or last fragment missing
|
|
missfrag.append(lst)
|
|
return
|
|
p = p.copy()
|
|
if conf.padding_layer in p:
|
|
del(p[conf.padding_layer].underlayer.payload)
|
|
ip = p[IP]
|
|
if ip.len is None or ip.ihl is None:
|
|
clen = len(ip.payload)
|
|
else:
|
|
clen = ip.len - (ip.ihl << 2)
|
|
txt = conf.raw_layer()
|
|
for q in lst[1:]:
|
|
if clen != q.frag << 3: # Wrong fragmentation offset
|
|
if clen > q.frag << 3:
|
|
warning("Fragment overlap (%i > %i) %r || %r || %r" % (clen, q.frag << 3, p, txt, q)) # noqa: E501
|
|
missfrag.append(lst)
|
|
break
|
|
if q[IP].len is None or q[IP].ihl is None:
|
|
clen += len(q[IP].payload)
|
|
else:
|
|
clen += q[IP].len - (q[IP].ihl << 2)
|
|
if conf.padding_layer in q:
|
|
del(q[conf.padding_layer].underlayer.payload)
|
|
txt.add_payload(q[IP].payload.copy())
|
|
if q.time > p.time:
|
|
p.time = q.time
|
|
else:
|
|
ip.flags.MF = False
|
|
del(ip.chksum)
|
|
del(ip.len)
|
|
p = p / txt
|
|
p._defrag_pos = max(x._defrag_pos for x in lst)
|
|
defrag.append(p)
|
|
|
|
|
|
def _defrag_logic(plist, complete=False):
|
|
"""Internal function used to defragment a list of packets.
|
|
It contains the logic behind the defrag() and defragment() functions
|
|
"""
|
|
frags = defaultdict(lambda: [])
|
|
final = []
|
|
pos = 0
|
|
for p in plist:
|
|
p._defrag_pos = pos
|
|
pos += 1
|
|
if IP in p:
|
|
ip = p[IP]
|
|
if ip.frag != 0 or ip.flags.MF:
|
|
uniq = (ip.id, ip.src, ip.dst, ip.proto)
|
|
frags[uniq].append(p)
|
|
continue
|
|
final.append(p)
|
|
|
|
defrag = []
|
|
missfrag = []
|
|
for lst in six.itervalues(frags):
|
|
lst.sort(key=lambda x: x.frag)
|
|
_defrag_list(lst, defrag, missfrag)
|
|
defrag2 = []
|
|
for p in defrag:
|
|
q = p.__class__(raw(p))
|
|
q._defrag_pos = p._defrag_pos
|
|
q.time = p.time
|
|
defrag2.append(q)
|
|
if complete:
|
|
final.extend(defrag2)
|
|
final.extend(missfrag)
|
|
final.sort(key=lambda x: x._defrag_pos)
|
|
if hasattr(plist, "listname"):
|
|
name = "Defragmented %s" % plist.listname
|
|
else:
|
|
name = "Defragmented"
|
|
return PacketList(final, name=name)
|
|
else:
|
|
return PacketList(final), PacketList(defrag2), PacketList(missfrag)
|
|
|
|
|
|
@conf.commands.register
|
|
def defrag(plist):
|
|
"""defrag(plist) -> ([not fragmented], [defragmented],
|
|
[ [bad fragments], [bad fragments], ... ])"""
|
|
return _defrag_logic(plist, complete=False)
|
|
|
|
|
|
@conf.commands.register
|
|
def defragment(plist):
|
|
"""defragment(plist) -> plist defragmented as much as possible """
|
|
return _defrag_logic(plist, complete=True)
|
|
|
|
|
|
# Add timeskew_graph() method to PacketList
|
|
def _packetlist_timeskew_graph(self, ip, **kargs):
|
|
"""Tries to graph the timeskew between the timestamps and real time for a given ip""" # noqa: E501
|
|
|
|
# Filter TCP segments which source address is 'ip'
|
|
tmp = (self._elt2pkt(x) for x in self.res)
|
|
b = (x for x in tmp if IP in x and x[IP].src == ip and TCP in x)
|
|
|
|
# Build a list of tuples (creation_time, replied_timestamp)
|
|
c = []
|
|
tsf = ICMPTimeStampField("", None)
|
|
for p in b:
|
|
opts = p.getlayer(TCP).options
|
|
for o in opts:
|
|
if o[0] == "Timestamp":
|
|
c.append((p.time, tsf.any2i("", o[1][0])))
|
|
|
|
# Stop if the list is empty
|
|
if not c:
|
|
warning("No timestamps found in packet list")
|
|
return []
|
|
|
|
# Prepare the data that will be plotted
|
|
first_creation_time = c[0][0]
|
|
first_replied_timestamp = c[0][1]
|
|
|
|
def _wrap_data(ts_tuple, wrap_seconds=2000):
|
|
"""Wrap the list of tuples."""
|
|
|
|
ct, rt = ts_tuple # (creation_time, replied_timestamp)
|
|
X = ct % wrap_seconds
|
|
Y = ((ct - first_creation_time) - ((rt - first_replied_timestamp) / 1000.0)) # noqa: E501
|
|
|
|
return X, Y
|
|
|
|
data = [_wrap_data(e) for e in c]
|
|
|
|
# Mimic the default gnuplot output
|
|
if kargs == {}:
|
|
kargs = MATPLOTLIB_DEFAULT_PLOT_KARGS
|
|
lines = plt.plot(data, **kargs)
|
|
|
|
# Call show() if matplotlib is not inlined
|
|
if not MATPLOTLIB_INLINED:
|
|
plt.show()
|
|
|
|
return lines
|
|
|
|
|
|
PacketList.timeskew_graph = _packetlist_timeskew_graph
|
|
|
|
|
|
# Create a new packet list
|
|
class TracerouteResult(SndRcvList):
|
|
__slots__ = ["graphdef", "graphpadding", "graphASres", "padding", "hloc",
|
|
"nloc"]
|
|
|
|
def __init__(self, res=None, name="Traceroute", stats=None):
|
|
SndRcvList.__init__(self, res, name, stats)
|
|
self.graphdef = None
|
|
self.graphASres = None
|
|
self.padding = 0
|
|
self.hloc = None
|
|
self.nloc = None
|
|
|
|
def show(self):
|
|
return self.make_table(lambda s, r: (s.sprintf("%IP.dst%:{TCP:tcp%ir,TCP.dport%}{UDP:udp%ir,UDP.dport%}{ICMP:ICMP}"), # noqa: E501
|
|
s.ttl,
|
|
r.sprintf("%-15s,IP.src% {TCP:%TCP.flags%}{ICMP:%ir,ICMP.type%}"))) # noqa: E501
|
|
|
|
def get_trace(self):
|
|
trace = {}
|
|
for s, r in self.res:
|
|
if IP not in s:
|
|
continue
|
|
d = s[IP].dst
|
|
if d not in trace:
|
|
trace[d] = {}
|
|
trace[d][s[IP].ttl] = r[IP].src, ICMP not in r
|
|
for k in six.itervalues(trace):
|
|
try:
|
|
m = min(x for x, y in six.iteritems(k) if y[1])
|
|
except ValueError:
|
|
continue
|
|
for l in list(k): # use list(): k is modified in the loop
|
|
if l > m:
|
|
del k[l]
|
|
return trace
|
|
|
|
def trace3D(self, join=True):
|
|
"""Give a 3D representation of the traceroute.
|
|
right button: rotate the scene
|
|
middle button: zoom
|
|
shift-left button: move the scene
|
|
left button on a ball: toggle IP displaying
|
|
double-click button on a ball: scan ports 21,22,23,25,80 and 443 and display the result""" # noqa: E501
|
|
# When not ran from a notebook, vpython pooly closes itself
|
|
# using os._exit once finished. We pack it into a Process
|
|
import multiprocessing
|
|
p = multiprocessing.Process(target=self.trace3D_notebook)
|
|
p.start()
|
|
if join:
|
|
p.join()
|
|
|
|
def trace3D_notebook(self):
|
|
"""Same than trace3D, used when ran from Jupyther notebooks"""
|
|
trace = self.get_trace()
|
|
import vpython
|
|
|
|
class IPsphere(vpython.sphere):
|
|
def __init__(self, ip, **kargs):
|
|
vpython.sphere.__init__(self, **kargs)
|
|
self.ip = ip
|
|
self.label = None
|
|
self.setlabel(self.ip)
|
|
self.last_clicked = None
|
|
self.full = False
|
|
self.savcolor = vpython.vec(*self.color.value)
|
|
|
|
def fullinfos(self):
|
|
self.full = True
|
|
self.color = vpython.vec(1, 0, 0)
|
|
a, b = sr(IP(dst=self.ip) / TCP(dport=[21, 22, 23, 25, 80, 443], flags="S"), timeout=2, verbose=0) # noqa: E501
|
|
if len(a) == 0:
|
|
txt = "%s:\nno results" % self.ip
|
|
else:
|
|
txt = "%s:\n" % self.ip
|
|
for s, r in a:
|
|
txt += r.sprintf("{TCP:%IP.src%:%TCP.sport% %TCP.flags%}{TCPerror:%IPerror.dst%:%TCPerror.dport% %IP.src% %ir,ICMP.type%}\n") # noqa: E501
|
|
self.setlabel(txt, visible=1)
|
|
|
|
def unfull(self):
|
|
self.color = self.savcolor
|
|
self.full = False
|
|
self.setlabel(self.ip)
|
|
|
|
def setlabel(self, txt, visible=None):
|
|
if self.label is not None:
|
|
if visible is None:
|
|
visible = self.label.visible
|
|
self.label.visible = 0
|
|
elif visible is None:
|
|
visible = 0
|
|
self.label = vpython.label(text=txt, pos=self.pos, space=self.radius, xoffset=10, yoffset=20, visible=visible) # noqa: E501
|
|
|
|
def check_double_click(self):
|
|
try:
|
|
if self.full or not self.label.visible:
|
|
return False
|
|
if self.last_clicked is not None:
|
|
return (time.time() - self.last_clicked) < 0.5
|
|
return False
|
|
finally:
|
|
self.last_clicked = time.time()
|
|
|
|
def action(self):
|
|
self.label.visible ^= 1
|
|
if self.full:
|
|
self.unfull()
|
|
|
|
vpython.scene = vpython.canvas()
|
|
vpython.scene.title = "<center><u><b>%s</b></u></center>" % self.listname # noqa: E501
|
|
vpython.scene.append_to_caption(
|
|
re.sub(
|
|
r'\%(.*)\%',
|
|
r'<span style="color: red">\1</span>',
|
|
re.sub(
|
|
r'\`(.*)\`',
|
|
r'<span style="color: #3399ff">\1</span>',
|
|
"""<u><b>Commands:</b></u>
|
|
%Click% to toggle information about a node.
|
|
%Double click% to perform a quick web scan on this node.
|
|
<u><b>Camera usage:</b></u>
|
|
`Right button drag or Ctrl-drag` to rotate "camera" to view scene.
|
|
`Shift-drag` to move the object around.
|
|
`Middle button or Alt-drag` to drag up or down to zoom in or out.
|
|
On a two-button mouse, `middle is wheel or left + right`.
|
|
Touch screen: pinch/extend to zoom, swipe or two-finger rotate."""
|
|
)
|
|
)
|
|
)
|
|
vpython.scene.exit = True
|
|
rings = {}
|
|
tr3d = {}
|
|
for i in trace:
|
|
tr = trace[i]
|
|
tr3d[i] = []
|
|
for t in range(1, max(tr) + 1):
|
|
if t not in rings:
|
|
rings[t] = []
|
|
if t in tr:
|
|
if tr[t] not in rings[t]:
|
|
rings[t].append(tr[t])
|
|
tr3d[i].append(rings[t].index(tr[t]))
|
|
else:
|
|
rings[t].append(("unk", -1))
|
|
tr3d[i].append(len(rings[t]) - 1)
|
|
|
|
for t in rings:
|
|
r = rings[t]
|
|
tmp_len = len(r)
|
|
for i in range(tmp_len):
|
|
if r[i][1] == -1:
|
|
col = vpython.vec(0.75, 0.75, 0.75)
|
|
elif r[i][1]:
|
|
col = vpython.color.green
|
|
else:
|
|
col = vpython.color.blue
|
|
|
|
s = IPsphere(pos=vpython.vec((tmp_len - 1) * vpython.cos(2 * i * vpython.pi / tmp_len), (tmp_len - 1) * vpython.sin(2 * i * vpython.pi / tmp_len), 2 * t), # noqa: E501
|
|
ip=r[i][0],
|
|
color=col)
|
|
for trlst in six.itervalues(tr3d):
|
|
if t <= len(trlst):
|
|
if trlst[t - 1] == i:
|
|
trlst[t - 1] = s
|
|
forecol = colgen(0.625, 0.4375, 0.25, 0.125)
|
|
for trlst in six.itervalues(tr3d):
|
|
col = vpython.vec(*next(forecol))
|
|
start = vpython.vec(0, 0, 0)
|
|
for ip in trlst:
|
|
vpython.cylinder(pos=start, axis=ip.pos - start, color=col, radius=0.2) # noqa: E501
|
|
start = ip.pos
|
|
|
|
vpython.rate(50)
|
|
|
|
# Keys handling
|
|
# TODO: there is currently no way of closing vpython correctly
|
|
# https://github.com/BruceSherwood/vpython-jupyter/issues/36
|
|
# def keyboard_press(ev):
|
|
# k = ev.key
|
|
# if k == "esc" or k == "q":
|
|
# pass # TODO: close
|
|
#
|
|
# vpython.scene.bind('keydown', keyboard_press)
|
|
|
|
# Mouse handling
|
|
def mouse_click(ev):
|
|
if ev.press == "left":
|
|
o = vpython.scene.mouse.pick
|
|
if o and isinstance(o, IPsphere):
|
|
if o.check_double_click():
|
|
if o.ip == "unk":
|
|
return
|
|
o.fullinfos()
|
|
else:
|
|
o.action()
|
|
|
|
vpython.scene.bind('mousedown', mouse_click)
|
|
|
|
def world_trace(self):
|
|
"""Display traceroute results on a world map."""
|
|
|
|
# Check that the geoip2 module can be imported
|
|
# Doc: http://geoip2.readthedocs.io/en/latest/
|
|
try:
|
|
# GeoIP2 modules need to be imported as below
|
|
import geoip2.database
|
|
import geoip2.errors
|
|
except ImportError:
|
|
warning("Cannot import geoip2. Won't be able to plot the world.")
|
|
return []
|
|
# Check availability of database
|
|
if not conf.geoip_city:
|
|
warning("Cannot import the geolite2 CITY database.\n"
|
|
"Download it from http://dev.maxmind.com/geoip/geoip2/geolite2/" # noqa: E501
|
|
" then set its path to conf.geoip_city")
|
|
return []
|
|
# Check availability of plotting devices
|
|
try:
|
|
import cartopy.crs as ccrs
|
|
except ImportError:
|
|
warning("Cannot import cartopy.\n"
|
|
"More infos on http://scitools.org.uk/cartopy/docs/latest/installing.html") # noqa: E501
|
|
return []
|
|
if not MATPLOTLIB:
|
|
warning("Matplotlib is not installed. Won't be able to plot the world.") # noqa: E501
|
|
return []
|
|
|
|
# Open & read the GeoListIP2 database
|
|
try:
|
|
db = geoip2.database.Reader(conf.geoip_city)
|
|
except Exception:
|
|
warning("Cannot open geoip2 database at %s", conf.geoip_city)
|
|
return []
|
|
|
|
# Regroup results per trace
|
|
ips = {}
|
|
rt = {}
|
|
ports_done = {}
|
|
for s, r in self.res:
|
|
ips[r.src] = None
|
|
if s.haslayer(TCP) or s.haslayer(UDP):
|
|
trace_id = (s.src, s.dst, s.proto, s.dport)
|
|
elif s.haslayer(ICMP):
|
|
trace_id = (s.src, s.dst, s.proto, s.type)
|
|
else:
|
|
trace_id = (s.src, s.dst, s.proto, 0)
|
|
trace = rt.get(trace_id, {})
|
|
if not r.haslayer(ICMP) or r.type != 11:
|
|
if trace_id in ports_done:
|
|
continue
|
|
ports_done[trace_id] = None
|
|
trace[s.ttl] = r.src
|
|
rt[trace_id] = trace
|
|
|
|
# Get the addresses locations
|
|
trt = {}
|
|
for trace_id in rt:
|
|
trace = rt[trace_id]
|
|
loctrace = []
|
|
for i in range(max(trace)):
|
|
ip = trace.get(i, None)
|
|
if ip is None:
|
|
continue
|
|
# Fetch database
|
|
try:
|
|
sresult = db.city(ip)
|
|
except geoip2.errors.AddressNotFoundError:
|
|
continue
|
|
loctrace.append((sresult.location.longitude, sresult.location.latitude)) # noqa: E501
|
|
if loctrace:
|
|
trt[trace_id] = loctrace
|
|
|
|
# Load the map renderer
|
|
plt.figure(num='Scapy')
|
|
ax = plt.axes(projection=ccrs.PlateCarree())
|
|
# Draw countries
|
|
ax.coastlines()
|
|
ax.stock_img()
|
|
# Set normal size
|
|
ax.set_global()
|
|
# Add title
|
|
plt.title("Scapy traceroute results")
|
|
|
|
from matplotlib.collections import LineCollection
|
|
from matplotlib import colors as mcolors
|
|
colors_cycle = iter(mcolors.BASE_COLORS)
|
|
lines = []
|
|
|
|
# Split traceroute measurement
|
|
for key, trc in six.iteritems(trt):
|
|
# Get next color
|
|
color = next(colors_cycle)
|
|
# Gather mesurments data
|
|
data_lines = [(trc[i], trc[i + 1]) for i in range(len(trc) - 1)]
|
|
# Create line collection
|
|
line_col = LineCollection(data_lines, linewidths=2,
|
|
label=key[1],
|
|
color=color)
|
|
lines.append(line_col)
|
|
ax.add_collection(line_col)
|
|
# Create map points
|
|
lines.extend([ax.plot(*x, marker='.', color=color) for x in trc])
|
|
|
|
# Generate legend
|
|
ax.legend()
|
|
|
|
# Call show() if matplotlib is not inlined
|
|
if not MATPLOTLIB_INLINED:
|
|
plt.show()
|
|
|
|
# Clean
|
|
ax.remove()
|
|
|
|
# Return the drawn lines
|
|
return lines
|
|
|
|
def make_graph(self, ASres=None, padding=0):
|
|
self.graphASres = ASres
|
|
self.graphpadding = padding
|
|
ips = {}
|
|
rt = {}
|
|
ports = {}
|
|
ports_done = {}
|
|
for s, r in self.res:
|
|
r = r.getlayer(IP) or (conf.ipv6_enabled and r[scapy.layers.inet6.IPv6]) or r # noqa: E501
|
|
s = s.getlayer(IP) or (conf.ipv6_enabled and s[scapy.layers.inet6.IPv6]) or s # noqa: E501
|
|
ips[r.src] = None
|
|
if TCP in s:
|
|
trace_id = (s.src, s.dst, 6, s.dport)
|
|
elif UDP in s:
|
|
trace_id = (s.src, s.dst, 17, s.dport)
|
|
elif ICMP in s:
|
|
trace_id = (s.src, s.dst, 1, s.type)
|
|
else:
|
|
trace_id = (s.src, s.dst, s.proto, 0)
|
|
trace = rt.get(trace_id, {})
|
|
ttl = conf.ipv6_enabled and scapy.layers.inet6.IPv6 in s and s.hlim or s.ttl # noqa: E501
|
|
if not (ICMP in r and r[ICMP].type == 11) and not (conf.ipv6_enabled and scapy.layers.inet6.IPv6 in r and scapy.layers.inet6.ICMPv6TimeExceeded in r): # noqa: E501
|
|
if trace_id in ports_done:
|
|
continue
|
|
ports_done[trace_id] = None
|
|
p = ports.get(r.src, [])
|
|
if TCP in r:
|
|
p.append(r.sprintf("<T%ir,TCP.sport%> %TCP.sport% %TCP.flags%")) # noqa: E501
|
|
trace[ttl] = r.sprintf('"%r,src%":T%ir,TCP.sport%')
|
|
elif UDP in r:
|
|
p.append(r.sprintf("<U%ir,UDP.sport%> %UDP.sport%"))
|
|
trace[ttl] = r.sprintf('"%r,src%":U%ir,UDP.sport%')
|
|
elif ICMP in r:
|
|
p.append(r.sprintf("<I%ir,ICMP.type%> ICMP %ICMP.type%"))
|
|
trace[ttl] = r.sprintf('"%r,src%":I%ir,ICMP.type%')
|
|
else:
|
|
p.append(r.sprintf("{IP:<P%ir,proto%> IP %proto%}{IPv6:<P%ir,nh%> IPv6 %nh%}")) # noqa: E501
|
|
trace[ttl] = r.sprintf('"%r,src%":{IP:P%ir,proto%}{IPv6:P%ir,nh%}') # noqa: E501
|
|
ports[r.src] = p
|
|
else:
|
|
trace[ttl] = r.sprintf('"%r,src%"')
|
|
rt[trace_id] = trace
|
|
|
|
# Fill holes with unk%i nodes
|
|
unknown_label = incremental_label("unk%i")
|
|
blackholes = []
|
|
bhip = {}
|
|
for rtk in rt:
|
|
trace = rt[rtk]
|
|
max_trace = max(trace)
|
|
for n in range(min(trace), max_trace):
|
|
if n not in trace:
|
|
trace[n] = next(unknown_label)
|
|
if rtk not in ports_done:
|
|
if rtk[2] == 1: # ICMP
|
|
bh = "%s %i/icmp" % (rtk[1], rtk[3])
|
|
elif rtk[2] == 6: # TCP
|
|
bh = "%s %i/tcp" % (rtk[1], rtk[3])
|
|
elif rtk[2] == 17: # UDP
|
|
bh = '%s %i/udp' % (rtk[1], rtk[3])
|
|
else:
|
|
bh = '%s %i/proto' % (rtk[1], rtk[2])
|
|
ips[bh] = None
|
|
bhip[rtk[1]] = bh
|
|
bh = '"%s"' % bh
|
|
trace[max_trace + 1] = bh
|
|
blackholes.append(bh)
|
|
|
|
# Find AS numbers
|
|
ASN_query_list = set(x.rsplit(" ", 1)[0] for x in ips)
|
|
if ASres is None:
|
|
ASNlist = []
|
|
else:
|
|
ASNlist = ASres.resolve(*ASN_query_list)
|
|
|
|
ASNs = {}
|
|
ASDs = {}
|
|
for ip, asn, desc, in ASNlist:
|
|
if asn is None:
|
|
continue
|
|
iplist = ASNs.get(asn, [])
|
|
if ip in bhip:
|
|
if ip in ports:
|
|
iplist.append(ip)
|
|
iplist.append(bhip[ip])
|
|
else:
|
|
iplist.append(ip)
|
|
ASNs[asn] = iplist
|
|
ASDs[asn] = desc
|
|
|
|
backcolorlist = colgen("60", "86", "ba", "ff")
|
|
forecolorlist = colgen("a0", "70", "40", "20")
|
|
|
|
s = "digraph trace {\n"
|
|
|
|
s += "\n\tnode [shape=ellipse,color=black,style=solid];\n\n"
|
|
|
|
s += "\n#ASN clustering\n"
|
|
for asn in ASNs:
|
|
s += '\tsubgraph cluster_%s {\n' % asn
|
|
col = next(backcolorlist)
|
|
s += '\t\tcolor="#%s%s%s";' % col
|
|
s += '\t\tnode [fillcolor="#%s%s%s",style=filled];' % col
|
|
s += '\t\tfontsize = 10;'
|
|
s += '\t\tlabel = "%s\\n[%s]"\n' % (asn, ASDs[asn])
|
|
for ip in ASNs[asn]:
|
|
|
|
s += '\t\t"%s";\n' % ip
|
|
s += "\t}\n"
|
|
|
|
s += "#endpoints\n"
|
|
for p in ports:
|
|
s += '\t"%s" [shape=record,color=black,fillcolor=green,style=filled,label="%s|%s"];\n' % (p, p, "|".join(ports[p])) # noqa: E501
|
|
|
|
s += "\n#Blackholes\n"
|
|
for bh in blackholes:
|
|
s += '\t%s [shape=octagon,color=black,fillcolor=red,style=filled];\n' % bh # noqa: E501
|
|
|
|
if padding:
|
|
s += "\n#Padding\n"
|
|
pad = {}
|
|
for snd, rcv in self.res:
|
|
if rcv.src not in ports and rcv.haslayer(conf.padding_layer):
|
|
p = rcv.getlayer(conf.padding_layer).load
|
|
if p != b"\x00" * len(p):
|
|
pad[rcv.src] = None
|
|
for rcv in pad:
|
|
s += '\t"%s" [shape=triangle,color=black,fillcolor=red,style=filled];\n' % rcv # noqa: E501
|
|
|
|
s += "\n\tnode [shape=ellipse,color=black,style=solid];\n\n"
|
|
|
|
for rtk in rt:
|
|
s += "#---[%s\n" % repr(rtk)
|
|
s += '\t\tedge [color="#%s%s%s"];\n' % next(forecolorlist)
|
|
trace = rt[rtk]
|
|
maxtrace = max(trace)
|
|
for n in range(min(trace), maxtrace):
|
|
s += '\t%s ->\n' % trace[n]
|
|
s += '\t%s;\n' % trace[maxtrace]
|
|
|
|
s += "}\n"
|
|
self.graphdef = s
|
|
|
|
def graph(self, ASres=conf.AS_resolver, padding=0, **kargs):
|
|
"""x.graph(ASres=conf.AS_resolver, other args):
|
|
ASres=None : no AS resolver => no clustering
|
|
ASres=AS_resolver() : default whois AS resolver (riswhois.ripe.net)
|
|
ASres=AS_resolver_cymru(): use whois.cymru.com whois database
|
|
ASres=AS_resolver(server="whois.ra.net")
|
|
type: output type (svg, ps, gif, jpg, etc.), passed to dot's "-T" option # noqa: E501
|
|
target: filename or redirect. Defaults pipe to Imagemagick's display program # noqa: E501
|
|
prog: which graphviz program to use"""
|
|
if (self.graphdef is None or
|
|
self.graphASres != ASres or
|
|
self.graphpadding != padding):
|
|
self.make_graph(ASres, padding)
|
|
|
|
return do_graph(self.graphdef, **kargs)
|
|
|
|
|
|
@conf.commands.register
|
|
def traceroute(target, dport=80, minttl=1, maxttl=30, sport=RandShort(), l4=None, filter=None, timeout=2, verbose=None, **kargs): # noqa: E501
|
|
"""Instant TCP traceroute
|
|
|
|
:param target: hostnames or IP addresses
|
|
:param dport: TCP destination port (default is 80)
|
|
:param minttl: minimum TTL (default is 1)
|
|
:param maxttl: maximum TTL (default is 30)
|
|
:param sport: TCP source port (default is random)
|
|
:param l4: use a Scapy packet instead of TCP
|
|
:param filter: BPF filter applied to received packets
|
|
:param timeout: time to wait for answers (default is 2s)
|
|
:param verbose: detailed output
|
|
:return: an TracerouteResult, and a list of unanswered packets"""
|
|
if verbose is None:
|
|
verbose = conf.verb
|
|
if filter is None:
|
|
# we only consider ICMP error packets and TCP packets with at
|
|
# least the ACK flag set *and* either the SYN or the RST flag
|
|
# set
|
|
filter = "(icmp and (icmp[0]=3 or icmp[0]=4 or icmp[0]=5 or icmp[0]=11 or icmp[0]=12)) or (tcp and (tcp[13] & 0x16 > 0x10))" # noqa: E501
|
|
if l4 is None:
|
|
a, b = sr(IP(dst=target, id=RandShort(), ttl=(minttl, maxttl)) / TCP(seq=RandInt(), sport=sport, dport=dport), # noqa: E501
|
|
timeout=timeout, filter=filter, verbose=verbose, **kargs)
|
|
else:
|
|
# this should always work
|
|
filter = "ip"
|
|
a, b = sr(IP(dst=target, id=RandShort(), ttl=(minttl, maxttl)) / l4,
|
|
timeout=timeout, filter=filter, verbose=verbose, **kargs)
|
|
|
|
a = TracerouteResult(a.res)
|
|
if verbose:
|
|
a.show()
|
|
return a, b
|
|
|
|
|
|
@conf.commands.register
|
|
def traceroute_map(ips, **kargs):
|
|
"""Util function to call traceroute on multiple targets, then
|
|
show the different paths on a map.
|
|
|
|
:param ips: a list of IPs on which traceroute will be called
|
|
:param kargs: (optional) kwargs, passed to traceroute
|
|
"""
|
|
kargs.setdefault("verbose", 0)
|
|
return traceroute(ips)[0].world_trace()
|
|
|
|
#############################
|
|
# Simple TCP client stack #
|
|
#############################
|
|
|
|
|
|
class TCP_client(Automaton):
|
|
"""
|
|
Creates a TCP Client Automaton.
|
|
This automaton will handle TCP 3-way handshake.
|
|
|
|
Usage: the easiest usage is to use it as a SuperSocket.
|
|
>>> a = TCP_client.tcplink(HTTP, "www.google.com", 80)
|
|
>>> a.send(HTTPRequest())
|
|
>>> a.recv()
|
|
"""
|
|
def parse_args(self, ip, port, *args, **kargs):
|
|
from scapy.sessions import TCPSession
|
|
self.dst = str(Net(ip))
|
|
self.dport = port
|
|
self.sport = random.randrange(0, 2**16)
|
|
self.l4 = IP(dst=ip) / TCP(sport=self.sport, dport=self.dport, flags=0,
|
|
seq=random.randrange(0, 2**32))
|
|
self.src = self.l4.src
|
|
self.sack = self.l4[TCP].ack
|
|
self.rel_seq = None
|
|
self.rcvbuf = TCPSession(self._transmit_packet, False)
|
|
bpf = "host %s and host %s and port %i and port %i" % (self.src,
|
|
self.dst,
|
|
self.sport,
|
|
self.dport)
|
|
Automaton.parse_args(self, filter=bpf, **kargs)
|
|
|
|
def _transmit_packet(self, pkt):
|
|
"""Transmits a packet from TCPSession to the SuperSocket"""
|
|
self.oi.tcp.send(raw(pkt[TCP].payload))
|
|
|
|
def master_filter(self, pkt):
|
|
return (IP in pkt and
|
|
pkt[IP].src == self.dst and
|
|
pkt[IP].dst == self.src and
|
|
TCP in pkt and
|
|
pkt[TCP].sport == self.dport and
|
|
pkt[TCP].dport == self.sport and
|
|
self.l4[TCP].seq >= pkt[TCP].ack and # XXX: seq/ack 2^32 wrap up # noqa: E501
|
|
((self.l4[TCP].ack == 0) or (self.sack <= pkt[TCP].seq <= self.l4[TCP].ack + pkt[TCP].window))) # noqa: E501
|
|
|
|
@ATMT.state(initial=1)
|
|
def START(self):
|
|
pass
|
|
|
|
@ATMT.state()
|
|
def SYN_SENT(self):
|
|
pass
|
|
|
|
@ATMT.state()
|
|
def ESTABLISHED(self):
|
|
pass
|
|
|
|
@ATMT.state()
|
|
def LAST_ACK(self):
|
|
pass
|
|
|
|
@ATMT.state(final=1)
|
|
def CLOSED(self):
|
|
pass
|
|
|
|
@ATMT.condition(START)
|
|
def connect(self):
|
|
raise self.SYN_SENT()
|
|
|
|
@ATMT.action(connect)
|
|
def send_syn(self):
|
|
self.l4[TCP].flags = "S"
|
|
self.send(self.l4)
|
|
self.l4[TCP].seq += 1
|
|
|
|
@ATMT.receive_condition(SYN_SENT)
|
|
def synack_received(self, pkt):
|
|
if pkt[TCP].flags.SA:
|
|
raise self.ESTABLISHED().action_parameters(pkt)
|
|
|
|
@ATMT.action(synack_received)
|
|
def send_ack_of_synack(self, pkt):
|
|
self.l4[TCP].ack = pkt[TCP].seq + 1
|
|
self.l4[TCP].flags = "A"
|
|
self.send(self.l4)
|
|
|
|
@ATMT.receive_condition(ESTABLISHED)
|
|
def incoming_data_received(self, pkt):
|
|
if not isinstance(pkt[TCP].payload, (NoPayload, conf.padding_layer)):
|
|
raise self.ESTABLISHED().action_parameters(pkt)
|
|
|
|
@ATMT.action(incoming_data_received)
|
|
def receive_data(self, pkt):
|
|
data = raw(pkt[TCP].payload)
|
|
if data and self.l4[TCP].ack == pkt[TCP].seq:
|
|
self.sack = self.l4[TCP].ack
|
|
self.l4[TCP].ack += len(data)
|
|
self.l4[TCP].flags = "A"
|
|
# Answer with an Ack
|
|
self.send(self.l4)
|
|
# Process data - will be sent to the SuperSocket through this
|
|
self.rcvbuf.on_packet_received(pkt)
|
|
|
|
@ATMT.ioevent(ESTABLISHED, name="tcp", as_supersocket="tcplink")
|
|
def outgoing_data_received(self, fd):
|
|
raise self.ESTABLISHED().action_parameters(fd.recv())
|
|
|
|
@ATMT.action(outgoing_data_received)
|
|
def send_data(self, d):
|
|
self.l4[TCP].flags = "PA"
|
|
self.send(self.l4 / d)
|
|
self.l4[TCP].seq += len(d)
|
|
|
|
@ATMT.receive_condition(ESTABLISHED)
|
|
def reset_received(self, pkt):
|
|
if pkt[TCP].flags.R:
|
|
raise self.CLOSED()
|
|
|
|
@ATMT.receive_condition(ESTABLISHED)
|
|
def fin_received(self, pkt):
|
|
if pkt[TCP].flags.F:
|
|
raise self.LAST_ACK().action_parameters(pkt)
|
|
|
|
@ATMT.action(fin_received)
|
|
def send_finack(self, pkt):
|
|
self.l4[TCP].flags = "FA"
|
|
self.l4[TCP].ack = pkt[TCP].seq + 1
|
|
self.send(self.l4)
|
|
self.l4[TCP].seq += 1
|
|
|
|
@ATMT.receive_condition(LAST_ACK)
|
|
def ack_of_fin_received(self, pkt):
|
|
if pkt[TCP].flags.A:
|
|
raise self.CLOSED()
|
|
|
|
|
|
#####################
|
|
# Reporting stuff #
|
|
#####################
|
|
|
|
|
|
@conf.commands.register
|
|
def report_ports(target, ports):
|
|
"""portscan a target and output a LaTeX table
|
|
report_ports(target, ports) -> string"""
|
|
ans, unans = sr(IP(dst=target) / TCP(dport=ports), timeout=5)
|
|
rep = "\\begin{tabular}{|r|l|l|}\n\\hline\n"
|
|
for s, r in ans:
|
|
if not r.haslayer(ICMP):
|
|
if r.payload.flags == 0x12:
|
|
rep += r.sprintf("%TCP.sport% & open & SA \\\\\n")
|
|
rep += "\\hline\n"
|
|
for s, r in ans:
|
|
if r.haslayer(ICMP):
|
|
rep += r.sprintf("%TCPerror.dport% & closed & ICMP type %ICMP.type%/%ICMP.code% from %IP.src% \\\\\n") # noqa: E501
|
|
elif r.payload.flags != 0x12:
|
|
rep += r.sprintf("%TCP.sport% & closed & TCP %TCP.flags% \\\\\n")
|
|
rep += "\\hline\n"
|
|
for i in unans:
|
|
rep += i.sprintf("%TCP.dport% & ? & unanswered \\\\\n")
|
|
rep += "\\hline\n\\end{tabular}\n"
|
|
return rep
|
|
|
|
|
|
@conf.commands.register
|
|
def IPID_count(lst, funcID=lambda x: x[1].id, funcpres=lambda x: x[1].summary()): # noqa: E501
|
|
"""Identify IP id values classes in a list of packets
|
|
|
|
lst: a list of packets
|
|
funcID: a function that returns IP id values
|
|
funcpres: a function used to summarize packets"""
|
|
idlst = [funcID(e) for e in lst]
|
|
idlst.sort()
|
|
classes = [idlst[0]]
|
|
classes += [t[1] for t in zip(idlst[:-1], idlst[1:]) if abs(t[0] - t[1]) > 50] # noqa: E501
|
|
lst = [(funcID(x), funcpres(x)) for x in lst]
|
|
lst.sort()
|
|
print("Probably %i classes:" % len(classes), classes)
|
|
for id, pr in lst:
|
|
print("%5i" % id, pr)
|
|
|
|
|
|
@conf.commands.register
|
|
def fragleak(target, sport=123, dport=123, timeout=0.2, onlyasc=0, count=None):
|
|
load = "XXXXYYYYYYYYYY"
|
|
pkt = IP(dst=target, id=RandShort(), options=b"\x00" * 40, flags=1)
|
|
pkt /= UDP(sport=sport, dport=sport) / load
|
|
s = conf.L3socket()
|
|
intr = 0
|
|
found = {}
|
|
try:
|
|
while count is None or count:
|
|
if count is not None and isinstance(count, int):
|
|
count -= 1
|
|
try:
|
|
if not intr:
|
|
s.send(pkt)
|
|
sin = select.select([s], [], [], timeout)[0]
|
|
if not sin:
|
|
continue
|
|
ans = s.recv(1600)
|
|
if not isinstance(ans, IP): # TODO: IPv6
|
|
continue
|
|
if not isinstance(ans.payload, ICMP):
|
|
continue
|
|
if not isinstance(ans.payload.payload, IPerror):
|
|
continue
|
|
if ans.payload.payload.dst != target:
|
|
continue
|
|
if ans.src != target:
|
|
print("leak from", ans.src)
|
|
if not ans.haslayer(conf.padding_layer):
|
|
continue
|
|
leak = ans.getlayer(conf.padding_layer).load
|
|
if leak not in found:
|
|
found[leak] = None
|
|
linehexdump(leak, onlyasc=onlyasc)
|
|
except KeyboardInterrupt:
|
|
if intr:
|
|
raise
|
|
intr = 1
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
|
|
@conf.commands.register
|
|
def fragleak2(target, timeout=0.4, onlyasc=0, count=None):
|
|
found = {}
|
|
try:
|
|
while count is None or count:
|
|
if count is not None and isinstance(count, int):
|
|
count -= 1
|
|
|
|
pkt = IP(dst=target, options=b"\x00" * 40, proto=200)
|
|
pkt /= "XXXXYYYYYYYYYYYY"
|
|
p = sr1(pkt, timeout=timeout, verbose=0)
|
|
if not p:
|
|
continue
|
|
if conf.padding_layer in p:
|
|
leak = p[conf.padding_layer].load
|
|
if leak not in found:
|
|
found[leak] = None
|
|
linehexdump(leak, onlyasc=onlyasc)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
conf.stats_classic_protocols += [TCP, UDP, ICMP]
|
|
conf.stats_dot11_protocols += [TCP, UDP, ICMP]
|
|
|
|
if conf.ipv6_enabled:
|
|
import scapy.layers.inet6
|