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"""Clone of Nmap's first generation OS fingerprinting. 7 8This code works with the first-generation OS detection and 9nmap-os-fingerprints, which has been removed from Nmap on November 3, 102007 (https://github.com/nmap/nmap/commit/50c49819), which means it is 11outdated. 12 13To get the last published version of this outdated fingerprint 14database, you can fetch it from 15<https://raw.githubusercontent.com/nmap/nmap/9efe1892/nmap-os-fingerprints>. 16 17""" 18 19import os 20import re 21 22from scapy.data import KnowledgeBase 23from scapy.config import conf 24from scapy.arch import WINDOWS 25from scapy.error import warning 26from scapy.layers.inet import IP, TCP, UDP, ICMP, UDPerror, IPerror 27from scapy.packet import NoPayload, Packet 28from scapy.sendrecv import sr 29from scapy.compat import plain_str, raw 30from scapy.plist import SndRcvList, PacketList 31 32# Typing imports 33from typing import ( 34 Dict, 35 List, 36 Tuple, 37 Optional, 38 cast, 39 Union, 40) 41 42if WINDOWS: 43 conf.nmap_base = os.environ["ProgramFiles"] + "\\nmap\\nmap-os-fingerprints" # noqa: E501 44else: 45 conf.nmap_base = "/usr/share/nmap/nmap-os-fingerprints" 46 47 48###################### 49# nmap OS fp stuff # 50###################### 51 52 53_NMAP_LINE = re.compile('^([^\\(]*)\\(([^\\)]*)\\)$') 54 55 56class NmapKnowledgeBase(KnowledgeBase): 57 """A KnowledgeBase specialized in Nmap first-generation OS 58fingerprints database. Loads from conf.nmap_base when self.filename is 59None. 60 61 """ 62 63 def lazy_init(self): 64 # type: () -> None 65 try: 66 fdesc = open(conf.nmap_base 67 if self.filename is None else 68 self.filename, "rb") 69 except (IOError, TypeError): 70 warning("Cannot open nmap database [%s]", self.filename) 71 self.filename = None 72 return 73 74 self.base = [] 75 self.base = cast(List[Tuple[str, Dict[str, Dict[str, str]]]], self.base) 76 name = None 77 sig = {} # type: Dict[str,Dict[str,str]] 78 for line in fdesc: 79 str_line = plain_str(line) 80 str_line = str_line.split('#', 1)[0].strip() 81 if not str_line: 82 continue 83 if str_line.startswith("Fingerprint "): 84 if name is not None: 85 self.base.append((name, sig)) 86 name = str_line[12:].strip() 87 sig = {} 88 continue 89 if str_line.startswith("Class "): 90 continue 91 match_line = _NMAP_LINE.search(str_line) 92 if match_line is None: 93 continue 94 test, values = match_line.groups() 95 sig[test] = dict(val.split('=', 1) for val in 96 (values.split('%') if values else [])) 97 if name is not None: 98 self.base.append((name, sig)) 99 fdesc.close() 100 101 def get_base(self): 102 # type: () -> List[Tuple[str, Dict]] 103 return cast(List[Tuple[str, Dict]], super(NmapKnowledgeBase, self).get_base()) 104 105 106conf.nmap_kdb = NmapKnowledgeBase(None) 107conf.nmap_kdb = cast(NmapKnowledgeBase, conf.nmap_kdb) 108 109 110def nmap_tcppacket_sig(pkt): 111 # type: (Optional[Packet]) -> Dict 112 res = {} 113 if pkt is not None: 114 res["DF"] = "Y" if pkt.flags.DF else "N" 115 res["W"] = "%X" % pkt.window 116 res["ACK"] = "S++" if pkt.ack == 2 else "S" if pkt.ack == 1 else "O" 117 res["Flags"] = str(pkt[TCP].flags)[::-1] 118 res["Ops"] = "".join(x[0][0] for x in pkt[TCP].options) 119 else: 120 res["Resp"] = "N" 121 return res 122 123 124def nmap_udppacket_sig(snd, rcv): 125 # type: (SndRcvList, PacketList) -> Dict 126 res = {} 127 if rcv is None: 128 res["Resp"] = "N" 129 else: 130 res["DF"] = "Y" if rcv.flags.DF else "N" 131 res["TOS"] = "%X" % rcv.tos 132 res["IPLEN"] = "%X" % rcv.len 133 res["RIPTL"] = "%X" % rcv.payload.payload.len 134 res["RID"] = "E" if snd.id == rcv[IPerror].id else "F" 135 res["RIPCK"] = "E" if snd.chksum == rcv[IPerror].chksum else ( 136 "0" if rcv[IPerror].chksum == 0 else "F" 137 ) 138 res["UCK"] = "E" if snd.payload.chksum == rcv[UDPerror].chksum else ( 139 "0" if rcv[UDPerror].chksum == 0 else "F" 140 ) 141 res["ULEN"] = "%X" % rcv[UDPerror].len 142 res["DAT"] = "E" if ( 143 isinstance(rcv[UDPerror].payload, NoPayload) or 144 raw(rcv[UDPerror].payload) == raw(snd[UDP].payload) 145 ) else "F" 146 return res 147 148 149def nmap_match_one_sig(seen, ref): 150 # type: (Dict, Dict) -> float 151 cnt = sum(val in ref.get(key, "").split("|") for key, val in seen.items()) 152 if cnt == 0 and seen.get("Resp") == "N": 153 return 0.7 154 return float(cnt) / len(seen) 155 156 157def nmap_sig(target, oport=80, cport=81, ucport=1): 158 # type: (str, int, int, int) -> Dict 159 res = {} 160 161 tcpopt = [("WScale", 10), 162 ("NOP", None), 163 ("MSS", 256), 164 ("Timestamp", (123, 0))] 165 tests = [ 166 IP(dst=target, id=1) / 167 TCP(seq=1, sport=5001 + i, dport=oport if i < 4 else cport, 168 options=tcpopt, flags=flags) 169 for i, flags in enumerate(["CS", "", "SFUP", "A", "S", "A", "FPU"]) 170 ] 171 tests.append(IP(dst=target) / UDP(sport=5008, dport=ucport) / (300 * "i")) 172 173 ans, unans = sr(tests, timeout=2) 174 ans.extend((x, None) for x in unans) 175 176 for snd, rcv in ans: 177 if snd.sport == 5008: 178 res["PU"] = (snd, rcv) 179 else: 180 test = "T%i" % (snd.sport - 5000) 181 if rcv is not None and ICMP in rcv: 182 warning("Test %s answered by an ICMP", test) 183 rcv = None # type: ignore 184 res[test] = rcv 185 186 return nmap_probes2sig(res) 187 188 189def nmap_probes2sig(tests): 190 # type: (Dict) -> Dict 191 tests = tests.copy() 192 res = {} 193 if "PU" in tests: 194 res["PU"] = nmap_udppacket_sig(*tests["PU"]) 195 del tests["PU"] 196 for k in tests: 197 res[k] = nmap_tcppacket_sig(tests[k]) 198 return res 199 200 201def nmap_search(sigs): 202 # type: (Dict) -> Tuple[Union[int, float], List] 203 guess = 0, [] # type: Tuple[Union[int, float], List] 204 conf.nmap_kdb = cast(NmapKnowledgeBase, conf.nmap_kdb) 205 for osval, fprint in conf.nmap_kdb.get_base(): 206 score = 0.0 207 for test, values in fprint.items(): 208 if test in sigs: 209 score += nmap_match_one_sig(sigs[test], values) 210 score /= len(sigs) 211 if score > guess[0]: 212 guess = score, [osval] 213 elif score == guess[0]: 214 guess[1].append(osval) 215 return guess 216 217 218@conf.commands.register 219def nmap_fp(target, oport=80, cport=81): 220 # type: (str, int, int) -> Tuple[Union[int, float], List] 221 """nmap fingerprinting 222nmap_fp(target, [oport=80,] [cport=81,]) -> list of best guesses with accuracy 223""" 224 sigs = nmap_sig(target, oport, cport) 225 return nmap_search(sigs) 226 227 228@conf.commands.register 229def nmap_sig2txt(sig): 230 # type: (Dict) -> str 231 torder = ["TSeq", "T1", "T2", "T3", "T4", "T5", "T6", "T7", "PU"] 232 korder = ["Class", "gcd", "SI", "IPID", "TS", 233 "Resp", "DF", "W", "ACK", "Flags", "Ops", 234 "TOS", "IPLEN", "RIPTL", "RID", "RIPCK", "UCK", "ULEN", "DAT"] 235 txt = [] 236 for i in sig: 237 if i not in torder: 238 torder.append(i) 239 for test in torder: 240 testsig = sig.get(test) 241 if testsig is None: 242 continue 243 txt.append("%s(%s)" % (test, "%".join( 244 "%s=%s" % (key, testsig[key]) for key in korder if key in testsig 245 ))) 246 return "\n".join(txt) 247