# This file is part of Scapy # See http://www.secdev.org/projects/scapy for more information # Copyright (C) Philippe Biondi # 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 = "
%s
" % self.listname # noqa: E501 vpython.scene.append_to_caption( re.sub( r'\%(.*)\%', r'\1', re.sub( r'\`(.*)\`', r'\1', """Commands: %Click% to toggle information about a node. %Double click% to perform a quick web scan on this node. Camera usage: `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(" %TCP.sport% %TCP.flags%")) # noqa: E501 trace[ttl] = r.sprintf('"%r,src%":T%ir,TCP.sport%') elif UDP in r: p.append(r.sprintf(" %UDP.sport%")) trace[ttl] = r.sprintf('"%r,src%":U%ir,UDP.sport%') elif ICMP in r: p.append(r.sprintf(" ICMP %ICMP.type%")) trace[ttl] = r.sprintf('"%r,src%":I%ir,ICMP.type%') else: p.append(r.sprintf("{IP: IP %proto%}{IPv6: 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