Ticket #76: patch.2

File patch.2, 17.4 kB (added by pierre@droids-corp.org, 10 months 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)