Changeset 1086:fa0c948e7ffa

Show
Ignore:
Timestamp:
09/21/09 19:03:59 (12 months ago)
Author:
Phil <phil@…>
Branch:
default
Message:

Improved p0f support (patch from ticket #76, P. Lalet)

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • scapy/modules/p0f.py

    r862 r1086  
    66from scapy.data import KnowledgeBase 
    77from scapy.config import conf 
     8from scapy.layers.inet import IP, TCP, TCPOptions 
     9from scapy.packet import NoPayload 
    810 
    911conf.p0f_base ="/etc/p0f/p0f.fp" 
     12conf.p0fa_base ="/etc/p0f/p0fa.fp" 
     13conf.p0fr_base ="/etc/p0f/p0fr.fp" 
     14conf.p0fo_base ="/etc/p0f/p0fo.fp" 
    1015 
    1116 
     
    2631# OS      - OS genre 
    2732# details - OS description 
    28  
    29  
    3033 
    3134class p0fKnowledgeBase(KnowledgeBase): 
     
    4750                if len(l) < 8: 
    4851                    continue 
    49                 li = map(int,l[1:4]) 
     52                def a2i(x): 
     53                    if x.isdigit(): 
     54                        return int(x) 
     55                    return x 
     56                li = map(a2i, l[1:4]) 
    5057                #if li[0] not in self.ttl_range: 
    5158                #    self.ttl_range.append(li[0]) 
     
    5865 
    5966p0f_kdb = p0fKnowledgeBase(conf.p0f_base) 
    60  
     67p0fa_kdb = p0fKnowledgeBase(conf.p0fa_base) 
     68p0fr_kdb = p0fKnowledgeBase(conf.p0fr_base) 
     69p0fo_kdb = p0fKnowledgeBase(conf.p0fo_base) 
     70 
     71def p0f_selectdb(flags): 
     72    # tested flags: S, R, A 
     73    if flags & 0x16 == 0x2: 
     74        # SYN 
     75        return p0f_kdb 
     76    elif flags & 0x16 == 0x12: 
     77        # SYN/ACK 
     78        return p0fa_kdb 
     79    elif flags & 0x16 in [ 0x4, 0x14 ]: 
     80        # RST RST/ACK 
     81        return p0fr_kdb 
     82    elif flags & 0x16 == 0x10: 
     83        # ACK 
     84        return p0fo_kdb 
     85    else: 
     86        return None 
    6187 
    6288def packet2p0f(pkt): 
     89    pkt = pkt.copy() 
     90    pkt = pkt.__class__(str(pkt)) 
    6391    while pkt.haslayer(IP) and pkt.haslayer(TCP): 
    6492        pkt = pkt.getlayer(IP) 
     
    6694            break 
    6795        pkt = pkt.payload 
    68  
     96     
    6997    if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): 
    7098        raise TypeError("Not a TCP/IP packet") 
    71     if pkt.payload.flags & 0x13 != 0x02: #S,!A,!F 
    72         raise TypeError("Not a syn packet") 
     99    #if pkt.payload.flags & 0x7 != 0x02: #S,!F,!R 
     100    #    raise TypeError("Not a SYN or SYN/ACK packet") 
     101     
     102    db = p0f_selectdb(pkt.payload.flags) 
    73103     
    74104    #t = p0f_kdb.ttl_range[:] 
     
    77107    #ttl=t[t.index(pkt.ttl)+1] 
    78108    ttl = pkt.ttl 
    79  
     109     
    80110    df = (pkt.flags & 2) / 2 
    81111    ss = len(pkt) 
    82112    # from p0f/config.h : PACKET_BIG = 100 
    83113    if ss > 100: 
    84         ss = 0 
    85  
     114        if db == p0fr_kdb: 
     115            # p0fr.fp: "Packet size may be wildcarded. The meaning of 
     116            #           wildcard is, however, hardcoded as 'size > 
     117            #           PACKET_BIG'" 
     118            ss = '*' 
     119        else: 
     120            ss = 0 
     121    if db == p0fo_kdb: 
     122        # p0fo.fp: "Packet size MUST be wildcarded." 
     123        ss = '*' 
     124     
    86125    ooo = "" 
    87126    mss = -1 
     
    89128    qqP = False 
    90129    #qqBroken = False 
    91     ilen = (pkt[TCP].dataofs << 2) - 20 # from p0f.c 
     130    ilen = (pkt.payload.dataofs << 2) - 20 # from p0f.c 
    92131    for option in pkt.payload.options: 
    93132        ilen -= 1 
     
    119158                qqP = True 
    120159        else: 
    121             ooo += "?," 
     160            if type(option[0]) is str: 
     161                ooo += "?%i," % TCPOptions[1][option[0]] 
     162            else: 
     163                ooo += "?%i," % option[0] 
    122164            # FIXME: ilen 
    123165    ooo = ooo[:-1] 
    124166    if ooo == "": ooo = "." 
    125  
     167     
    126168    win = pkt.payload.window 
    127169    if mss != -1: 
    128         if win % mss == 0: 
     170        if mss != 0 and win % mss == 0: 
    129171            win = "S" + str(win/mss) 
    130172        elif win % (mss + 40) == 0: 
    131173            win = "T" + str(win/(mss+40)) 
    132         win = str(win) 
    133  
     174    win = str(win) 
     175     
    134176    qq = "" 
    135  
     177     
     178    if db == p0fr_kdb: 
     179        if pkt.payload.flags & 0x10 == 0x10: 
     180            # p0fr.fp: "A new quirk, 'K', is introduced to denote 
     181            #           RST+ACK packets" 
     182            qq += "K" 
     183    # The two next cases should also be only for p0f*r*, but although 
     184    # it's not documented (or I have not noticed), p0f seems to 
     185    # support the '0' and 'Q' quirks on any databases (or at the least 
     186    # "classical" p0f.fp). 
     187    if pkt.payload.seq == pkt.payload.ack: 
     188        # p0fr.fp: "A new quirk, 'Q', is used to denote SEQ number 
     189        #           equal to ACK number." 
     190        qq += "Q" 
     191    if pkt.payload.seq == 0: 
     192        # p0fr.fp: "A new quirk, '0', is used to denote packets 
     193        #           with SEQ number set to 0." 
     194        qq += "0" 
    136195    if qqP: 
    137196        qq += "P" 
    138     if pkt[IP].id == 0: 
     197    if pkt.id == 0: 
    139198        qq += "Z" 
    140     if pkt[IP].options != '': 
     199    if pkt.options != []: 
    141200        qq += "I" 
    142     if pkt[TCP].urgptr != 0: 
     201    if pkt.payload.urgptr != 0: 
    143202        qq += "U" 
    144     if pkt[TCP].reserved != 0: 
     203    if pkt.payload.reserved != 0: 
    145204        qq += "X" 
    146     if pkt[TCP].ack != 0: 
     205    if pkt.payload.ack != 0: 
    147206        qq += "A" 
    148207    if qqT: 
    149208        qq += "T" 
    150     if pkt[TCP].flags & 40 != 0: 
    151         # U or P 
    152         qq += "F" 
    153     if not isinstance(pkt[TCP].payload, NoPayload): 
     209    if db == p0fo_kdb: 
     210        if pkt.payload.flags & 0x20 != 0: 
     211            # U 
     212            # p0fo.fp: "PUSH flag is excluded from 'F' quirk checks" 
     213            qq += "F" 
     214    else: 
     215        if pkt.payload.flags & 0x28 != 0: 
     216            # U or P 
     217            qq += "F" 
     218    if db != p0fo_kdb and not isinstance(pkt.payload.payload, NoPayload): 
     219        # p0fo.fp: "'D' quirk is not checked for." 
    154220        qq += "D" 
    155     # FIXME : "!" - broken options segment 
     221    # FIXME : "!" - broken options segment: not handled yet 
    156222 
    157223    if qq == "": 
    158224        qq = "." 
    159225 
    160     return (win, 
    161             ttl, 
    162             df, 
    163             ss, 
    164             ooo, 
    165             qq) 
     226    return (db, (win, ttl, df, ss, ooo, qq)) 
    166227 
    167228def p0f_correl(x,y): 
    168229    d = 0 
    169     # wwww can be "*" or "%nn" 
     230    # wwww can be "*" or "%nn". "Tnn" and "Snn" should work fine with 
     231    # the x[0] == y[0] test. 
    170232    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)) 
    171233    # ttl 
    172234    d += (y[1] >= x[1] and y[1] - x[1] < 32) 
    173     for i in [2, 3, 5]: 
    174         d += (x[i] == y[i]) 
     235    for i in [2, 5]: 
     236        d += (x[i] == y[i] or y[i] == '*') 
     237    # '*' has a special meaning for ss 
     238    d += x[3] == y[3] 
    175239    xopt = x[4].split(",") 
    176240    yopt = y[4].split(",") 
     
    193257@conf.commands.register 
    194258def p0f(pkt): 
    195     """Passive OS fingerprinting: which OS emitted this TCP SYN ? 
     259    """Passive OS fingerprinting: which OS emitted this TCP packet ? 
    196260p0f(packet) -> accuracy, [list of guesses] 
    197261""" 
    198     pb = p0f_kdb.get_base() 
     262    db, sig = packet2p0f(pkt) 
     263    if db: 
     264        pb = db.get_base() 
     265    else: 
     266        pb = [] 
    199267    if not pb: 
    200268        warning("p0f base empty.") 
    201269        return [] 
    202     s = len(pb[0][0]) 
     270    #s = len(pb[0][0]) 
    203271    r = [] 
    204     sig = packet2p0f(pkt) 
    205272    max = len(sig[4].split(",")) + 5 
    206273    for b in pb: 
     
    209276            r.append((b[6], b[7], b[1] - pkt[IP].ttl)) 
    210277    return r 
    211              
    212278 
    213279def prnp0f(pkt): 
     280    # we should print which DB we use 
    214281    try: 
    215282        r = p0f(pkt) 
     
    217284        return 
    218285    if r == []: 
    219         r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt))) + ":?:?]", None) 
     286        r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt)[1])) + ":?:?]", None) 
    220287    else: 
    221288        r = r[0] 
     
    229296    res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1]) 
    230297    if uptime is not None: 
    231         res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n  -> %IP.dst%:%TCP.dport%") 
    232     else: 
    233         res += pkt.sprintf("\n  -> %IP.dst%:%TCP.dport%") 
     298        res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n  -> %IP.dst%:%TCP.dport% (%TCP.flags%)") 
     299    else: 
     300        res += pkt.sprintf("\n  -> %IP.dst%:%TCP.dport% (%TCP.flags%)") 
    234301    if r[2] is not None: 
    235302        res += " (distance " + str(r[2]) + ")" 
     
    238305@conf.commands.register 
    239306def pkt2uptime(pkt, HZ=100): 
    240     """Calculate the date the machine which emitted the packet booted using TCP timestamp 
     307    """Calculate the date the machine which emitted the packet booted using TCP timestamp  
    241308pkt2uptime(pkt, [HZ=100])""" 
    242309    if not isinstance(pkt, Packet): 
     
    254321    raise TypeError("No timestamp option") 
    255322 
    256  
     323def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, 
     324                    extrahops=0, mtu=1500, uptime=None): 
     325    """Modifies pkt so that p0f will think it has been sent by a 
     326specific OS.  If osdetails is None, then we randomly pick up a 
     327personality matching osgenre. If osgenre and signature are also None, 
     328we use a local signature (using p0f_getlocalsigs). If signature is 
     329specified (as a tuple), we use the signature. 
     330 
     331For now, only TCP Syn packets are supported. 
     332Some specifications of the p0f.fp file are not (yet) implemented.""" 
     333    pkt = pkt.copy() 
     334    #pkt = pkt.__class__(str(pkt)) 
     335    while pkt.haslayer(IP) and pkt.haslayer(TCP): 
     336        pkt = pkt.getlayer(IP) 
     337        if isinstance(pkt.payload, TCP): 
     338            break 
     339        pkt = pkt.payload 
     340     
     341    if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): 
     342        raise TypeError("Not a TCP/IP packet") 
     343     
     344    if uptime is None: 
     345        uptime = random.randint(120,100*60*60*24*365) 
     346     
     347    db = p0f_selectdb(pkt.payload.flags) 
     348    if osgenre: 
     349        pb = db.get_base() 
     350        if pb is None: 
     351            pb = [] 
     352        pb = filter(lambda x: x[6] == osgenre, pb) 
     353        if osdetails: 
     354            pb = filter(lambda x: x[7] == osdetails, pb) 
     355    elif signature: 
     356        pb = [signature] 
     357    else: 
     358        pb = p0f_getlocalsigs()[db] 
     359    if db == p0fr_kdb: 
     360        # 'K' quirk <=> RST+ACK 
     361        if pkt.payload.flags & 0x4 == 0x4: 
     362            pb = filter(lambda x: 'K' in x[5], pb) 
     363        else: 
     364            pb = filter(lambda x: 'K' not in x[5], pb) 
     365    if not pb: 
     366        raise Scapy_Exception("No match in the p0f database") 
     367    pers = pb[random.randint(0, len(pb) - 1)] 
     368     
     369    # options (we start with options because of MSS) 
     370    ## TODO: let the options already set if they are valid 
     371    options = [] 
     372    if pers[4] != '.': 
     373        for opt in pers[4].split(','): 
     374            if opt[0] == 'M': 
     375                # MSS might have a maximum size because of window size 
     376                # specification 
     377                if pers[0][0] == 'S': 
     378                    maxmss = (2L**16-1) / int(pers[0][1:]) 
     379                else: 
     380                    maxmss = (2L**16-1) 
     381                # If we have to randomly pick up a value, we cannot use 
     382                # scapy RandXXX() functions, because the value has to be 
     383                # set in case we need it for the window size value. That's 
     384                # why we use random.randint() 
     385                if opt[1:] == '*': 
     386                    options.append(('MSS', random.randint(1,maxmss))) 
     387                elif opt[1] == '%': 
     388                    coef = int(opt[2:]) 
     389                    options.append(('MSS', coef*random.randint(1,maxmss/coef))) 
     390                else: 
     391                    options.append(('MSS', int(opt[1:]))) 
     392            elif opt[0] == 'W': 
     393                if opt[1:] == '*': 
     394                    options.append(('WScale', RandByte())) 
     395                elif opt[1] == '%': 
     396                    coef = int(opt[2:]) 
     397                    options.append(('WScale', coef*RandNum(min=1, 
     398                                                           max=(2L**8-1)/coef))) 
     399                else: 
     400                    options.append(('WScale', int(opt[1:]))) 
     401            elif opt == 'T0': 
     402                options.append(('Timestamp', (0, 0))) 
     403            elif opt == 'T': 
     404                if 'T' in pers[5]: 
     405                    # FIXME: RandInt() here does not work (bug (?) in 
     406                    # TCPOptionsField.m2i often raises "OverflowError: 
     407                    # long int too large to convert to int" in: 
     408                    #    oval = struct.pack(ofmt, *oval)" 
     409                    # Actually, this is enough to often raise the error: 
     410                    #    struct.pack('I', RandInt()) 
     411                    options.append(('Timestamp', (uptime, random.randint(1,2**32-1)))) 
     412                else: 
     413                    options.append(('Timestamp', (uptime, 0))) 
     414            elif opt == 'S': 
     415                options.append(('SAckOK', '')) 
     416            elif opt == 'N': 
     417                options.append(('NOP', None)) 
     418            elif opt == 'E': 
     419                options.append(('EOL', None)) 
     420            elif opt[0] == '?': 
     421                if int(opt[1:]) in TCPOptions[0]: 
     422                    optname = TCPOptions[0][int(opt[1:])][0] 
     423                    optstruct = TCPOptions[0][int(opt[1:])][1] 
     424                    options.append((optname, 
     425                                    struct.unpack(optstruct, 
     426                                                  RandString(struct.calcsize(optstruct))._fix()))) 
     427                else: 
     428                    options.append((int(opt[1:]), '')) 
     429            ## FIXME: qqP not handled 
     430            else: 
     431                warning("unhandled TCP option " + opt) 
     432            pkt.payload.options = options 
     433     
     434    # window size 
     435    if pers[0] == '*': 
     436        pkt.payload.window = RandShort() 
     437    elif pers[0].isdigit(): 
     438        pkt.payload.window = int(pers[0]) 
     439    elif pers[0][0] == '%': 
     440        coef = int(pers[0][1:]) 
     441        pkt.payload.window = coef * RandNum(min=1,max=(2L**16-1)/coef) 
     442    elif pers[0][0] == 'T': 
     443        pkt.payload.window = mtu * int(pers[0][1:]) 
     444    elif pers[0][0] == 'S': 
     445        ## needs MSS set 
     446        MSS = filter(lambda x: x[0] == 'MSS', options) 
     447        if not filter(lambda x: x[0] == 'MSS', options): 
     448            raise Scapy_Exception("TCP window value requires MSS, and MSS option not set") 
     449        pkt.payload.window = filter(lambda x: x[0] == 'MSS', options)[0][1] * int(pers[0][1:]) 
     450    else: 
     451        raise Scapy_Exception('Unhandled window size specification') 
     452     
     453    # ttl 
     454    pkt.ttl = pers[1]-extrahops 
     455    # DF flag 
     456    pkt.flags |= (2 * pers[2]) 
     457    ## FIXME: ss (packet size) not handled (how ? may be with D quirk 
     458    ## if present) 
     459    # Quirks 
     460    if pers[5] != '.': 
     461        for qq in pers[5]: 
     462            ## FIXME: not handled: P, I, X, ! 
     463            # T handled with the Timestamp option 
     464            if qq == 'Z': pkt.id = 0 
     465            elif qq == 'U': pkt.payload.urgptr = RandShort() 
     466            elif qq == 'A': pkt.payload.ack = RandInt() 
     467            elif qq == 'F': 
     468                if db == p0fo_kdb: 
     469                    pkt.payload.flags |= 0x20 # U 
     470                else: 
     471                    pkt.payload.flags |= RandChoice(8, 32, 40) #P / U / PU 
     472            elif qq == 'D' and db != p0fo_kdb: 
     473                pkt /= Raw(load=RandString(random.randint(1, 10))) # XXX p0fo.fp 
     474            elif qq == 'Q': pkt.payload.seq = pkt.payload.ack 
     475            #elif qq == '0': pkt.payload.seq = 0 
     476        #if db == p0fr_kdb: 
     477        # '0' quirk is actually not only for p0fr.fp (see 
     478        # packet2p0f()) 
     479    if '0' in pers[5]: 
     480        pkt.payload.seq = 0 
     481    elif pkt.payload.seq == 0: 
     482        pkt.payload.seq = RandInt() 
     483     
     484    while pkt.underlayer: 
     485        pkt = pkt.underlayer 
     486    return pkt 
     487 
     488def p0f_getlocalsigs(): 
     489    """This function returns a dictionary of signatures indexed by p0f 
     490db (e.g., p0f_kdb, p0fa_kdb, ...) for the local TCP/IP stack. 
     491 
     492You need to have your firewall at least accepting the TCP packets 
     493from/to a high port (30000 <= x <= 40000) on your loopback interface. 
     494 
     495Please note that the generated signatures come from the loopback 
     496interface and may (are likely to) be different than those generated on 
     497"normal" interfaces.""" 
     498    pid = os.fork() 
     499    port = random.randint(30000, 40000) 
     500    if pid > 0: 
     501        # parent: sniff 
     502        result = {} 
     503        def addresult(res): 
     504            # TODO: wildcard window size in some cases? and maybe some 
     505            # other values? 
     506            if res[0] not in result: 
     507                result[res[0]] = [res[1]] 
     508            else: 
     509                if res[1] not in result[res[0]]: 
     510                    result[res[0]].append(res[1]) 
     511        # XXX could we try with a "normal" interface using other hosts 
     512        iface = conf.route.route('127.0.0.1')[0] 
     513        # each packet is seen twice: S + RA, S + SA + A + FA + A 
     514        # XXX are the packets also seen twice on non Linux systems ? 
     515        count=14 
     516        pl = sniff(iface=iface, filter='tcp and port ' + str(port), count = count, timeout=3) 
     517        map(addresult, map(packet2p0f, pl)) 
     518        os.waitpid(pid,0) 
     519    elif pid < 0: 
     520        log_runtime.error("fork error") 
     521    else: 
     522        # child: send 
     523        # XXX erk 
     524        time.sleep(1) 
     525        s1 = socket.socket(socket.AF_INET, type = socket.SOCK_STREAM) 
     526        # S & RA 
     527        try: 
     528            s1.connect(('127.0.0.1', port)) 
     529        except socket.error: 
     530            pass 
     531        # S, SA, A, FA, A 
     532        s1.bind(('127.0.0.1', port)) 
     533        s1.connect(('127.0.0.1', port)) 
     534        # howto: get an RST w/o ACK packet 
     535        s1.close() 
     536        os._exit(0) 
     537    return result 
     538