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