• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Philippe Biondi <phil@secdev.org>
5
6"""
7Clone of p0f v3 passive OS fingerprinting
8"""
9
10import re
11import struct
12import random
13
14from scapy.data import KnowledgeBase, select_path
15from scapy.config import conf
16from scapy.compat import raw, orb
17from scapy.packet import NoPayload
18from scapy.layers.inet import IP, TCP, TCPOptions
19from scapy.layers.http import HTTP, HTTPRequest, HTTPResponse
20from scapy.layers.inet6 import IPv6
21from scapy.volatile import RandByte, RandShort, RandString
22from scapy.error import warning
23
24_p0fpaths = ["/etc/p0f", "/usr/share/p0f", "/opt/local"]
25conf.p0f_base = select_path(_p0fpaths, "p0f.fp")
26
27MIN_TCP4 = 40  # Min size of IPv4/TCP headers
28MIN_TCP6 = 60  # Min size of IPv6/TCP headers
29MAX_DIST = 35  # Maximum TTL distance for non-fuzzy signature matching
30
31WIN_TYPE_NORMAL = 0  # Literal value
32WIN_TYPE_ANY = 1  # Wildcard
33WIN_TYPE_MOD = 2  # Modulo check
34WIN_TYPE_MSS = 3  # Window size MSS multiplier
35WIN_TYPE_MTU = 4  # Window size MTU multiplier
36
37# Convert TCP option num to p0f (nop is handled separately)
38tcp_options_p0f = {
39    2: "mss",  # maximum segment size
40    3: "ws",  # window scaling
41    4: "sok",  # selective ACK permitted
42    5: "sack",  # selective ACK (should not be seen)
43    8: "ts",  # timestamp
44}
45
46
47# Signatures
48class TCP_Signature(object):
49    __slots__ = ["olayout", "quirks", "ip_opt_len", "ip_ver", "ttl",
50                 "mss", "win", "win_type", "wscale", "pay_class", "ts1"]
51
52    def __init__(self, olayout, quirks, ip_opt_len, ip_ver, ttl,
53                 mss, win, win_type, wscale, pay_class, ts1):
54        self.olayout = olayout
55        self.quirks = quirks
56        self.ip_opt_len = ip_opt_len
57        self.ip_ver = ip_ver
58        self.ttl = ttl
59        self.mss = mss
60        self.win = win
61        self.win_type = win_type  # None for packet signatures
62        self.wscale = wscale
63        self.pay_class = pay_class
64        self.ts1 = ts1  # None for base signatures
65
66    @classmethod
67    def from_packet(cls, pkt):
68        """
69        Receives a TCP packet (assuming it's valid), and returns
70        a TCP_Signature object
71        """
72        ip_ver = pkt.version
73        quirks = set()
74
75        def addq(name):
76            quirks.add(name)
77
78        # IPv4/IPv6 parsing
79        if ip_ver == 4:
80            ttl = pkt.ttl
81            ip_opt_len = (pkt.ihl * 4) - 20
82            if pkt.tos & (0x01 | 0x02):
83                addq("ecn")
84            if pkt.flags.evil:
85                addq("0+")
86            if pkt.flags.DF:
87                addq("df")
88                if pkt.id:
89                    addq("id+")
90            elif pkt.id == 0:
91                addq("id-")
92        else:
93            ttl = pkt.hlim
94            ip_opt_len = 0
95            if pkt.fl:
96                addq("flow")
97            if pkt.tc & (0x01 | 0x02):
98                addq("ecn")
99
100        # TCP parsing
101        tcp = pkt[TCP]
102        win = tcp.window
103        if tcp.flags & (0x40 | 0x80 | 0x01):
104            addq("ecn")
105        if tcp.seq == 0:
106            addq("seq-")
107        if tcp.flags.A:
108            if tcp.ack == 0:
109                addq("ack-")
110        elif tcp.ack:
111            addq("ack+")
112        if tcp.flags.U:
113            addq("urgf+")
114        elif tcp.urgptr:
115            addq("uptr+")
116        if tcp.flags.P:
117            addq("pushf+")
118
119        pay_class = 1 if tcp.payload else 0
120
121        # Manual TCP options parsing
122        mss = 0
123        wscale = 0
124        ts1 = 0
125        olayout = ""
126        optlen = (tcp.dataofs << 2) - 20
127        x = raw(tcp)[-optlen:]  # raw bytes of TCP options
128        while x:
129            onum = orb(x[0])
130            if onum == 0:
131                x = x[1:]
132                olayout += "eol+%i," % len(x)
133                if x.strip(b"\x00"):  # non-zero past EOL
134                    addq("opt+")
135                break
136            if onum == 1:
137                x = x[1:]
138                olayout += "nop,"
139                continue
140            try:
141                olen = orb(x[1])
142            except IndexError:  # no room for length field
143                addq("bad")
144                break
145            oval = x[2:olen]
146            if onum in tcp_options_p0f:
147                ofmt = TCPOptions[0][onum][1]
148                olayout += "%s," % tcp_options_p0f[onum]
149                optsize = 2 + struct.calcsize(ofmt) if ofmt else 2  # total len
150                if len(x) < optsize:  # option would end past end of header
151                    addq("bad")
152                    break
153
154                if onum == 5:
155                    if olen < 10 or olen > 34:  # SACK length out of range
156                        addq("bad")
157                        break
158                else:
159                    if olen != optsize:  # length field doesn't fit option type
160                        addq("bad")
161                        break
162                    if ofmt:
163                        oval = struct.unpack(ofmt, oval)
164                        if len(oval) == 1:
165                            oval = oval[0]
166                    if onum == 2:
167                        mss = oval
168                    elif onum == 3:
169                        wscale = oval
170                        if wscale > 14:
171                            addq("exws")
172                    elif onum == 8:
173                        ts1 = oval[0]
174                        if not ts1:
175                            addq("ts1-")
176                        if oval[1] and (tcp.flags.S and not tcp.flags.A):
177                            addq("ts2+")
178            else:  # Unknown option, presumably with specified size
179                if olen < 2 or olen > 40 or olen > len(x):
180                    addq("bad")
181                    break
182            x = x[olen:]
183        olayout = olayout[:-1]
184
185        return cls(olayout, quirks, ip_opt_len, ip_ver, ttl, mss, win, None, wscale, pay_class, ts1)  # noqa: E501
186
187    @classmethod
188    def from_raw_sig(cls, sig_line):
189        """
190        Parses a TCP sig line and returns a tuple consisting of a
191        TCP_Signature object and bad_ttl as bool
192        """
193        ver, ttl, olen, mss, wsize, olayout, quirks, pclass = lparse(sig_line, 8)  # noqa: E501
194        wsize, _, scale = wsize.partition(",")
195
196        ip_ver = -1 if ver == "*" else int(ver)
197        ttl, bad_ttl = (int(ttl[:-1]), True) if ttl[-1] == "-" else (int(ttl), False)  # noqa: E501
198        ip_opt_len = int(olen)
199        mss = -1 if mss == "*" else int(mss)
200        if wsize == "*":
201            win, win_type = (0, WIN_TYPE_ANY)
202        elif wsize[:3] == "mss":
203            win, win_type = (int(wsize[4:]), WIN_TYPE_MSS)
204        elif wsize[0] == "%":
205            win, win_type = (int(wsize[1:]), WIN_TYPE_MOD)
206        elif wsize[:3] == "mtu":
207            win, win_type = (int(wsize[4:]), WIN_TYPE_MTU)
208        else:
209            win, win_type = (int(wsize), WIN_TYPE_NORMAL)
210        wscale = -1 if scale == "*" else int(scale)
211        if quirks:
212            quirks = frozenset(q for q in quirks.split(","))
213        else:
214            quirks = frozenset()
215        pay_class = -1 if pclass == "*" else int(pclass == "+")
216
217        sig = cls(olayout, quirks, ip_opt_len, ip_ver, ttl, mss, win, win_type, wscale, pay_class, None)  # noqa: E501
218        return sig, bad_ttl
219
220    def __str__(self):
221        quirks = ",".join(q for q in self.quirks)
222        fmt = "%i:%i+%i:%i:%i:%i,%i:%s:%s:%i"
223        s = fmt % (self.ip_ver, self.ttl, guess_dist(self.ttl),
224                   self.ip_opt_len, self.mss, self.win, self.wscale,
225                   self.olayout, quirks, self.pay_class)
226        return s
227
228
229class HTTP_Signature(object):
230    __slots__ = ["http_ver", "hdr", "hdr_set", "habsent", "sw"]
231
232    def __init__(self, http_ver, hdr, hdr_set, habsent, sw):
233        self.http_ver = http_ver
234        self.hdr = hdr
235        self.hdr_set = hdr_set
236        self.habsent = habsent  # None for packet signatures
237        self.sw = sw
238
239    @classmethod
240    def from_packet(cls, pkt):
241        """
242        Receives an HTTP packet (assuming it's valid), and returns
243        a HTTP_Signature object
244        """
245        http_payload = raw(pkt[TCP].payload)
246
247        crlfcrlf = b"\r\n\r\n"
248        crlfcrlfIndex = http_payload.find(crlfcrlf)
249        if crlfcrlfIndex != -1:
250            headers = http_payload[:crlfcrlfIndex + len(crlfcrlf)]
251        else:
252            headers = http_payload
253        headers = headers.decode()  # XXX: Check if this could fail
254        first_line, headers = headers.split("\r\n", 1)
255
256        if "1.0" in first_line:
257            http_ver = 0
258        elif "1.1" in first_line:
259            http_ver = 1
260        else:
261            raise ValueError("HTTP version is not 1.0/1.1")
262
263        sw = ""
264        headers_found = []
265        hdr_set = set()
266        for header_line in headers.split("\r\n"):
267            name, _, value = header_line.partition(":")
268            if value:
269                value = value.strip()
270                headers_found.append((name, value))
271                hdr_set.add(name)
272                if name in ("User-Agent", "Server"):
273                    sw = value
274        hdr = tuple(headers_found)
275        return cls(http_ver, hdr, hdr_set, None, sw)
276
277    @classmethod
278    def from_raw_sig(cls, sig_line):
279        """
280        Parses an HTTP sig line and returns a HTTP_Signature object
281        """
282        ver, horder, habsent, expsw = lparse(sig_line, 4)
283        http_ver = -1 if ver == "*" else int(ver)
284
285        # horder parsing - split by commas that aren't in []
286        new_horder = []
287        for header in re.split(r",(?![^\[]*\])", horder):
288            name, _, value = header.partition("=")
289            if name[0] == "?":  # Optional header
290                new_horder.append((name[1:], value[1:-1], True))
291            else:
292                new_horder.append((name, value[1:-1], False))
293        hdr = tuple(new_horder)
294        hdr_set = frozenset(header[0] for header in hdr if not header[2])
295        habsent = frozenset(habsent.split(","))
296        return cls(http_ver, hdr, hdr_set, habsent, expsw)
297
298    def __str__(self):
299        # values that depend on the context are not included in the string
300        skipval = ("Host", "User-Agent", "Date", "Content-Type", "Server")
301        hdr = ",".join(n if n in skipval else "%s=[%s]" % (n, v) for n, v in self.hdr)  # noqa: E501
302        fmt = "%i:%s::%s"
303        s = fmt % (self.http_ver, hdr, self.sw)
304        return s
305
306
307# Records
308class MTU_Record(object):
309    __slots__ = ["label_id", "mtu"]
310
311    def __init__(self, label_id, sig_line):
312        self.label_id = label_id
313        self.mtu = int(sig_line)
314
315
316class TCP_Record(object):
317    __slots__ = ["label_id", "bad_ttl", "sig"]
318
319    def __init__(self, label_id, sig_line):
320        self.label_id = label_id
321        sig, bad_ttl = TCP_Signature.from_raw_sig(sig_line)
322        self.bad_ttl = bad_ttl
323        self.sig = sig
324
325
326class HTTP_Record(object):
327    __slots__ = ["label_id", "sig"]
328
329    def __init__(self, label_id, sig_line):
330        self.label_id = label_id
331        self.sig = HTTP_Signature.from_raw_sig(sig_line)
332
333
334class p0fKnowledgeBase(KnowledgeBase):
335    """
336    self.base = {
337        "mtu" (str): [sig(tuple), ...]
338        "tcp"/"http" (str): {
339            direction (str): [sig(tuple), ...]
340            }
341    }
342    self.labels = (label(tuple), ...)
343    """
344    def lazy_init(self):
345        try:
346            f = open(self.filename)
347        except Exception:
348            warning("Can't open base %s", self.filename)
349            return
350
351        self.base = {}
352        self.labels = []
353        self._parse_file(f)
354        self.labels = tuple(self.labels)
355        f.close()
356
357    def _parse_file(self, file):
358        """
359        Parses p0f.fp file and stores the data with described structures.
360        """
361        label_id = -1
362
363        for line in file:
364            if line[0] in (";", "\n"):
365                continue
366            line = line.strip()
367
368            if line[0] == "[":
369                section, direction = lparse(line[1:-1], 2)
370                if section == "mtu":
371                    self.base[section] = []
372                    curr_records = self.base[section]
373                else:
374                    if section not in self.base:
375                        self.base[section] = {direction: []}
376                    elif direction not in self.base[section]:
377                        self.base[section][direction] = []
378                    curr_records = self.base[section][direction]
379            else:
380                param, _, val = line.partition(" = ")
381                param = param.strip()
382
383                if param == "sig":
384                    if section == "mtu":
385                        record_class = MTU_Record
386                    elif section == "tcp":
387                        record_class = TCP_Record
388                    elif section == "http":
389                        record_class = HTTP_Record
390                    curr_records.append(record_class(label_id, val))
391
392                elif param == "label":
393                    label_id += 1
394                    if section == "mtu":
395                        self.labels.append(val)
396                        continue
397                    # label = type:class:name:flavor
398                    t, c, name, flavor = lparse(val, 4)
399                    self.labels.append((t, c, name, flavor))
400
401                elif param == "sys":
402                    sys_names = tuple(name for name in val.split(","))
403                    self.labels[label_id] += (sys_names,)
404
405    def get_sigs_by_os(self, direction, osgenre, osdetails=None):
406        """Get TCP signatures that match an OS genre and details (if specified).
407        If osdetails isn't specified, then we pick all signatures
408        that match osgenre.
409
410        Examples:
411            >>> p0fdb.get_sigs_by_os("request", "Linux", "2.6")
412            >>> p0fdb.get_sigs_by_os("response", "Windows", "8")
413            >>> p0fdb.get_sigs_by_os("request", "FreeBSD")
414        """
415        sigs = []
416        for tcp_record in self.base["tcp"][direction]:
417            label = self.labels[tcp_record.label_id]
418            name, flavor = label[2], label[3]
419            if osgenre and osgenre == name:
420                if osdetails:
421                    if osdetails in flavor:
422                        sigs.append(tcp_record.sig)
423                else:
424                    sigs.append(tcp_record.sig)
425        return sigs
426
427    def tcp_find_match(self, ts, direction):
428        """
429        Finds the best match for the given signature and direction.
430        If a match is found, returns a tuple consisting of:
431        - label: the matched label
432        - dist: guessed distance from the packet source
433        - fuzzy: whether the match is fuzzy
434        Returns None if no match was found
435        """
436        win_multi, use_mtu = detect_win_multi(ts)
437
438        gmatch = None  # generic match
439        fmatch = None  # fuzzy match
440        for tcp_record in self.base["tcp"][direction]:
441            rs = tcp_record.sig
442
443            fuzzy = False
444            ref_quirks = rs.quirks
445
446            if rs.olayout != ts.olayout:
447                continue
448
449            if rs.ip_ver == -1:
450                ref_quirks -= {"flow"} if ts.ip_ver == 4 else {"df", "id+", "id-"}  # noqa: E501
451
452            if ref_quirks != ts.quirks:
453                deleted = (ref_quirks ^ ts.quirks) & ref_quirks
454                added = (ref_quirks ^ ts.quirks) & ts.quirks
455
456                if (fmatch or (deleted - {"df", "id+"}) or (added - {"id-", "ecn"})):  # noqa: E501
457                    continue
458                fuzzy = True
459
460            if rs.ip_opt_len != ts.ip_opt_len:
461                continue
462            if tcp_record.bad_ttl:
463                if rs.ttl < ts.ttl:
464                    continue
465            else:
466                if rs.ttl < ts.ttl or rs.ttl - ts.ttl > MAX_DIST:
467                    fuzzy = True
468
469            if ((rs.mss != -1 and rs.mss != ts.mss) or
470               (rs.wscale != -1 and rs.wscale != ts.wscale) or
471               (rs.pay_class != -1 and rs.pay_class != ts.pay_class)):
472                continue
473
474            if rs.win_type == WIN_TYPE_NORMAL:
475                if rs.win != ts.win:
476                    continue
477            elif rs.win_type == WIN_TYPE_MOD:
478                if ts.win % rs.win:
479                    continue
480            elif rs.win_type == WIN_TYPE_MSS:
481                if (use_mtu or rs.win != win_multi):
482                    continue
483            elif rs.win_type == WIN_TYPE_MTU:
484                if (not use_mtu or rs.win != win_multi):
485                    continue
486
487            # Got a match? If not fuzzy, return. If fuzzy, keep looking.
488            label = self.labels[tcp_record.label_id]
489            match = (label, rs.ttl - ts.ttl, fuzzy)
490            if not fuzzy:
491                if label[0] == "s":
492                    return match
493                elif not gmatch:
494                    gmatch = match
495            elif not fmatch:
496                fmatch = match
497
498        if gmatch:
499            return gmatch
500        if fmatch:
501            return fmatch
502        return None
503
504    def http_find_match(self, ts, direction):
505        """
506        Finds the best match for the given signature and direction.
507        If a match is found, returns a tuple consisting of:
508        - label: the matched label
509        - dishonest: whether the software was detected as dishonest
510        Returns None if no match was found
511        """
512        gmatch = None  # generic match
513        for http_record in self.base["http"][direction]:
514            rs = http_record.sig
515
516            if rs.http_ver != -1 and rs.http_ver != ts.http_ver:
517                continue
518
519            # Check that all non-optional headers appear in the packet
520            if not (ts.hdr_set & rs.hdr_set) == rs.hdr_set:
521                continue
522
523            # Check that no forbidden headers appear in the packet.
524            if len(rs.habsent & ts.hdr_set) > 0:
525                continue
526
527            def headers_correl():
528                phi = 0  # Packet HTTP header index
529                hdr_len = len(ts.hdr)
530
531                # Confirm the ordering and values of headers
532                # (this is relatively slow, hence the if statements above).
533                # The algorithm is derived from the original p0f/fp_http.c
534                for kh in rs.hdr:
535                    orig_phi = phi
536                    while (phi < hdr_len and
537                           kh[0] != ts.hdr[phi][0]):
538                        phi += 1
539
540                    if phi == hdr_len:
541                        if not kh[2]:
542                            return False
543
544                        for ph in ts.hdr:
545                            if kh[0] == ph[0]:
546                                return False
547
548                        phi = orig_phi
549                        continue
550
551                    if kh[1] not in ts.hdr[phi][1]:
552                        return False
553                    phi += 1
554                return True
555
556            if not headers_correl():
557                continue
558
559            # Got a match
560            label = self.labels[http_record.label_id]
561            dishonest = rs.sw and ts.sw and rs.sw not in ts.sw
562            match = (label, dishonest)
563            if label[0] == "s":
564                return match
565            elif not gmatch:
566                gmatch = match
567        return gmatch if gmatch else None
568
569    def mtu_find_match(self, mtu):
570        """
571        Finds a match for the given MTU.
572        If a match is found, returns the label string.
573        Returns None if no match was found
574        """
575        for mtu_record in self.base["mtu"]:
576            if mtu == mtu_record.mtu:
577                return self.labels[mtu_record.label_id]
578        return None
579
580
581p0fdb = p0fKnowledgeBase(conf.p0f_base)
582
583
584def guess_dist(ttl):
585    for ottl in (32, 64, 128, 255):
586        if ttl <= ottl:
587            return ottl - ttl
588
589
590def lparse(line, n, delimiter=":", default=""):
591    """
592    Parsing of 'a:b:c:d:e' lines
593    """
594    a = line.split(delimiter)[:n]
595    for elt in a:
596        yield elt
597    for _ in range(n - len(a)):
598        yield default
599
600
601def validate_packet(pkt):
602    """
603    Validate that the packet is an IPv4/IPv6 and TCP packet.
604    If the packet is valid, a copy is returned. If not, TypeError is raised.
605    """
606    pkt = pkt.copy()
607    valid = pkt.haslayer(TCP) and (pkt.haslayer(IP) or pkt.haslayer(IPv6))
608    if not valid:
609        raise TypeError("Not a TCP/IP packet")
610    return pkt
611
612
613def detect_win_multi(ts):
614    """
615    Figure out if window size is a multiplier of MSS or MTU.
616    Receives a TCP signature and returns the multiplier and
617    whether mtu should be used
618    """
619    mss = ts.mss
620    win = ts.win
621    if not win or mss < 100:
622        return -1, False
623
624    options = [
625        (mss, False),
626        (1500 - MIN_TCP4, False),
627        (1500 - MIN_TCP4 - 12, False),
628        (mss + MIN_TCP4, True),
629        (1500, True)
630    ]
631    if ts.ts1:
632        options.append((mss - 12, False))
633    if ts.ip_ver == 6:
634        options.append((1500 - MIN_TCP6, False))
635        options.append((1500 - MIN_TCP6 - 12, False))
636        options.append((mss + MIN_TCP6, True))
637
638    for div, use_mtu in options:
639        if not win % div:
640            return win / div, use_mtu
641    return -1, False
642
643
644def packet2p0f(pkt):
645    """
646    Returns a p0f signature of the packet, and the direction.
647    Raises TypeError if the packet isn't valid for p0f
648    """
649    pkt = validate_packet(pkt)
650    pkt = pkt.__class__(raw(pkt))
651
652    if pkt[TCP].flags.S:
653        if pkt[TCP].flags.A:
654            direction = "response"
655        else:
656            direction = "request"
657        sig = TCP_Signature.from_packet(pkt)
658
659    elif pkt[TCP].payload:
660        # XXX: guess_payload_class doesn't use any class related attributes
661        pclass = HTTP().guess_payload_class(raw(pkt[TCP].payload))
662        if pclass == HTTPRequest:
663            direction = "request"
664        elif pclass == HTTPResponse:
665            direction = "response"
666        else:
667            raise TypeError("Not an HTTP payload")
668        sig = HTTP_Signature.from_packet(pkt)
669    else:
670        raise TypeError("Not a SYN, SYN/ACK, or HTTP packet")
671    return sig, direction
672
673
674def fingerprint_mtu(pkt):
675    """
676    Fingerprints the MTU based on the maximum segment size specified
677    in TCP options.
678    If a match was found, returns the label. If not returns None
679    """
680    pkt = validate_packet(pkt)
681    mss = 0
682    for name, value in pkt.payload.options:
683        if name == "MSS":
684            mss = value
685
686    if not mss:
687        return None
688
689    mtu = (mss + MIN_TCP4) if pkt.version == 4 else (mss + MIN_TCP6)
690
691    if not p0fdb.get_base():
692        warning("p0f base empty.")
693        return None
694
695    return p0fdb.mtu_find_match(mtu)
696
697
698def p0f(pkt):
699    sig, direction = packet2p0f(pkt)
700    if not p0fdb.get_base():
701        warning("p0f base empty.")
702        return None
703
704    if isinstance(sig, TCP_Signature):
705        return p0fdb.tcp_find_match(sig, direction)
706    else:
707        return p0fdb.http_find_match(sig, direction)
708
709
710def prnp0f(pkt):
711    """Calls p0f and prints a user-friendly output"""
712    try:
713        r = p0f(pkt)
714    except Exception:
715        return
716
717    sig, direction = packet2p0f(pkt)
718    is_tcp_sig = isinstance(sig, TCP_Signature)
719    to_server = direction == "request"
720
721    if is_tcp_sig:
722        pkt_type = "SYN" if to_server else "SYN+ACK"
723    else:
724        pkt_type = "HTTP Request" if to_server else "HTTP Response"
725
726    res = pkt.sprintf(".-[ %IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport% (" + pkt_type + ") ]-\n|\n")  # noqa: E501
727    fields = []
728
729    def add_field(name, value):
730        fields.append("| %-8s = %s\n" % (name, value))
731
732    cli_or_svr = "Client" if to_server else "Server"
733    add_field(cli_or_svr, pkt.sprintf("%IP.src%:%TCP.sport%"))
734
735    if r:
736        label = r[0]
737        app_or_os = "App" if label[1] == "!" else "OS"
738        add_field(app_or_os, label[2] + " " + label[3])
739        if len(label) == 5:  # label includes sys
740            add_field("Sys", ", ".join(name for name in label[4]))
741        if is_tcp_sig:
742            add_field("Distance", r[1])
743    else:
744        app_or_os = "OS" if is_tcp_sig else "App"
745        add_field(app_or_os, "UNKNOWN")
746
747    add_field("Raw sig", str(sig))
748
749    res += "".join(fields)
750    res += "`____\n"
751    print(res)
752
753
754def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
755                    extrahops=0, mtu=1500, uptime=None):
756    """Modifies pkt so that p0f will think it has been sent by a
757    specific OS. Either osgenre or signature is required to impersonate.
758    If signature is specified (as a raw string), we use the signature.
759    signature format:
760        "ip_ver:ttl:ip_opt_len:mss:window,wscale:opt_layout:quirks:pay_class"
761
762    If osgenre is specified, we randomly pick a signature with a label
763    that matches osgenre (and osdetails, if specified).
764    Note: osgenre is case sensitive ("linux" -> "Linux" etc.), and osdetails
765    is a substring of a label flavor ("7", "8" and "7 or 8" will
766    all match the label "s:win:Windows:7 or 8")
767
768    For now, only TCP SYN/SYN+ACK packets are supported."""
769    pkt = validate_packet(pkt)
770
771    if not osgenre and not signature:
772        raise ValueError("osgenre or signature is required to impersonate!")
773
774    tcp = pkt[TCP]
775    tcp_type = tcp.flags & (0x02 | 0x10)  # SYN / SYN+ACK
776
777    if signature:
778        if isinstance(signature, str):
779            sig, _ = TCP_Signature.from_raw_sig(signature)
780        else:
781            raise TypeError("Unsupported signature type")
782    else:
783        if not p0fdb.get_base():
784            sigs = []
785        else:
786            direction = "request" if tcp_type == 0x02 else "response"
787            sigs = p0fdb.get_sigs_by_os(direction, osgenre, osdetails)
788
789        # If IPv6 packet, remove IPv4-only signatures and vice versa
790        sigs = [s for s in sigs if s.ip_ver == -1 or s.ip_ver == pkt.version]
791        if not sigs:
792            raise ValueError("No match in the p0f database")
793        sig = random.choice(sigs)
794
795    if sig.ip_ver != -1 and pkt.version != sig.ip_ver:
796        raise ValueError("Can't convert between IPv4 and IPv6")
797
798    quirks = sig.quirks
799
800    if pkt.version == 4:
801        pkt.ttl = sig.ttl - extrahops
802        if sig.ip_opt_len != 0:
803            # FIXME: Non-zero IPv4 options not handled
804            warning("Unhandled IPv4 option field")
805        else:
806            pkt.options = []
807
808        if "df" in quirks:
809            pkt.flags |= 0x02  # set DF flag
810            if "id+" in quirks:
811                if pkt.id == 0:
812                    pkt.id = random.randint(1, 2**16 - 1)
813            else:
814                pkt.id = 0
815        else:
816            pkt.flags &= ~(0x02)  # DF flag not set
817            if "id-" in quirks:
818                pkt.id = 0
819            elif pkt.id == 0:
820                pkt.id = random.randint(1, 2**16 - 1)
821        if "ecn" in quirks:
822            pkt.tos |= random.randint(0x01, 0x03)
823        pkt.flags = pkt.flags | 0x04 if "0+" in quirks else pkt.flags & ~(0x04)
824    else:
825        pkt.hlim = sig.ttl - extrahops
826        if "flow" in quirks:
827            pkt.fl = random.randint(1, 2**20 - 1)
828        if "ecn" in quirks:
829            pkt.tc |= random.randint(0x01, 0x03)
830
831    # Take the options already set as "hints" to use in the new packet if we
832    # can. we'll use the already-set values if they're valid integers.
833    def int_only(val):
834        return val if isinstance(val, int) else None
835    orig_opts = dict(tcp.options)
836    mss_hint = int_only(orig_opts.get("MSS"))
837    ws_hint = int_only(orig_opts.get("WScale"))
838    ts_hint = [int_only(o) for o in orig_opts.get("Timestamp", (None, None))]
839
840    options = []
841    for opt in sig.olayout.split(","):
842        if opt == "mss":
843            # MSS might have a maximum size because of WIN_TYPE_MSS
844            if sig.win_type == WIN_TYPE_MSS:
845                maxmss = (2**16 - 1) // sig.win
846            else:
847                maxmss = (2**16 - 1)
848
849            if sig.mss == -1:  # wildcard mss
850                if mss_hint and 0 <= mss_hint <= maxmss:
851                    options.append(("MSS", mss_hint))
852                else:  # invalid hint, generate new value
853                    options.append(("MSS", random.randint(100, maxmss)))
854            else:
855                options.append(("MSS", sig.mss))
856
857        elif opt == "ws":
858            if sig.wscale == -1:  # wildcard wscale
859                maxws = 2**8
860                if "exws" in quirks:  # wscale > 14
861                    if ws_hint and 14 < ws_hint < maxws:
862                        options.append(("WScale", ws_hint))
863                    else:  # invalid hint, generate new value > 14
864                        options.append(("WScale", random.randint(15, maxws - 1)))  # noqa: E501
865                else:
866                    if ws_hint and 0 <= ws_hint < maxws:
867                        options.append(("WScale", ws_hint))
868                    else:  # invalid hint, generate new value
869                        options.append(("WScale", RandByte()))
870            else:
871                options.append(("WScale", sig.wscale))
872
873        elif opt == "ts":
874            ts1, ts2 = ts_hint
875
876            if "ts1-" in quirks:  # own timestamp specified as zero
877                ts1 = 0
878            elif uptime is not None:  # if specified uptime, override
879                ts1 = uptime
880            elif ts1 is None or not (0 < ts1 < 2**32):  # invalid hint
881                ts1 = random.randint(120, 100 * 60 * 60 * 24 * 365)
882
883            # non-zero peer timestamp on initial SYN
884            if "ts2+" in quirks and tcp_type == 0x02:
885                if ts2 is None or not (0 < ts2 < 2**32):  # invalid hint
886                    ts2 = random.randint(1, 2**32 - 1)
887            else:
888                ts2 = 0
889            options.append(("Timestamp", (ts1, ts2)))
890
891        elif opt == "nop":
892            options.append(("NOP", None))
893        elif opt == "sok":
894            options.append(("SAckOK", ""))
895        elif opt[:3] == "eol":
896            options.append(("EOL", None))
897            # FIXME: opt+ quirk not handled
898            if "opt+" in quirks:
899                warning("Unhandled opt+ quirk")
900        elif opt == "sack":
901            # Randomize SAck value in range of 10 <= val <= 34
902            sack_len = random.choice([10, 18, 26, 34]) - 2
903            optstruct = "!%iI" % (sack_len // 4)
904            rand_val = RandString(struct.calcsize(optstruct))._fix()
905            options.append(("SAck", struct.unpack(optstruct, rand_val)))
906        else:
907            warning("Unhandled TCP option %s", opt)
908        tcp.options = options
909
910    if sig.win_type == WIN_TYPE_NORMAL:
911        tcp.window = sig.win
912    elif sig.win_type == WIN_TYPE_MSS:
913        mss = [x for x in options if x[0] == "MSS"]
914        if not mss:
915            raise ValueError("TCP window value requires MSS, and MSS option not set")  # noqa: E501
916        tcp.window = mss[0][1] * sig.win
917    elif sig.win_type == WIN_TYPE_MOD:
918        tcp.window = sig.win * random.randint(1, (2**16 - 1) // sig.win)
919    elif sig.win_type == WIN_TYPE_MTU:
920        tcp.window = mtu * sig.win
921    elif sig.win_type == WIN_TYPE_ANY:
922        tcp.window = RandShort()
923    else:
924        warning("Unhandled window size specification")
925
926    if "seq-" in quirks:
927        tcp.seq = 0
928    elif tcp.seq == 0:
929        tcp.seq = random.randint(1, 2**32 - 1)
930
931    if "ack+" in quirks:
932        tcp.flags &= ~(0x10)  # ACK flag not set
933        if tcp.ack == 0:
934            tcp.ack = random.randint(1, 2**32 - 1)
935    elif "ack-" in quirks:
936        tcp.flags |= 0x10  # ACK flag set
937        tcp.ack = 0
938
939    if "uptr+" in quirks:
940        tcp.flags &= ~(0x020)  # URG flag not set
941        if tcp.urgptr == 0:
942            tcp.urgptr = random.randint(1, 2**16 - 1)
943    elif "urgf+" in quirks:
944        tcp.flags |= 0x020  # URG flag used
945
946    tcp.flags = tcp.flags | 0x08 if "pushf+" in quirks else tcp.flags & ~(0x08)
947
948    if sig.pay_class:  # signature has payload
949        if not tcp.payload:
950            pkt /= conf.raw_layer(load=RandString(random.randint(1, 10)))
951    else:
952        tcp.payload = NoPayload()
953
954    return pkt
955