1# SPDX-License-Identifier: GPL-2.0-only 2# This file is part of Scapy 3# See https://scapy.net/ for more information 4# Copyright (C) Philippe Biondi <phil@secdev.org> 5 6""" 7Clone of p0f v3 passive OS fingerprinting 8""" 9 10import re 11import struct 12import random 13 14from scapy.data import KnowledgeBase, select_path 15from scapy.config import conf 16from scapy.compat import raw, orb 17from scapy.packet import NoPayload 18from scapy.layers.inet import IP, TCP, TCPOptions 19from scapy.layers.http import HTTP, HTTPRequest, HTTPResponse 20from scapy.layers.inet6 import IPv6 21from scapy.volatile import RandByte, RandShort, RandString 22from scapy.error import warning 23 24_p0fpaths = ["/etc/p0f", "/usr/share/p0f", "/opt/local"] 25conf.p0f_base = select_path(_p0fpaths, "p0f.fp") 26 27MIN_TCP4 = 40 # Min size of IPv4/TCP headers 28MIN_TCP6 = 60 # Min size of IPv6/TCP headers 29MAX_DIST = 35 # Maximum TTL distance for non-fuzzy signature matching 30 31WIN_TYPE_NORMAL = 0 # Literal value 32WIN_TYPE_ANY = 1 # Wildcard 33WIN_TYPE_MOD = 2 # Modulo check 34WIN_TYPE_MSS = 3 # Window size MSS multiplier 35WIN_TYPE_MTU = 4 # Window size MTU multiplier 36 37# Convert TCP option num to p0f (nop is handled separately) 38tcp_options_p0f = { 39 2: "mss", # maximum segment size 40 3: "ws", # window scaling 41 4: "sok", # selective ACK permitted 42 5: "sack", # selective ACK (should not be seen) 43 8: "ts", # timestamp 44} 45 46 47# Signatures 48class TCP_Signature(object): 49 __slots__ = ["olayout", "quirks", "ip_opt_len", "ip_ver", "ttl", 50 "mss", "win", "win_type", "wscale", "pay_class", "ts1"] 51 52 def __init__(self, olayout, quirks, ip_opt_len, ip_ver, ttl, 53 mss, win, win_type, wscale, pay_class, ts1): 54 self.olayout = olayout 55 self.quirks = quirks 56 self.ip_opt_len = ip_opt_len 57 self.ip_ver = ip_ver 58 self.ttl = ttl 59 self.mss = mss 60 self.win = win 61 self.win_type = win_type # None for packet signatures 62 self.wscale = wscale 63 self.pay_class = pay_class 64 self.ts1 = ts1 # None for base signatures 65 66 @classmethod 67 def from_packet(cls, pkt): 68 """ 69 Receives a TCP packet (assuming it's valid), and returns 70 a TCP_Signature object 71 """ 72 ip_ver = pkt.version 73 quirks = set() 74 75 def addq(name): 76 quirks.add(name) 77 78 # IPv4/IPv6 parsing 79 if ip_ver == 4: 80 ttl = pkt.ttl 81 ip_opt_len = (pkt.ihl * 4) - 20 82 if pkt.tos & (0x01 | 0x02): 83 addq("ecn") 84 if pkt.flags.evil: 85 addq("0+") 86 if pkt.flags.DF: 87 addq("df") 88 if pkt.id: 89 addq("id+") 90 elif pkt.id == 0: 91 addq("id-") 92 else: 93 ttl = pkt.hlim 94 ip_opt_len = 0 95 if pkt.fl: 96 addq("flow") 97 if pkt.tc & (0x01 | 0x02): 98 addq("ecn") 99 100 # TCP parsing 101 tcp = pkt[TCP] 102 win = tcp.window 103 if tcp.flags & (0x40 | 0x80 | 0x01): 104 addq("ecn") 105 if tcp.seq == 0: 106 addq("seq-") 107 if tcp.flags.A: 108 if tcp.ack == 0: 109 addq("ack-") 110 elif tcp.ack: 111 addq("ack+") 112 if tcp.flags.U: 113 addq("urgf+") 114 elif tcp.urgptr: 115 addq("uptr+") 116 if tcp.flags.P: 117 addq("pushf+") 118 119 pay_class = 1 if tcp.payload else 0 120 121 # Manual TCP options parsing 122 mss = 0 123 wscale = 0 124 ts1 = 0 125 olayout = "" 126 optlen = (tcp.dataofs << 2) - 20 127 x = raw(tcp)[-optlen:] # raw bytes of TCP options 128 while x: 129 onum = orb(x[0]) 130 if onum == 0: 131 x = x[1:] 132 olayout += "eol+%i," % len(x) 133 if x.strip(b"\x00"): # non-zero past EOL 134 addq("opt+") 135 break 136 if onum == 1: 137 x = x[1:] 138 olayout += "nop," 139 continue 140 try: 141 olen = orb(x[1]) 142 except IndexError: # no room for length field 143 addq("bad") 144 break 145 oval = x[2:olen] 146 if onum in tcp_options_p0f: 147 ofmt = TCPOptions[0][onum][1] 148 olayout += "%s," % tcp_options_p0f[onum] 149 optsize = 2 + struct.calcsize(ofmt) if ofmt else 2 # total len 150 if len(x) < optsize: # option would end past end of header 151 addq("bad") 152 break 153 154 if onum == 5: 155 if olen < 10 or olen > 34: # SACK length out of range 156 addq("bad") 157 break 158 else: 159 if olen != optsize: # length field doesn't fit option type 160 addq("bad") 161 break 162 if ofmt: 163 oval = struct.unpack(ofmt, oval) 164 if len(oval) == 1: 165 oval = oval[0] 166 if onum == 2: 167 mss = oval 168 elif onum == 3: 169 wscale = oval 170 if wscale > 14: 171 addq("exws") 172 elif onum == 8: 173 ts1 = oval[0] 174 if not ts1: 175 addq("ts1-") 176 if oval[1] and (tcp.flags.S and not tcp.flags.A): 177 addq("ts2+") 178 else: # Unknown option, presumably with specified size 179 if olen < 2 or olen > 40 or olen > len(x): 180 addq("bad") 181 break 182 x = x[olen:] 183 olayout = olayout[:-1] 184 185 return cls(olayout, quirks, ip_opt_len, ip_ver, ttl, mss, win, None, wscale, pay_class, ts1) # noqa: E501 186 187 @classmethod 188 def from_raw_sig(cls, sig_line): 189 """ 190 Parses a TCP sig line and returns a tuple consisting of a 191 TCP_Signature object and bad_ttl as bool 192 """ 193 ver, ttl, olen, mss, wsize, olayout, quirks, pclass = lparse(sig_line, 8) # noqa: E501 194 wsize, _, scale = wsize.partition(",") 195 196 ip_ver = -1 if ver == "*" else int(ver) 197 ttl, bad_ttl = (int(ttl[:-1]), True) if ttl[-1] == "-" else (int(ttl), False) # noqa: E501 198 ip_opt_len = int(olen) 199 mss = -1 if mss == "*" else int(mss) 200 if wsize == "*": 201 win, win_type = (0, WIN_TYPE_ANY) 202 elif wsize[:3] == "mss": 203 win, win_type = (int(wsize[4:]), WIN_TYPE_MSS) 204 elif wsize[0] == "%": 205 win, win_type = (int(wsize[1:]), WIN_TYPE_MOD) 206 elif wsize[:3] == "mtu": 207 win, win_type = (int(wsize[4:]), WIN_TYPE_MTU) 208 else: 209 win, win_type = (int(wsize), WIN_TYPE_NORMAL) 210 wscale = -1 if scale == "*" else int(scale) 211 if quirks: 212 quirks = frozenset(q for q in quirks.split(",")) 213 else: 214 quirks = frozenset() 215 pay_class = -1 if pclass == "*" else int(pclass == "+") 216 217 sig = cls(olayout, quirks, ip_opt_len, ip_ver, ttl, mss, win, win_type, wscale, pay_class, None) # noqa: E501 218 return sig, bad_ttl 219 220 def __str__(self): 221 quirks = ",".join(q for q in self.quirks) 222 fmt = "%i:%i+%i:%i:%i:%i,%i:%s:%s:%i" 223 s = fmt % (self.ip_ver, self.ttl, guess_dist(self.ttl), 224 self.ip_opt_len, self.mss, self.win, self.wscale, 225 self.olayout, quirks, self.pay_class) 226 return s 227 228 229class HTTP_Signature(object): 230 __slots__ = ["http_ver", "hdr", "hdr_set", "habsent", "sw"] 231 232 def __init__(self, http_ver, hdr, hdr_set, habsent, sw): 233 self.http_ver = http_ver 234 self.hdr = hdr 235 self.hdr_set = hdr_set 236 self.habsent = habsent # None for packet signatures 237 self.sw = sw 238 239 @classmethod 240 def from_packet(cls, pkt): 241 """ 242 Receives an HTTP packet (assuming it's valid), and returns 243 a HTTP_Signature object 244 """ 245 http_payload = raw(pkt[TCP].payload) 246 247 crlfcrlf = b"\r\n\r\n" 248 crlfcrlfIndex = http_payload.find(crlfcrlf) 249 if crlfcrlfIndex != -1: 250 headers = http_payload[:crlfcrlfIndex + len(crlfcrlf)] 251 else: 252 headers = http_payload 253 headers = headers.decode() # XXX: Check if this could fail 254 first_line, headers = headers.split("\r\n", 1) 255 256 if "1.0" in first_line: 257 http_ver = 0 258 elif "1.1" in first_line: 259 http_ver = 1 260 else: 261 raise ValueError("HTTP version is not 1.0/1.1") 262 263 sw = "" 264 headers_found = [] 265 hdr_set = set() 266 for header_line in headers.split("\r\n"): 267 name, _, value = header_line.partition(":") 268 if value: 269 value = value.strip() 270 headers_found.append((name, value)) 271 hdr_set.add(name) 272 if name in ("User-Agent", "Server"): 273 sw = value 274 hdr = tuple(headers_found) 275 return cls(http_ver, hdr, hdr_set, None, sw) 276 277 @classmethod 278 def from_raw_sig(cls, sig_line): 279 """ 280 Parses an HTTP sig line and returns a HTTP_Signature object 281 """ 282 ver, horder, habsent, expsw = lparse(sig_line, 4) 283 http_ver = -1 if ver == "*" else int(ver) 284 285 # horder parsing - split by commas that aren't in [] 286 new_horder = [] 287 for header in re.split(r",(?![^\[]*\])", horder): 288 name, _, value = header.partition("=") 289 if name[0] == "?": # Optional header 290 new_horder.append((name[1:], value[1:-1], True)) 291 else: 292 new_horder.append((name, value[1:-1], False)) 293 hdr = tuple(new_horder) 294 hdr_set = frozenset(header[0] for header in hdr if not header[2]) 295 habsent = frozenset(habsent.split(",")) 296 return cls(http_ver, hdr, hdr_set, habsent, expsw) 297 298 def __str__(self): 299 # values that depend on the context are not included in the string 300 skipval = ("Host", "User-Agent", "Date", "Content-Type", "Server") 301 hdr = ",".join(n if n in skipval else "%s=[%s]" % (n, v) for n, v in self.hdr) # noqa: E501 302 fmt = "%i:%s::%s" 303 s = fmt % (self.http_ver, hdr, self.sw) 304 return s 305 306 307# Records 308class MTU_Record(object): 309 __slots__ = ["label_id", "mtu"] 310 311 def __init__(self, label_id, sig_line): 312 self.label_id = label_id 313 self.mtu = int(sig_line) 314 315 316class TCP_Record(object): 317 __slots__ = ["label_id", "bad_ttl", "sig"] 318 319 def __init__(self, label_id, sig_line): 320 self.label_id = label_id 321 sig, bad_ttl = TCP_Signature.from_raw_sig(sig_line) 322 self.bad_ttl = bad_ttl 323 self.sig = sig 324 325 326class HTTP_Record(object): 327 __slots__ = ["label_id", "sig"] 328 329 def __init__(self, label_id, sig_line): 330 self.label_id = label_id 331 self.sig = HTTP_Signature.from_raw_sig(sig_line) 332 333 334class p0fKnowledgeBase(KnowledgeBase): 335 """ 336 self.base = { 337 "mtu" (str): [sig(tuple), ...] 338 "tcp"/"http" (str): { 339 direction (str): [sig(tuple), ...] 340 } 341 } 342 self.labels = (label(tuple), ...) 343 """ 344 def lazy_init(self): 345 try: 346 f = open(self.filename) 347 except Exception: 348 warning("Can't open base %s", self.filename) 349 return 350 351 self.base = {} 352 self.labels = [] 353 self._parse_file(f) 354 self.labels = tuple(self.labels) 355 f.close() 356 357 def _parse_file(self, file): 358 """ 359 Parses p0f.fp file and stores the data with described structures. 360 """ 361 label_id = -1 362 363 for line in file: 364 if line[0] in (";", "\n"): 365 continue 366 line = line.strip() 367 368 if line[0] == "[": 369 section, direction = lparse(line[1:-1], 2) 370 if section == "mtu": 371 self.base[section] = [] 372 curr_records = self.base[section] 373 else: 374 if section not in self.base: 375 self.base[section] = {direction: []} 376 elif direction not in self.base[section]: 377 self.base[section][direction] = [] 378 curr_records = self.base[section][direction] 379 else: 380 param, _, val = line.partition(" = ") 381 param = param.strip() 382 383 if param == "sig": 384 if section == "mtu": 385 record_class = MTU_Record 386 elif section == "tcp": 387 record_class = TCP_Record 388 elif section == "http": 389 record_class = HTTP_Record 390 curr_records.append(record_class(label_id, val)) 391 392 elif param == "label": 393 label_id += 1 394 if section == "mtu": 395 self.labels.append(val) 396 continue 397 # label = type:class:name:flavor 398 t, c, name, flavor = lparse(val, 4) 399 self.labels.append((t, c, name, flavor)) 400 401 elif param == "sys": 402 sys_names = tuple(name for name in val.split(",")) 403 self.labels[label_id] += (sys_names,) 404 405 def get_sigs_by_os(self, direction, osgenre, osdetails=None): 406 """Get TCP signatures that match an OS genre and details (if specified). 407 If osdetails isn't specified, then we pick all signatures 408 that match osgenre. 409 410 Examples: 411 >>> p0fdb.get_sigs_by_os("request", "Linux", "2.6") 412 >>> p0fdb.get_sigs_by_os("response", "Windows", "8") 413 >>> p0fdb.get_sigs_by_os("request", "FreeBSD") 414 """ 415 sigs = [] 416 for tcp_record in self.base["tcp"][direction]: 417 label = self.labels[tcp_record.label_id] 418 name, flavor = label[2], label[3] 419 if osgenre and osgenre == name: 420 if osdetails: 421 if osdetails in flavor: 422 sigs.append(tcp_record.sig) 423 else: 424 sigs.append(tcp_record.sig) 425 return sigs 426 427 def tcp_find_match(self, ts, direction): 428 """ 429 Finds the best match for the given signature and direction. 430 If a match is found, returns a tuple consisting of: 431 - label: the matched label 432 - dist: guessed distance from the packet source 433 - fuzzy: whether the match is fuzzy 434 Returns None if no match was found 435 """ 436 win_multi, use_mtu = detect_win_multi(ts) 437 438 gmatch = None # generic match 439 fmatch = None # fuzzy match 440 for tcp_record in self.base["tcp"][direction]: 441 rs = tcp_record.sig 442 443 fuzzy = False 444 ref_quirks = rs.quirks 445 446 if rs.olayout != ts.olayout: 447 continue 448 449 if rs.ip_ver == -1: 450 ref_quirks -= {"flow"} if ts.ip_ver == 4 else {"df", "id+", "id-"} # noqa: E501 451 452 if ref_quirks != ts.quirks: 453 deleted = (ref_quirks ^ ts.quirks) & ref_quirks 454 added = (ref_quirks ^ ts.quirks) & ts.quirks 455 456 if (fmatch or (deleted - {"df", "id+"}) or (added - {"id-", "ecn"})): # noqa: E501 457 continue 458 fuzzy = True 459 460 if rs.ip_opt_len != ts.ip_opt_len: 461 continue 462 if tcp_record.bad_ttl: 463 if rs.ttl < ts.ttl: 464 continue 465 else: 466 if rs.ttl < ts.ttl or rs.ttl - ts.ttl > MAX_DIST: 467 fuzzy = True 468 469 if ((rs.mss != -1 and rs.mss != ts.mss) or 470 (rs.wscale != -1 and rs.wscale != ts.wscale) or 471 (rs.pay_class != -1 and rs.pay_class != ts.pay_class)): 472 continue 473 474 if rs.win_type == WIN_TYPE_NORMAL: 475 if rs.win != ts.win: 476 continue 477 elif rs.win_type == WIN_TYPE_MOD: 478 if ts.win % rs.win: 479 continue 480 elif rs.win_type == WIN_TYPE_MSS: 481 if (use_mtu or rs.win != win_multi): 482 continue 483 elif rs.win_type == WIN_TYPE_MTU: 484 if (not use_mtu or rs.win != win_multi): 485 continue 486 487 # Got a match? If not fuzzy, return. If fuzzy, keep looking. 488 label = self.labels[tcp_record.label_id] 489 match = (label, rs.ttl - ts.ttl, fuzzy) 490 if not fuzzy: 491 if label[0] == "s": 492 return match 493 elif not gmatch: 494 gmatch = match 495 elif not fmatch: 496 fmatch = match 497 498 if gmatch: 499 return gmatch 500 if fmatch: 501 return fmatch 502 return None 503 504 def http_find_match(self, ts, direction): 505 """ 506 Finds the best match for the given signature and direction. 507 If a match is found, returns a tuple consisting of: 508 - label: the matched label 509 - dishonest: whether the software was detected as dishonest 510 Returns None if no match was found 511 """ 512 gmatch = None # generic match 513 for http_record in self.base["http"][direction]: 514 rs = http_record.sig 515 516 if rs.http_ver != -1 and rs.http_ver != ts.http_ver: 517 continue 518 519 # Check that all non-optional headers appear in the packet 520 if not (ts.hdr_set & rs.hdr_set) == rs.hdr_set: 521 continue 522 523 # Check that no forbidden headers appear in the packet. 524 if len(rs.habsent & ts.hdr_set) > 0: 525 continue 526 527 def headers_correl(): 528 phi = 0 # Packet HTTP header index 529 hdr_len = len(ts.hdr) 530 531 # Confirm the ordering and values of headers 532 # (this is relatively slow, hence the if statements above). 533 # The algorithm is derived from the original p0f/fp_http.c 534 for kh in rs.hdr: 535 orig_phi = phi 536 while (phi < hdr_len and 537 kh[0] != ts.hdr[phi][0]): 538 phi += 1 539 540 if phi == hdr_len: 541 if not kh[2]: 542 return False 543 544 for ph in ts.hdr: 545 if kh[0] == ph[0]: 546 return False 547 548 phi = orig_phi 549 continue 550 551 if kh[1] not in ts.hdr[phi][1]: 552 return False 553 phi += 1 554 return True 555 556 if not headers_correl(): 557 continue 558 559 # Got a match 560 label = self.labels[http_record.label_id] 561 dishonest = rs.sw and ts.sw and rs.sw not in ts.sw 562 match = (label, dishonest) 563 if label[0] == "s": 564 return match 565 elif not gmatch: 566 gmatch = match 567 return gmatch if gmatch else None 568 569 def mtu_find_match(self, mtu): 570 """ 571 Finds a match for the given MTU. 572 If a match is found, returns the label string. 573 Returns None if no match was found 574 """ 575 for mtu_record in self.base["mtu"]: 576 if mtu == mtu_record.mtu: 577 return self.labels[mtu_record.label_id] 578 return None 579 580 581p0fdb = p0fKnowledgeBase(conf.p0f_base) 582 583 584def guess_dist(ttl): 585 for ottl in (32, 64, 128, 255): 586 if ttl <= ottl: 587 return ottl - ttl 588 589 590def lparse(line, n, delimiter=":", default=""): 591 """ 592 Parsing of 'a:b:c:d:e' lines 593 """ 594 a = line.split(delimiter)[:n] 595 for elt in a: 596 yield elt 597 for _ in range(n - len(a)): 598 yield default 599 600 601def validate_packet(pkt): 602 """ 603 Validate that the packet is an IPv4/IPv6 and TCP packet. 604 If the packet is valid, a copy is returned. If not, TypeError is raised. 605 """ 606 pkt = pkt.copy() 607 valid = pkt.haslayer(TCP) and (pkt.haslayer(IP) or pkt.haslayer(IPv6)) 608 if not valid: 609 raise TypeError("Not a TCP/IP packet") 610 return pkt 611 612 613def detect_win_multi(ts): 614 """ 615 Figure out if window size is a multiplier of MSS or MTU. 616 Receives a TCP signature and returns the multiplier and 617 whether mtu should be used 618 """ 619 mss = ts.mss 620 win = ts.win 621 if not win or mss < 100: 622 return -1, False 623 624 options = [ 625 (mss, False), 626 (1500 - MIN_TCP4, False), 627 (1500 - MIN_TCP4 - 12, False), 628 (mss + MIN_TCP4, True), 629 (1500, True) 630 ] 631 if ts.ts1: 632 options.append((mss - 12, False)) 633 if ts.ip_ver == 6: 634 options.append((1500 - MIN_TCP6, False)) 635 options.append((1500 - MIN_TCP6 - 12, False)) 636 options.append((mss + MIN_TCP6, True)) 637 638 for div, use_mtu in options: 639 if not win % div: 640 return win / div, use_mtu 641 return -1, False 642 643 644def packet2p0f(pkt): 645 """ 646 Returns a p0f signature of the packet, and the direction. 647 Raises TypeError if the packet isn't valid for p0f 648 """ 649 pkt = validate_packet(pkt) 650 pkt = pkt.__class__(raw(pkt)) 651 652 if pkt[TCP].flags.S: 653 if pkt[TCP].flags.A: 654 direction = "response" 655 else: 656 direction = "request" 657 sig = TCP_Signature.from_packet(pkt) 658 659 elif pkt[TCP].payload: 660 # XXX: guess_payload_class doesn't use any class related attributes 661 pclass = HTTP().guess_payload_class(raw(pkt[TCP].payload)) 662 if pclass == HTTPRequest: 663 direction = "request" 664 elif pclass == HTTPResponse: 665 direction = "response" 666 else: 667 raise TypeError("Not an HTTP payload") 668 sig = HTTP_Signature.from_packet(pkt) 669 else: 670 raise TypeError("Not a SYN, SYN/ACK, or HTTP packet") 671 return sig, direction 672 673 674def fingerprint_mtu(pkt): 675 """ 676 Fingerprints the MTU based on the maximum segment size specified 677 in TCP options. 678 If a match was found, returns the label. If not returns None 679 """ 680 pkt = validate_packet(pkt) 681 mss = 0 682 for name, value in pkt.payload.options: 683 if name == "MSS": 684 mss = value 685 686 if not mss: 687 return None 688 689 mtu = (mss + MIN_TCP4) if pkt.version == 4 else (mss + MIN_TCP6) 690 691 if not p0fdb.get_base(): 692 warning("p0f base empty.") 693 return None 694 695 return p0fdb.mtu_find_match(mtu) 696 697 698def p0f(pkt): 699 sig, direction = packet2p0f(pkt) 700 if not p0fdb.get_base(): 701 warning("p0f base empty.") 702 return None 703 704 if isinstance(sig, TCP_Signature): 705 return p0fdb.tcp_find_match(sig, direction) 706 else: 707 return p0fdb.http_find_match(sig, direction) 708 709 710def prnp0f(pkt): 711 """Calls p0f and prints a user-friendly output""" 712 try: 713 r = p0f(pkt) 714 except Exception: 715 return 716 717 sig, direction = packet2p0f(pkt) 718 is_tcp_sig = isinstance(sig, TCP_Signature) 719 to_server = direction == "request" 720 721 if is_tcp_sig: 722 pkt_type = "SYN" if to_server else "SYN+ACK" 723 else: 724 pkt_type = "HTTP Request" if to_server else "HTTP Response" 725 726 res = pkt.sprintf(".-[ %IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport% (" + pkt_type + ") ]-\n|\n") # noqa: E501 727 fields = [] 728 729 def add_field(name, value): 730 fields.append("| %-8s = %s\n" % (name, value)) 731 732 cli_or_svr = "Client" if to_server else "Server" 733 add_field(cli_or_svr, pkt.sprintf("%IP.src%:%TCP.sport%")) 734 735 if r: 736 label = r[0] 737 app_or_os = "App" if label[1] == "!" else "OS" 738 add_field(app_or_os, label[2] + " " + label[3]) 739 if len(label) == 5: # label includes sys 740 add_field("Sys", ", ".join(name for name in label[4])) 741 if is_tcp_sig: 742 add_field("Distance", r[1]) 743 else: 744 app_or_os = "OS" if is_tcp_sig else "App" 745 add_field(app_or_os, "UNKNOWN") 746 747 add_field("Raw sig", str(sig)) 748 749 res += "".join(fields) 750 res += "`____\n" 751 print(res) 752 753 754def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, 755 extrahops=0, mtu=1500, uptime=None): 756 """Modifies pkt so that p0f will think it has been sent by a 757 specific OS. Either osgenre or signature is required to impersonate. 758 If signature is specified (as a raw string), we use the signature. 759 signature format: 760 "ip_ver:ttl:ip_opt_len:mss:window,wscale:opt_layout:quirks:pay_class" 761 762 If osgenre is specified, we randomly pick a signature with a label 763 that matches osgenre (and osdetails, if specified). 764 Note: osgenre is case sensitive ("linux" -> "Linux" etc.), and osdetails 765 is a substring of a label flavor ("7", "8" and "7 or 8" will 766 all match the label "s:win:Windows:7 or 8") 767 768 For now, only TCP SYN/SYN+ACK packets are supported.""" 769 pkt = validate_packet(pkt) 770 771 if not osgenre and not signature: 772 raise ValueError("osgenre or signature is required to impersonate!") 773 774 tcp = pkt[TCP] 775 tcp_type = tcp.flags & (0x02 | 0x10) # SYN / SYN+ACK 776 777 if signature: 778 if isinstance(signature, str): 779 sig, _ = TCP_Signature.from_raw_sig(signature) 780 else: 781 raise TypeError("Unsupported signature type") 782 else: 783 if not p0fdb.get_base(): 784 sigs = [] 785 else: 786 direction = "request" if tcp_type == 0x02 else "response" 787 sigs = p0fdb.get_sigs_by_os(direction, osgenre, osdetails) 788 789 # If IPv6 packet, remove IPv4-only signatures and vice versa 790 sigs = [s for s in sigs if s.ip_ver == -1 or s.ip_ver == pkt.version] 791 if not sigs: 792 raise ValueError("No match in the p0f database") 793 sig = random.choice(sigs) 794 795 if sig.ip_ver != -1 and pkt.version != sig.ip_ver: 796 raise ValueError("Can't convert between IPv4 and IPv6") 797 798 quirks = sig.quirks 799 800 if pkt.version == 4: 801 pkt.ttl = sig.ttl - extrahops 802 if sig.ip_opt_len != 0: 803 # FIXME: Non-zero IPv4 options not handled 804 warning("Unhandled IPv4 option field") 805 else: 806 pkt.options = [] 807 808 if "df" in quirks: 809 pkt.flags |= 0x02 # set DF flag 810 if "id+" in quirks: 811 if pkt.id == 0: 812 pkt.id = random.randint(1, 2**16 - 1) 813 else: 814 pkt.id = 0 815 else: 816 pkt.flags &= ~(0x02) # DF flag not set 817 if "id-" in quirks: 818 pkt.id = 0 819 elif pkt.id == 0: 820 pkt.id = random.randint(1, 2**16 - 1) 821 if "ecn" in quirks: 822 pkt.tos |= random.randint(0x01, 0x03) 823 pkt.flags = pkt.flags | 0x04 if "0+" in quirks else pkt.flags & ~(0x04) 824 else: 825 pkt.hlim = sig.ttl - extrahops 826 if "flow" in quirks: 827 pkt.fl = random.randint(1, 2**20 - 1) 828 if "ecn" in quirks: 829 pkt.tc |= random.randint(0x01, 0x03) 830 831 # Take the options already set as "hints" to use in the new packet if we 832 # can. we'll use the already-set values if they're valid integers. 833 def int_only(val): 834 return val if isinstance(val, int) else None 835 orig_opts = dict(tcp.options) 836 mss_hint = int_only(orig_opts.get("MSS")) 837 ws_hint = int_only(orig_opts.get("WScale")) 838 ts_hint = [int_only(o) for o in orig_opts.get("Timestamp", (None, None))] 839 840 options = [] 841 for opt in sig.olayout.split(","): 842 if opt == "mss": 843 # MSS might have a maximum size because of WIN_TYPE_MSS 844 if sig.win_type == WIN_TYPE_MSS: 845 maxmss = (2**16 - 1) // sig.win 846 else: 847 maxmss = (2**16 - 1) 848 849 if sig.mss == -1: # wildcard mss 850 if mss_hint and 0 <= mss_hint <= maxmss: 851 options.append(("MSS", mss_hint)) 852 else: # invalid hint, generate new value 853 options.append(("MSS", random.randint(100, maxmss))) 854 else: 855 options.append(("MSS", sig.mss)) 856 857 elif opt == "ws": 858 if sig.wscale == -1: # wildcard wscale 859 maxws = 2**8 860 if "exws" in quirks: # wscale > 14 861 if ws_hint and 14 < ws_hint < maxws: 862 options.append(("WScale", ws_hint)) 863 else: # invalid hint, generate new value > 14 864 options.append(("WScale", random.randint(15, maxws - 1))) # noqa: E501 865 else: 866 if ws_hint and 0 <= ws_hint < maxws: 867 options.append(("WScale", ws_hint)) 868 else: # invalid hint, generate new value 869 options.append(("WScale", RandByte())) 870 else: 871 options.append(("WScale", sig.wscale)) 872 873 elif opt == "ts": 874 ts1, ts2 = ts_hint 875 876 if "ts1-" in quirks: # own timestamp specified as zero 877 ts1 = 0 878 elif uptime is not None: # if specified uptime, override 879 ts1 = uptime 880 elif ts1 is None or not (0 < ts1 < 2**32): # invalid hint 881 ts1 = random.randint(120, 100 * 60 * 60 * 24 * 365) 882 883 # non-zero peer timestamp on initial SYN 884 if "ts2+" in quirks and tcp_type == 0x02: 885 if ts2 is None or not (0 < ts2 < 2**32): # invalid hint 886 ts2 = random.randint(1, 2**32 - 1) 887 else: 888 ts2 = 0 889 options.append(("Timestamp", (ts1, ts2))) 890 891 elif opt == "nop": 892 options.append(("NOP", None)) 893 elif opt == "sok": 894 options.append(("SAckOK", "")) 895 elif opt[:3] == "eol": 896 options.append(("EOL", None)) 897 # FIXME: opt+ quirk not handled 898 if "opt+" in quirks: 899 warning("Unhandled opt+ quirk") 900 elif opt == "sack": 901 # Randomize SAck value in range of 10 <= val <= 34 902 sack_len = random.choice([10, 18, 26, 34]) - 2 903 optstruct = "!%iI" % (sack_len // 4) 904 rand_val = RandString(struct.calcsize(optstruct))._fix() 905 options.append(("SAck", struct.unpack(optstruct, rand_val))) 906 else: 907 warning("Unhandled TCP option %s", opt) 908 tcp.options = options 909 910 if sig.win_type == WIN_TYPE_NORMAL: 911 tcp.window = sig.win 912 elif sig.win_type == WIN_TYPE_MSS: 913 mss = [x for x in options if x[0] == "MSS"] 914 if not mss: 915 raise ValueError("TCP window value requires MSS, and MSS option not set") # noqa: E501 916 tcp.window = mss[0][1] * sig.win 917 elif sig.win_type == WIN_TYPE_MOD: 918 tcp.window = sig.win * random.randint(1, (2**16 - 1) // sig.win) 919 elif sig.win_type == WIN_TYPE_MTU: 920 tcp.window = mtu * sig.win 921 elif sig.win_type == WIN_TYPE_ANY: 922 tcp.window = RandShort() 923 else: 924 warning("Unhandled window size specification") 925 926 if "seq-" in quirks: 927 tcp.seq = 0 928 elif tcp.seq == 0: 929 tcp.seq = random.randint(1, 2**32 - 1) 930 931 if "ack+" in quirks: 932 tcp.flags &= ~(0x10) # ACK flag not set 933 if tcp.ack == 0: 934 tcp.ack = random.randint(1, 2**32 - 1) 935 elif "ack-" in quirks: 936 tcp.flags |= 0x10 # ACK flag set 937 tcp.ack = 0 938 939 if "uptr+" in quirks: 940 tcp.flags &= ~(0x020) # URG flag not set 941 if tcp.urgptr == 0: 942 tcp.urgptr = random.randint(1, 2**16 - 1) 943 elif "urgf+" in quirks: 944 tcp.flags |= 0x020 # URG flag used 945 946 tcp.flags = tcp.flags | 0x08 if "pushf+" in quirks else tcp.flags & ~(0x08) 947 948 if sig.pay_class: # signature has payload 949 if not tcp.payload: 950 pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) 951 else: 952 tcp.payload = NoPayload() 953 954 return pkt 955