xref: /aosp_15_r20/external/scapy/scapy/modules/p0f.py (revision 7dc08ffc4802948ccbc861daaf1e81c405c2c4bd)
1## This file is part of Scapy
2## See http://www.secdev.org/projects/scapy for more informations
3## Copyright (C) Philippe Biondi <[email protected]>
4## This program is published under a GPLv2 license
5
6"""
7Clone of p0f passive OS fingerprinting
8"""
9
10from __future__ import absolute_import
11from __future__ import print_function
12import time
13import struct
14import os
15import socket
16import random
17
18from scapy.data import KnowledgeBase
19from scapy.config import conf
20from scapy.compat import raw
21from scapy.layers.inet import IP, TCP, TCPOptions
22from scapy.packet import NoPayload, Packet
23from scapy.error import warning, Scapy_Exception, log_runtime
24from scapy.volatile import RandInt, RandByte, RandChoice, RandNum, RandShort, RandString
25from scapy.sendrecv import sniff
26from scapy.modules import six
27from scapy.modules.six.moves import map, range
28if conf.route is None:
29    # unused import, only to initialize conf.route
30    import scapy.route
31
32conf.p0f_base ="/etc/p0f/p0f.fp"
33conf.p0fa_base ="/etc/p0f/p0fa.fp"
34conf.p0fr_base ="/etc/p0f/p0fr.fp"
35conf.p0fo_base ="/etc/p0f/p0fo.fp"
36
37
38###############
39## p0f stuff ##
40###############
41
42# File format (according to p0f.fp) :
43#
44# wwww:ttt:D:ss:OOO...:QQ:OS:Details
45#
46# wwww    - window size
47# ttt     - initial TTL
48# D       - don't fragment bit  (0=unset, 1=set)
49# ss      - overall SYN packet size
50# OOO     - option value and order specification
51# QQ      - quirks list
52# OS      - OS genre
53# details - OS description
54
55class p0fKnowledgeBase(KnowledgeBase):
56    def __init__(self, filename):
57        KnowledgeBase.__init__(self, filename)
58        #self.ttl_range=[255]
59    def lazy_init(self):
60        try:
61            f=open(self.filename)
62        except IOError:
63            warning("Can't open base %s", self.filename)
64            return
65        try:
66            self.base = []
67            for l in f:
68                if l[0] in ["#","\n"]:
69                    continue
70                l = tuple(l.split(":"))
71                if len(l) < 8:
72                    continue
73                def a2i(x):
74                    if x.isdigit():
75                        return int(x)
76                    return x
77                li = [a2i(e) for e in l[1:4]]
78                #if li[0] not in self.ttl_range:
79                #    self.ttl_range.append(li[0])
80                #    self.ttl_range.sort()
81                self.base.append((l[0], li[0], li[1], li[2], l[4], l[5], l[6], l[7][:-1]))
82        except:
83            warning("Can't parse p0f database (new p0f version ?)")
84            self.base = None
85        f.close()
86
87p0f_kdb, p0fa_kdb, p0fr_kdb, p0fo_kdb = None, None, None, None
88
89def p0f_load_knowledgebases():
90    global p0f_kdb, p0fa_kdb, p0fr_kdb, p0fo_kdb
91    p0f_kdb = p0fKnowledgeBase(conf.p0f_base)
92    p0fa_kdb = p0fKnowledgeBase(conf.p0fa_base)
93    p0fr_kdb = p0fKnowledgeBase(conf.p0fr_base)
94    p0fo_kdb = p0fKnowledgeBase(conf.p0fo_base)
95
96p0f_load_knowledgebases()
97
98def p0f_selectdb(flags):
99    # tested flags: S, R, A
100    if flags & 0x16 == 0x2:
101        # SYN
102        return p0f_kdb
103    elif flags & 0x16 == 0x12:
104        # SYN/ACK
105        return p0fa_kdb
106    elif flags & 0x16 in [ 0x4, 0x14 ]:
107        # RST RST/ACK
108        return p0fr_kdb
109    elif flags & 0x16 == 0x10:
110        # ACK
111        return p0fo_kdb
112    else:
113        return None
114
115def packet2p0f(pkt):
116    pkt = pkt.copy()
117    pkt = pkt.__class__(raw(pkt))
118    while pkt.haslayer(IP) and pkt.haslayer(TCP):
119        pkt = pkt.getlayer(IP)
120        if isinstance(pkt.payload, TCP):
121            break
122        pkt = pkt.payload
123
124    if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
125        raise TypeError("Not a TCP/IP packet")
126    #if pkt.payload.flags & 0x7 != 0x02: #S,!F,!R
127    #    raise TypeError("Not a SYN or SYN/ACK packet")
128
129    db = p0f_selectdb(pkt.payload.flags)
130
131    #t = p0f_kdb.ttl_range[:]
132    #t += [pkt.ttl]
133    #t.sort()
134    #ttl=t[t.index(pkt.ttl)+1]
135    ttl = pkt.ttl
136
137    ss = len(pkt)
138    # from p0f/config.h : PACKET_BIG = 100
139    if ss > 100:
140        if db == p0fr_kdb:
141            # p0fr.fp: "Packet size may be wildcarded. The meaning of
142            #           wildcard is, however, hardcoded as 'size >
143            #           PACKET_BIG'"
144            ss = '*'
145        else:
146            ss = 0
147    if db == p0fo_kdb:
148        # p0fo.fp: "Packet size MUST be wildcarded."
149        ss = '*'
150
151    ooo = ""
152    mss = -1
153    qqT = False
154    qqP = False
155    #qqBroken = False
156    ilen = (pkt.payload.dataofs << 2) - 20 # from p0f.c
157    for option in pkt.payload.options:
158        ilen -= 1
159        if option[0] == "MSS":
160            ooo += "M" + str(option[1]) + ","
161            mss = option[1]
162            # FIXME: qqBroken
163            ilen -= 3
164        elif option[0] == "WScale":
165            ooo += "W" + str(option[1]) + ","
166            # FIXME: qqBroken
167            ilen -= 2
168        elif option[0] == "Timestamp":
169            if option[1][0] == 0:
170                ooo += "T0,"
171            else:
172                ooo += "T,"
173            if option[1][1] != 0:
174                qqT = True
175            ilen -= 9
176        elif option[0] == "SAckOK":
177            ooo += "S,"
178            ilen -= 1
179        elif option[0] == "NOP":
180            ooo += "N,"
181        elif option[0] == "EOL":
182            ooo += "E,"
183            if ilen > 0:
184                qqP = True
185        else:
186            if isinstance(option[0], str):
187                ooo += "?%i," % TCPOptions[1][option[0]]
188            else:
189                ooo += "?%i," % option[0]
190            # FIXME: ilen
191    ooo = ooo[:-1]
192    if ooo == "": ooo = "."
193
194    win = pkt.payload.window
195    if mss != -1:
196        if mss != 0 and win % mss == 0:
197            win = "S" + str(win/mss)
198        elif win % (mss + 40) == 0:
199            win = "T" + str(win/(mss+40))
200    win = str(win)
201
202    qq = ""
203
204    if db == p0fr_kdb:
205        if pkt.payload.flags & 0x10 == 0x10:
206            # p0fr.fp: "A new quirk, 'K', is introduced to denote
207            #           RST+ACK packets"
208            qq += "K"
209    # The two next cases should also be only for p0f*r*, but although
210    # it's not documented (or I have not noticed), p0f seems to
211    # support the '0' and 'Q' quirks on any databases (or at the least
212    # "classical" p0f.fp).
213    if pkt.payload.seq == pkt.payload.ack:
214        # p0fr.fp: "A new quirk, 'Q', is used to denote SEQ number
215        #           equal to ACK number."
216        qq += "Q"
217    if pkt.payload.seq == 0:
218        # p0fr.fp: "A new quirk, '0', is used to denote packets
219        #           with SEQ number set to 0."
220        qq += "0"
221    if qqP:
222        qq += "P"
223    if pkt.id == 0:
224        qq += "Z"
225    if pkt.options != []:
226        qq += "I"
227    if pkt.payload.urgptr != 0:
228        qq += "U"
229    if pkt.payload.reserved != 0:
230        qq += "X"
231    if pkt.payload.ack != 0:
232        qq += "A"
233    if qqT:
234        qq += "T"
235    if db == p0fo_kdb:
236        if pkt.payload.flags & 0x20 != 0:
237            # U
238            # p0fo.fp: "PUSH flag is excluded from 'F' quirk checks"
239            qq += "F"
240    else:
241        if pkt.payload.flags & 0x28 != 0:
242            # U or P
243            qq += "F"
244    if db != p0fo_kdb and not isinstance(pkt.payload.payload, NoPayload):
245        # p0fo.fp: "'D' quirk is not checked for."
246        qq += "D"
247    # FIXME : "!" - broken options segment: not handled yet
248
249    if qq == "":
250        qq = "."
251
252    return (db, (win, ttl, pkt.flags.DF, ss, ooo, qq))
253
254def p0f_correl(x,y):
255    d = 0
256    # wwww can be "*" or "%nn". "Tnn" and "Snn" should work fine with
257    # the x[0] == y[0] test.
258    d += (x[0] == y[0] or y[0] == "*" or (y[0][0] == "%" and x[0].isdigit() and (int(x[0]) % int(y[0][1:])) == 0))
259    # ttl
260    d += (y[1] >= x[1] and y[1] - x[1] < 32)
261    for i in [2, 5]:
262        d += (x[i] == y[i] or y[i] == '*')
263    # '*' has a special meaning for ss
264    d += x[3] == y[3]
265    xopt = x[4].split(",")
266    yopt = y[4].split(",")
267    if len(xopt) == len(yopt):
268        same = True
269        for i in range(len(xopt)):
270            if not (xopt[i] == yopt[i] or
271                    (len(yopt[i]) == 2 and len(xopt[i]) > 1 and
272                     yopt[i][1] == "*" and xopt[i][0] == yopt[i][0]) or
273                    (len(yopt[i]) > 2 and len(xopt[i]) > 1 and
274                     yopt[i][1] == "%" and xopt[i][0] == yopt[i][0] and
275                     int(xopt[i][1:]) % int(yopt[i][2:]) == 0)):
276                same = False
277                break
278        if same:
279            d += len(xopt)
280    return d
281
282
283@conf.commands.register
284def p0f(pkt):
285    """Passive OS fingerprinting: which OS emitted this TCP packet ?
286p0f(packet) -> accuracy, [list of guesses]
287"""
288    db, sig = packet2p0f(pkt)
289    if db:
290        pb = db.get_base()
291    else:
292        pb = []
293    if not pb:
294        warning("p0f base empty.")
295        return []
296    #s = len(pb[0][0])
297    r = []
298    max = len(sig[4].split(",")) + 5
299    for b in pb:
300        d = p0f_correl(sig,b)
301        if d == max:
302            r.append((b[6], b[7], b[1] - pkt[IP].ttl))
303    return r
304
305def prnp0f(pkt):
306    """Calls p0f and returns a user-friendly output"""
307    # we should print which DB we use
308    try:
309        r = p0f(pkt)
310    except:
311        return
312    if r == []:
313        r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt)[1])) + ":?:?]", None)
314    else:
315        r = r[0]
316    uptime = None
317    try:
318        uptime = pkt2uptime(pkt)
319    except:
320        pass
321    if uptime == 0:
322        uptime = None
323    res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1])
324    if uptime is not None:
325        res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n  -> %IP.dst%:%TCP.dport% (%TCP.flags%)")
326    else:
327        res += pkt.sprintf("\n  -> %IP.dst%:%TCP.dport% (%TCP.flags%)")
328    if r[2] is not None:
329        res += " (distance " + str(r[2]) + ")"
330    print(res)
331
332@conf.commands.register
333def pkt2uptime(pkt, HZ=100):
334    """Calculate the date the machine which emitted the packet booted using TCP timestamp
335pkt2uptime(pkt, [HZ=100])"""
336    if not isinstance(pkt, Packet):
337        raise TypeError("Not a TCP packet")
338    if isinstance(pkt,NoPayload):
339        raise TypeError("Not a TCP packet")
340    if not isinstance(pkt, TCP):
341        return pkt2uptime(pkt.payload)
342    for opt in pkt.options:
343        if opt[0] == "Timestamp":
344            #t = pkt.time - opt[1][0] * 1.0/HZ
345            #return time.ctime(t)
346            t = opt[1][0] / HZ
347            return t
348    raise TypeError("No timestamp option")
349
350def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
351                    extrahops=0, mtu=1500, uptime=None):
352    """Modifies pkt so that p0f will think it has been sent by a
353specific OS.  If osdetails is None, then we randomly pick up a
354personality matching osgenre. If osgenre and signature are also None,
355we use a local signature (using p0f_getlocalsigs). If signature is
356specified (as a tuple), we use the signature.
357
358For now, only TCP Syn packets are supported.
359Some specifications of the p0f.fp file are not (yet) implemented."""
360    pkt = pkt.copy()
361    #pkt = pkt.__class__(raw(pkt))
362    while pkt.haslayer(IP) and pkt.haslayer(TCP):
363        pkt = pkt.getlayer(IP)
364        if isinstance(pkt.payload, TCP):
365            break
366        pkt = pkt.payload
367
368    if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
369        raise TypeError("Not a TCP/IP packet")
370
371    db = p0f_selectdb(pkt.payload.flags)
372    if osgenre:
373        pb = db.get_base()
374        if pb is None:
375            pb = []
376        pb = [x for x in pb if x[6] == osgenre]
377        if osdetails:
378            pb = [x for x in pb if x[7] == osdetails]
379    elif signature:
380        pb = [signature]
381    else:
382        pb = p0f_getlocalsigs()[db]
383    if db == p0fr_kdb:
384        # 'K' quirk <=> RST+ACK
385        if pkt.payload.flags & 0x4 == 0x4:
386            pb = [x for x in pb if 'K' in x[5]]
387        else:
388            pb = [x for x in pb if 'K' not in x[5]]
389    if not pb:
390        raise Scapy_Exception("No match in the p0f database")
391    pers = pb[random.randint(0, len(pb) - 1)]
392
393    # options (we start with options because of MSS)
394    # Take the options already set as "hints" to use in the new packet if we
395    # can. MSS, WScale and Timestamp can all be wildcarded in a signature, so
396    # we'll use the already-set values if they're valid integers.
397    orig_opts = dict(pkt.payload.options)
398    int_only = lambda val: val if isinstance(val, six.integer_types) else None
399    mss_hint = int_only(orig_opts.get('MSS'))
400    wscale_hint = int_only(orig_opts.get('WScale'))
401    ts_hint = [int_only(o) for o in orig_opts.get('Timestamp', (None, None))]
402
403    options = []
404    if pers[4] != '.':
405        for opt in pers[4].split(','):
406            if opt[0] == 'M':
407                # MSS might have a maximum size because of window size
408                # specification
409                if pers[0][0] == 'S':
410                    maxmss = (2**16-1) // int(pers[0][1:])
411                else:
412                    maxmss = (2**16-1)
413                # disregard hint if out of range
414                if mss_hint and not 0 <= mss_hint <= maxmss:
415                    mss_hint = None
416                # If we have to randomly pick up a value, we cannot use
417                # scapy RandXXX() functions, because the value has to be
418                # set in case we need it for the window size value. That's
419                # why we use random.randint()
420                if opt[1:] == '*':
421                    if mss_hint is not None:
422                        options.append(('MSS', mss_hint))
423                    else:
424                        options.append(('MSS', random.randint(1, maxmss)))
425                elif opt[1] == '%':
426                    coef = int(opt[2:])
427                    if mss_hint is not None and mss_hint % coef == 0:
428                        options.append(('MSS', mss_hint))
429                    else:
430                        options.append((
431                            'MSS', coef*random.randint(1, maxmss//coef)))
432                else:
433                    options.append(('MSS', int(opt[1:])))
434            elif opt[0] == 'W':
435                if wscale_hint and not 0 <= wscale_hint < 2**8:
436                    wscale_hint = None
437                if opt[1:] == '*':
438                    if wscale_hint is not None:
439                        options.append(('WScale', wscale_hint))
440                    else:
441                        options.append(('WScale', RandByte()))
442                elif opt[1] == '%':
443                    coef = int(opt[2:])
444                    if wscale_hint is not None and wscale_hint % coef == 0:
445                        options.append(('WScale', wscale_hint))
446                    else:
447                        options.append((
448                            'WScale', coef*RandNum(min=1, max=(2**8-1)//coef)))
449                else:
450                    options.append(('WScale', int(opt[1:])))
451            elif opt == 'T0':
452                options.append(('Timestamp', (0, 0)))
453            elif opt == 'T':
454                # Determine first timestamp.
455                if uptime is not None:
456                    ts_a = uptime
457                elif ts_hint[0] and 0 < ts_hint[0] < 2**32:
458                    # Note: if first ts is 0, p0f registers it as "T0" not "T",
459                    # hence we don't want to use the hint if it was 0.
460                    ts_a = ts_hint[0]
461                else:
462                    ts_a = random.randint(120, 100*60*60*24*365)
463                # Determine second timestamp.
464                if 'T' not in pers[5]:
465                    ts_b = 0
466                elif ts_hint[1] and 0 < ts_hint[1] < 2**32:
467                    ts_b = ts_hint[1]
468                else:
469                    # FIXME: RandInt() here does not work (bug (?) in
470                    # TCPOptionsField.m2i often raises "OverflowError:
471                    # long int too large to convert to int" in:
472                    #    oval = struct.pack(ofmt, *oval)"
473                    # Actually, this is enough to often raise the error:
474                    #    struct.pack('I', RandInt())
475                    ts_b = random.randint(1, 2**32-1)
476                options.append(('Timestamp', (ts_a, ts_b)))
477            elif opt == 'S':
478                options.append(('SAckOK', ''))
479            elif opt == 'N':
480                options.append(('NOP', None))
481            elif opt == 'E':
482                options.append(('EOL', None))
483            elif opt[0] == '?':
484                if int(opt[1:]) in TCPOptions[0]:
485                    optname = TCPOptions[0][int(opt[1:])][0]
486                    optstruct = TCPOptions[0][int(opt[1:])][1]
487                    options.append((optname,
488                                    struct.unpack(optstruct,
489                                                  RandString(struct.calcsize(optstruct))._fix())))
490                else:
491                    options.append((int(opt[1:]), ''))
492            ## FIXME: qqP not handled
493            else:
494                warning("unhandled TCP option " + opt)
495            pkt.payload.options = options
496
497    # window size
498    if pers[0] == '*':
499        pkt.payload.window = RandShort()
500    elif pers[0].isdigit():
501        pkt.payload.window = int(pers[0])
502    elif pers[0][0] == '%':
503        coef = int(pers[0][1:])
504        pkt.payload.window = coef * RandNum(min=1, max=(2**16-1)//coef)
505    elif pers[0][0] == 'T':
506        pkt.payload.window = mtu * int(pers[0][1:])
507    elif pers[0][0] == 'S':
508        ## needs MSS set
509        mss = [x for x in options if x[0] == 'MSS']
510        if not mss:
511            raise Scapy_Exception("TCP window value requires MSS, and MSS option not set")
512        pkt.payload.window = mss[0][1] * int(pers[0][1:])
513    else:
514        raise Scapy_Exception('Unhandled window size specification')
515
516    # ttl
517    pkt.ttl = pers[1]-extrahops
518    # DF flag
519    pkt.flags |= (2 * pers[2])
520    ## FIXME: ss (packet size) not handled (how ? may be with D quirk
521    ## if present)
522    # Quirks
523    if pers[5] != '.':
524        for qq in pers[5]:
525            ## FIXME: not handled: P, I, X, !
526            # T handled with the Timestamp option
527            if qq == 'Z': pkt.id = 0
528            elif qq == 'U': pkt.payload.urgptr = RandShort()
529            elif qq == 'A': pkt.payload.ack = RandInt()
530            elif qq == 'F':
531                if db == p0fo_kdb:
532                    pkt.payload.flags |= 0x20 # U
533                else:
534                    pkt.payload.flags |= random.choice([8, 32, 40])  # P/U/PU
535            elif qq == 'D' and db != p0fo_kdb:
536                pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) # XXX p0fo.fp
537            elif qq == 'Q': pkt.payload.seq = pkt.payload.ack
538            #elif qq == '0': pkt.payload.seq = 0
539        #if db == p0fr_kdb:
540        # '0' quirk is actually not only for p0fr.fp (see
541        # packet2p0f())
542    if '0' in pers[5]:
543        pkt.payload.seq = 0
544    elif pkt.payload.seq == 0:
545        pkt.payload.seq = RandInt()
546
547    while pkt.underlayer:
548        pkt = pkt.underlayer
549    return pkt
550
551def p0f_getlocalsigs():
552    """This function returns a dictionary of signatures indexed by p0f
553db (e.g., p0f_kdb, p0fa_kdb, ...) for the local TCP/IP stack.
554
555You need to have your firewall at least accepting the TCP packets
556from/to a high port (30000 <= x <= 40000) on your loopback interface.
557
558Please note that the generated signatures come from the loopback
559interface and may (are likely to) be different than those generated on
560"normal" interfaces."""
561    pid = os.fork()
562    port = random.randint(30000, 40000)
563    if pid > 0:
564        # parent: sniff
565        result = {}
566        def addresult(res):
567            # TODO: wildcard window size in some cases? and maybe some
568            # other values?
569            if res[0] not in result:
570                result[res[0]] = [res[1]]
571            else:
572                if res[1] not in result[res[0]]:
573                    result[res[0]].append(res[1])
574        # XXX could we try with a "normal" interface using other hosts
575        iface = conf.route.route('127.0.0.1')[0]
576        # each packet is seen twice: S + RA, S + SA + A + FA + A
577        # XXX are the packets also seen twice on non Linux systems ?
578        count=14
579        pl = sniff(iface=iface, filter='tcp and port ' + str(port), count = count, timeout=3)
580        for pkt in pl:
581            for elt in packet2p0f(pkt):
582                addresult(elt)
583        os.waitpid(pid,0)
584    elif pid < 0:
585        log_runtime.error("fork error")
586    else:
587        # child: send
588        # XXX erk
589        time.sleep(1)
590        s1 = socket.socket(socket.AF_INET, type = socket.SOCK_STREAM)
591        # S & RA
592        try:
593            s1.connect(('127.0.0.1', port))
594        except socket.error:
595            pass
596        # S, SA, A, FA, A
597        s1.bind(('127.0.0.1', port))
598        s1.connect(('127.0.0.1', port))
599        # howto: get an RST w/o ACK packet
600        s1.close()
601        os._exit(0)
602    return result
603
604