• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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