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