Ticket #76: patch.2

File patch.2, 17.4 KB (added by pierre@…, 2 years ago)

bugfixes, code cleaner, adds p0f_getlocalsigs()

Line 
1--- scapy.py.orig       2008-01-13 23:23:08.000000000 +0100
2+++ scapy.py    2008-01-13 23:20:41.000000000 +0100
3@@ -10693,8 +10693,6 @@ def locate_ip(ip):
4 # OS      - OS genre
5 # details - OS description
6 
7-
8-
9 class p0fKnowledgeBase(KnowledgeBase):
10     def __init__(self, filename):
11         KnowledgeBase.__init__(self, filename)
12@@ -10713,7 +10711,11 @@ class p0fKnowledgeBase(KnowledgeBase):
13                 l = tuple(l.split(":"))
14                 if len(l) < 8:
15                     continue
16-                li = map(int,l[1:4])
17+                def a2i(x):
18+                    if x.isdigit():
19+                        return int(x)
20+                    return x
21+                li = map(a2i, l[1:4])
22                 #if li[0] not in self.ttl_range:
23                 #    self.ttl_range.append(li[0])
24                 #    self.ttl_range.sort()
25@@ -10723,37 +10725,66 @@ class p0fKnowledgeBase(KnowledgeBase):
26             self.base = None
27         f.close()
28 
29+def p0f_selectdb(flags):
30+    # tested flags: S, R, A
31+    if flags & 0x16 == 0x2:
32+        # SYN
33+        return p0f_kdb
34+    elif flags & 0x16 == 0x12:
35+        # SYN/ACK
36+        return p0fa_kdb
37+    elif flags & 0x16 in [ 0x4, 0x14 ]:
38+        # RST RST/ACK
39+        return p0fr_kdb
40+    elif flags & 0x16 == 0x10:
41+        # ACK
42+        return p0fo_kdb
43+    else:
44+        return None
45 
46 def packet2p0f(pkt):
47+    pkt = pkt.copy()
48+    pkt = pkt.__class__(str(pkt))
49     while pkt.haslayer(IP) and pkt.haslayer(TCP):
50         pkt = pkt.getlayer(IP)
51         if isinstance(pkt.payload, TCP):
52             break
53         pkt = pkt.payload
54-
55+   
56     if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
57         raise TypeError("Not a TCP/IP packet")
58-    if pkt.payload.flags & 0x13 != 0x02: #S,!A,!F
59-        raise TypeError("Not a syn packet")
60+    #if pkt.payload.flags & 0x7 != 0x02: #S,!F,!R
61+    #    raise TypeError("Not a SYN or SYN/ACK packet")
62+   
63+    db = p0f_selectdb(pkt.payload.flags)
64     
65     #t = p0f_kdb.ttl_range[:]
66     #t += [pkt.ttl]
67     #t.sort()
68     #ttl=t[t.index(pkt.ttl)+1]
69     ttl = pkt.ttl
70-
71+   
72     df = (pkt.flags & 2) / 2
73     ss = len(pkt)
74     # from p0f/config.h : PACKET_BIG = 100
75     if ss > 100:
76-        ss = 0
77-
78+        if db == p0fr_kdb:
79+            # p0fr.fp: "Packet size may be wildcarded. The meaning of
80+            #           wildcard is, however, hardcoded as 'size >
81+            #           PACKET_BIG'"
82+            ss = '*'
83+        else:
84+            ss = 0
85+    if db == p0fo_kdb:
86+        # p0fo.fp: "Packet size MUST be wildcarded."
87+        ss = '*'
88+   
89     ooo = ""
90     mss = -1
91     qqT = False
92     qqP = False
93     #qqBroken = False
94-    ilen = (pkt[TCP].dataofs << 2) - 20 # from p0f.c
95+    ilen = (pkt.payload.dataofs << 2) - 20 # from p0f.c
96     for option in pkt.payload.options:
97         ilen -= 1
98         if option[0] == "MSS":
99@@ -10783,60 +10814,85 @@ def packet2p0f(pkt):
100             if ilen > 0:
101                 qqP = True
102         else:
103-            ooo += "?,"
104+            if type(option[0]) is str:
105+                ooo += "?%i," % TCPOptions[1][option[0]]
106+            else:
107+                ooo += "?%i," % option[0]
108             # FIXME: ilen
109     ooo = ooo[:-1]
110     if ooo == "": ooo = "."
111-
112+   
113     win = pkt.payload.window
114     if mss != -1:
115-        if win % mss == 0:
116+        if mss != 0 and win % mss == 0:
117             win = "S" + str(win/mss)
118         elif win % (mss + 40) == 0:
119             win = "T" + str(win/(mss+40))
120-        win = str(win)
121-
122+    win = str(win)
123+   
124     qq = ""
125-
126+   
127+    if db == p0fr_kdb:
128+        if pkt.payload.flags & 0x10 == 0x10:
129+            # p0fr.fp: "A new quirk, 'K', is introduced to denote
130+            #           RST+ACK packets"
131+            qq += "K"
132+    # The two next cases should also be only for p0f*r*, but although
133+    # it's not documented (or I have not noticed), p0f seems to
134+    # support the '0' and 'Q' quirks on any databases (or at the least
135+    # "classical" p0f.fp).
136+    if pkt.payload.seq == pkt.payload.ack:
137+        # p0fr.fp: "A new quirk, 'Q', is used to denote SEQ number
138+        #           equal to ACK number."
139+        qq += "Q"
140+    if pkt.payload.seq == 0:
141+        # p0fr.fp: "A new quirk, '0', is used to denote packets
142+        #           with SEQ number set to 0."
143+        qq += "0"
144     if qqP:
145         qq += "P"
146-    if pkt[IP].id == 0:
147+    if pkt.id == 0:
148         qq += "Z"
149-    if pkt[IP].options != '':
150+    if pkt.options != '':
151         qq += "I"
152-    if pkt[TCP].urgptr != 0:
153+    if pkt.payload.urgptr != 0:
154         qq += "U"
155-    if pkt[TCP].reserved != 0:
156+    if pkt.payload.reserved != 0:
157         qq += "X"
158-    if pkt[TCP].ack != 0:
159+    if pkt.payload.ack != 0:
160         qq += "A"
161     if qqT:
162         qq += "T"
163-    if pkt[TCP].flags & 40 != 0:
164-        # U or P
165-        qq += "F"
166-    if not isinstance(pkt[TCP].payload, NoPayload):
167+    if db == p0fo_kdb:
168+        if pkt.payload.flags & 0x20 != 0:
169+            # U
170+            # p0fo.fp: "PUSH flag is excluded from 'F' quirk checks"
171+            qq += "F"
172+    else:
173+        if pkt.payload.flags & 0x28 != 0:
174+            # U or P
175+            qq += "F"
176+    if db != p0fo_kdb and not isinstance(pkt.payload.payload, NoPayload):
177+        # p0fo.fp: "'D' quirk is not checked for."
178         qq += "D"
179-    # FIXME : "!" - broken options segment
180+    # FIXME : "!" - broken options segment: not handled yet
181 
182     if qq == "":
183         qq = "."
184 
185-    return (win,
186-            ttl,
187-            df,
188-            ss,
189-            ooo,
190-            qq)
191+    return (db, (win, ttl, df, ss, ooo, qq))
192 
193 def p0f_correl(x,y):
194     d = 0
195-    # wwww can be "*" or "%nn"
196+    # wwww can be "*" or "%nn". "Tnn" and "Snn" should work fine with
197+    # the x[0] == y[0] test.
198     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))
199     # ttl
200     d += (y[1] >= x[1] and y[1] - x[1] < 32)
201-    for i in [2, 3, 5]:
202-        d += (x[i] == y[i])
203+    for i in [2, 5]:
204+        d += (x[i] == y[i] or y[i] == '*')
205+    # '*' has a special meaning for ss
206+    d += x[3] == y[3]
207     xopt = x[4].split(",")
208     yopt = y[4].split(",")
209     if len(xopt) == len(yopt):
210@@ -10856,31 +10912,34 @@ def p0f_correl(x,y):
211 
212 
213 def p0f(pkt):
214-    """Passive OS fingerprinting: which OS emitted this TCP SYN ?
215+    """Passive OS fingerprinting: which OS emitted this TCP packet ?
216 p0f(packet) -> accuracy, [list of guesses]
217 """
218-    pb = p0f_kdb.get_base()
219+    db, sig = packet2p0f(pkt)
220+    if db:
221+        pb = db.get_base()
222+    else:
223+        pb = []
224     if not pb:
225         warning("p0f base empty.")
226         return []
227-    s = len(pb[0][0])
228+    #s = len(pb[0][0])
229     r = []
230-    sig = packet2p0f(pkt)
231     max = len(sig[4].split(",")) + 5
232     for b in pb:
233         d = p0f_correl(sig,b)
234         if d == max:
235             r.append((b[6], b[7], b[1] - pkt[IP].ttl))
236     return r
237-           
238 
239 def prnp0f(pkt):
240+    # we should print which DB we use
241     try:
242         r = p0f(pkt)
243     except:
244         return
245     if r == []:
246-        r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt))) + ":?:?]", None)
247+        r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt)[1])) + ":?:?]", None)
248     else:
249         r = r[0]
250     uptime = None
251@@ -10892,9 +10951,9 @@ def prnp0f(pkt):
252         uptime = None
253     res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1])
254     if uptime is not None:
255-        res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n  -> %IP.dst%:%TCP.dport%")
256+        res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n  -> %IP.dst%:%TCP.dport% (%TCP.flags%)")
257     else:
258-        res += pkt.sprintf("\n  -> %IP.dst%:%TCP.dport%")
259+        res += pkt.sprintf("\n  -> %IP.dst%:%TCP.dport% (%TCP.flags%)")
260     if r[2] is not None:
261         res += " (distance " + str(r[2]) + ")"
262     print res
263@@ -10918,6 +10977,221 @@ pkt2uptime(pkt, [HZ=100])"""
264     raise TypeError("No timestamp option")
265 
266 
267+def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
268+                    extrahops=0, mtu=1500, uptime=None):
269+    """Modifies pkt so that p0f will think it has been sent by a
270+specific OS.  If osdetails is None, then we randomly pick up a
271+personality matching osgenre. If osgenre and signature are also None,
272+we use a local signature (using p0f_getlocalsigs). If signature is
273+specified (as a tuple), we use the signature.
274+
275+For now, only TCP Syn packets are supported.
276+Some specifications of the p0f.fp file are not (yet) implemented."""
277+    pkt = pkt.copy()
278+    #pkt = pkt.__class__(str(pkt))
279+    while pkt.haslayer(IP) and pkt.haslayer(TCP):
280+        pkt = pkt.getlayer(IP)
281+        if isinstance(pkt.payload, TCP):
282+            break
283+        pkt = pkt.payload
284+   
285+    if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
286+        raise TypeError("Not a TCP/IP packet")
287+   
288+    if uptime is None:
289+        uptime = random.randint(120,100*60*60*24*365)
290+   
291+    db = p0f_selectdb(pkt.payload.flags)
292+    if osgenre:
293+        pb = db.get_base()
294+        if pb is None:
295+            pb = []
296+        pb = filter(lambda x: x[6] == osgenre, pb)
297+        if osdetails:
298+            pb = filter(lambda x: x[7] == osdetails, pb)
299+    elif signature:
300+        pb = [signature]
301+    else:
302+        pb = p0f_getlocalsigs()[db]
303+    if db == p0fr_kdb:
304+        # 'K' quirk <=> RST+ACK
305+        if pkt.payload.flags & 0x4 == 0x4:
306+            pb = filter(lambda x: 'K' in x[5], pb)
307+        else:
308+            pb = filter(lambda x: 'K' not in x[5], pb)
309+    if not pb:
310+        raise Scapy_Exception("No match in the p0f database")
311+    pers = pb[random.randint(0, len(pb) - 1)]
312+   
313+    # options (we start with options because of MSS)
314+    ## TODO: let the options already set if they are valid
315+    options = []
316+    if pers[4] != '.':
317+        for opt in pers[4].split(','):
318+            if opt[0] == 'M':
319+                # MSS might have a maximum size because of window size
320+                # specification
321+                if pers[0][0] == 'S':
322+                    maxmss = (2L**16-1) / int(pers[0][1:])
323+                else:
324+                    maxmss = (2L**16-1)
325+                # If we have to randomly pick up a value, we cannot use
326+                # scapy RandXXX() functions, because the value has to be
327+                # set in case we need it for the window size value. That's
328+                # why we use random.randint()
329+                if opt[1:] == '*':
330+                    options.append(('MSS', random.randint(1,maxmss)))
331+                elif opt[1] == '%':
332+                    coef = int(opt[2:])
333+                    options.append(('MSS', coef*random.randint(1,maxmss/coef)))
334+                else:
335+                    options.append(('MSS', int(opt[1:])))
336+            elif opt[0] == 'W':
337+                if opt[1:] == '*':
338+                    options.append(('WScale', RandByte()))
339+                elif opt[1] == '%':
340+                    coef = int(opt[2:])
341+                    options.append(('WScale', coef*RandNum(min=1,
342+                                                           max=(2L**8-1)/coef)))
343+                else:
344+                    options.append(('WScale', int(opt[1:])))
345+            elif opt == 'T0':
346+                options.append(('Timestamp', (0, 0)))
347+            elif opt == 'T':
348+                if 'T' in pers[5]:
349+                    # FIXME: RandInt() here does not work (bug (?) in
350+                    # TCPOptionsField.m2i often raises "OverflowError:
351+                    # long int too large to convert to int" in:
352+                    #    oval = struct.pack(ofmt, *oval)"
353+                    # Actually, this is enough to often raise the error:
354+                    #    struct.pack('I', RandInt())
355+                    options.append(('Timestamp', (uptime, random.randint(1,2**32-1))))
356+                else:
357+                    options.append(('Timestamp', (uptime, 0)))
358+            elif opt == 'S':
359+                options.append(('SAckOK', ''))
360+            elif opt == 'N':
361+                options.append(('NOP', None))
362+            elif opt == 'E':
363+                options.append(('EOL', None))
364+            elif opt[0] == '?':
365+                if int(opt[1:]) in TCPOptions[0]:
366+                    optname = TCPOptions[0][int(opt[1:])][0]
367+                    optstruct = TCPOptions[0][int(opt[1:])][1]
368+                    options.append((optname,
369+                                    struct.unpack(optstruct,
370+                                                  RandString(struct.calcsize(optstruct))._fix())))
371+                else:
372+                    options.append((int(opt[1:]), ''))
373+            ## FIXME: qqP not handled
374+            else:
375+                warning("unhandled TCP option " + opt)
376+            pkt.payload.options = options
377+   
378+    # window size
379+    if pers[0] == '*':
380+        pkt.payload.window = RandShort()
381+    elif pers[0].isdigit():
382+        pkt.payload.window = int(pers[0])
383+    elif pers[0][0] == '%':
384+        coef = int(pers[0][1:])
385+        pkt.payload.window = coef * RandNum(min=1,max=(2L**16-1)/coef)
386+    elif pers[0][0] == 'T':
387+        pkt.payload.window = mtu * int(pers[0][1:])
388+    elif pers[0][0] == 'S':
389+        ## needs MSS set
390+        MSS = filter(lambda x: x[0] == 'MSS', options)
391+        if not filter(lambda x: x[0] == 'MSS', options):
392+            raise Scapy_Exception("TCP window value requires MSS, and MSS option not set")
393+        pkt.payload.window = filter(lambda x: x[0] == 'MSS', options)[0][1] * int(pers[0][1:])
394+    else:
395+        raise Scapy_Exception('Unhandled window size specification')
396+   
397+    # ttl
398+    pkt.ttl = pers[1]-extrahops
399+    # DF flag
400+    pkt.flags |= (2 * pers[2])
401+    ## FIXME: ss (packet size) not handled (how ? may be with D quirk
402+    ## if present)
403+    # Quirks
404+    if pers[5] != '.':
405+        for qq in pers[5]:
406+            ## FIXME: not handled: P, I, X, !
407+            # T handled with the Timestamp option
408+            if qq == 'Z': pkt.id = 0
409+            elif qq == 'U': pkt.payload.urgptr = RandShort()
410+            elif qq == 'A': pkt.payload.ack = RandInt()
411+            elif qq == 'F':
412+                if db == p0fo_kdb:
413+                    pkt.payload.flags |= 0x20 # U
414+                else:
415+                    pkt.payload.flags |= RandChoice(8, 32, 40) #P / U / PU
416+            elif qq == 'D' and db != p0fo_kdb:
417+                pkt /= Raw(load=RandString(random.randint(1, 10))) # XXX p0fo.fp
418+            elif qq == 'Q': pkt.payload.seq = pkt.payload.ack
419+            #elif qq == '0': pkt.payload.seq = 0
420+        #if db == p0fr_kdb:
421+        # '0' quirk is actually not only for p0fr.fp (see
422+        # packet2p0f())
423+    if '0' in pers[5]:
424+        pkt.payload.seq = 0
425+    elif pkt.payload.seq == 0:
426+        pkt.payload.seq = RandInt()
427+   
428+    while pkt.underlayer:
429+        pkt = pkt.underlayer
430+    return pkt
431+
432+def p0f_getlocalsigs():
433+    """This function returns a dictionary of signatures indexed by p0f
434+db (e.g., p0f_kdb, p0fa_kdb, ...) for the local TCP/IP stack.
435+
436+You need to have your firewall at least accepting the TCP packets
437+from/to a high port (30000 <= x <= 40000) on your loopback interface.
438+
439+Please note that the generated signatures come from the loopback
440+interface and may (are likely to) be different than those generated on
441+"normal" interfaces."""
442+    pid = os.fork()
443+    port = random.randint(30000, 40000)
444+    if pid > 0:
445+        # parent: sniff
446+        result = {}
447+        def addresult(res):
448+            # TODO: wildcard window size in some cases? and maybe some
449+            # other values?
450+            if res[0] not in result:
451+                result[res[0]] = [res[1]]
452+            else:
453+                if res[1] not in result[res[0]]:
454+                    result[res[0]].append([res[1]])
455+        # XXX could we try with a "normal" interface using other hosts
456+        iface = conf.route.route('127.0.0.1')[0]
457+        # each packet is seen twice: S + RA, S + SA + A + FA + A
458+        # XXX are the packets also seen twice on non Linux systems ?
459+        count=14
460+        pl = sniff(iface=iface, filter='tcp and port ' + str(port), count = count, timeout=3)
461+        map(addresult, map(packet2p0f, pl))
462+        os.waitpid(pid,0)
463+    elif pid < 0:
464+        log_runtime.error("fork error")
465+    else:
466+        # child: send
467+        # XXX erk
468+        time.sleep(1)
469+        s1 = socket.socket(socket.AF_INET, type = socket.SOCK_STREAM)
470+        # S & RA
471+        try:
472+            s1.connect(('127.0.0.1', port))
473+        except socket.error:
474+            pass
475+        # S, SA, A, FA, A
476+        s1.bind(('127.0.0.1', port))
477+        s1.connect(('127.0.0.1', port))
478+        # howto: get an RST w/o ACK packet
479+        s1.close()
480+        os._exit(0)
481+    return result
482 
483 #################
484 ## Queso stuff ##
485@@ -13236,6 +13510,9 @@ extensions_paths: path or list of paths
486     histfile = os.path.join(os.environ["HOME"], ".scapy_history")
487     padding = 1
488     p0f_base ="/etc/p0f/p0f.fp"
489+    p0fa_base ="/etc/p0f/p0fa.fp"
490+    p0fr_base ="/etc/p0f/p0fr.fp"
491+    p0fo_base ="/etc/p0f/p0fo.fp"
492     queso_base ="/etc/queso.conf"
493     nmap_base ="/usr/share/nmap/nmap-os-fingerprints"
494     IPCountry_base = "GeoIPCountry4Scapy.gz"
495@@ -13278,6 +13555,9 @@ if PCAP:
496 
497 
498 p0f_kdb = p0fKnowledgeBase(conf.p0f_base)
499+p0fa_kdb = p0fKnowledgeBase(conf.p0fa_base)
500+p0fr_kdb = p0fKnowledgeBase(conf.p0fr_base)
501+p0fo_kdb = p0fKnowledgeBase(conf.p0fo_base)
502 queso_kdb = QuesoKnowledgeBase(conf.queso_base)
503 nmap_kdb = NmapKnowledgeBase(conf.nmap_base)
504 IP_country_kdb = IPCountryKnowledgeBase(conf.IPCountry_base)