Skip to content
Snippets Groups Projects
Commit cacbfa88 authored by Phil's avatar Phil
Browse files

Improved p0f support (patch from ticket #76, P. Lalet)

parent 3f5d9e58
No related branches found
No related tags found
No related merge requests found
...@@ -5,8 +5,13 @@ ...@@ -5,8 +5,13 @@
from scapy.data import KnowledgeBase from scapy.data import KnowledgeBase
from scapy.config import conf from scapy.config import conf
from scapy.layers.inet import IP, TCP, TCPOptions
from scapy.packet import NoPayload
conf.p0f_base ="/etc/p0f/p0f.fp" conf.p0f_base ="/etc/p0f/p0f.fp"
conf.p0fa_base ="/etc/p0f/p0fa.fp"
conf.p0fr_base ="/etc/p0f/p0fr.fp"
conf.p0fo_base ="/etc/p0f/p0fo.fp"
############### ###############
...@@ -26,8 +31,6 @@ conf.p0f_base ="/etc/p0f/p0f.fp" ...@@ -26,8 +31,6 @@ conf.p0f_base ="/etc/p0f/p0f.fp"
# OS - OS genre # OS - OS genre
# details - OS description # details - OS description
class p0fKnowledgeBase(KnowledgeBase): class p0fKnowledgeBase(KnowledgeBase):
def __init__(self, filename): def __init__(self, filename):
KnowledgeBase.__init__(self, filename) KnowledgeBase.__init__(self, filename)
...@@ -46,7 +49,11 @@ class p0fKnowledgeBase(KnowledgeBase): ...@@ -46,7 +49,11 @@ class p0fKnowledgeBase(KnowledgeBase):
l = tuple(l.split(":")) l = tuple(l.split(":"))
if len(l) < 8: if len(l) < 8:
continue continue
li = map(int,l[1:4]) def a2i(x):
if x.isdigit():
return int(x)
return x
li = map(a2i, l[1:4])
#if li[0] not in self.ttl_range: #if li[0] not in self.ttl_range:
# self.ttl_range.append(li[0]) # self.ttl_range.append(li[0])
# self.ttl_range.sort() # self.ttl_range.sort()
...@@ -57,38 +64,70 @@ class p0fKnowledgeBase(KnowledgeBase): ...@@ -57,38 +64,70 @@ class p0fKnowledgeBase(KnowledgeBase):
f.close() f.close()
p0f_kdb = p0fKnowledgeBase(conf.p0f_base) p0f_kdb = p0fKnowledgeBase(conf.p0f_base)
p0fa_kdb = p0fKnowledgeBase(conf.p0fa_base)
p0fr_kdb = p0fKnowledgeBase(conf.p0fr_base)
p0fo_kdb = p0fKnowledgeBase(conf.p0fo_base)
def p0f_selectdb(flags):
# tested flags: S, R, A
if flags & 0x16 == 0x2:
# SYN
return p0f_kdb
elif flags & 0x16 == 0x12:
# SYN/ACK
return p0fa_kdb
elif flags & 0x16 in [ 0x4, 0x14 ]:
# RST RST/ACK
return p0fr_kdb
elif flags & 0x16 == 0x10:
# ACK
return p0fo_kdb
else:
return None
def packet2p0f(pkt): def packet2p0f(pkt):
pkt = pkt.copy()
pkt = pkt.__class__(str(pkt))
while pkt.haslayer(IP) and pkt.haslayer(TCP): while pkt.haslayer(IP) and pkt.haslayer(TCP):
pkt = pkt.getlayer(IP) pkt = pkt.getlayer(IP)
if isinstance(pkt.payload, TCP): if isinstance(pkt.payload, TCP):
break break
pkt = pkt.payload pkt = pkt.payload
if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP): if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
raise TypeError("Not a TCP/IP packet") raise TypeError("Not a TCP/IP packet")
if pkt.payload.flags & 0x13 != 0x02: #S,!A,!F #if pkt.payload.flags & 0x7 != 0x02: #S,!F,!R
raise TypeError("Not a syn packet") # raise TypeError("Not a SYN or SYN/ACK packet")
db = p0f_selectdb(pkt.payload.flags)
#t = p0f_kdb.ttl_range[:] #t = p0f_kdb.ttl_range[:]
#t += [pkt.ttl] #t += [pkt.ttl]
#t.sort() #t.sort()
#ttl=t[t.index(pkt.ttl)+1] #ttl=t[t.index(pkt.ttl)+1]
ttl = pkt.ttl ttl = pkt.ttl
df = (pkt.flags & 2) / 2 df = (pkt.flags & 2) / 2
ss = len(pkt) ss = len(pkt)
# from p0f/config.h : PACKET_BIG = 100 # from p0f/config.h : PACKET_BIG = 100
if ss > 100: if ss > 100:
ss = 0 if db == p0fr_kdb:
# p0fr.fp: "Packet size may be wildcarded. The meaning of
# wildcard is, however, hardcoded as 'size >
# PACKET_BIG'"
ss = '*'
else:
ss = 0
if db == p0fo_kdb:
# p0fo.fp: "Packet size MUST be wildcarded."
ss = '*'
ooo = "" ooo = ""
mss = -1 mss = -1
qqT = False qqT = False
qqP = False qqP = False
#qqBroken = False #qqBroken = False
ilen = (pkt[TCP].dataofs << 2) - 20 # from p0f.c ilen = (pkt.payload.dataofs << 2) - 20 # from p0f.c
for option in pkt.payload.options: for option in pkt.payload.options:
ilen -= 1 ilen -= 1
if option[0] == "MSS": if option[0] == "MSS":
...@@ -118,60 +157,85 @@ def packet2p0f(pkt): ...@@ -118,60 +157,85 @@ def packet2p0f(pkt):
if ilen > 0: if ilen > 0:
qqP = True qqP = True
else: else:
ooo += "?," if type(option[0]) is str:
ooo += "?%i," % TCPOptions[1][option[0]]
else:
ooo += "?%i," % option[0]
# FIXME: ilen # FIXME: ilen
ooo = ooo[:-1] ooo = ooo[:-1]
if ooo == "": ooo = "." if ooo == "": ooo = "."
win = pkt.payload.window win = pkt.payload.window
if mss != -1: if mss != -1:
if win % mss == 0: if mss != 0 and win % mss == 0:
win = "S" + str(win/mss) win = "S" + str(win/mss)
elif win % (mss + 40) == 0: elif win % (mss + 40) == 0:
win = "T" + str(win/(mss+40)) win = "T" + str(win/(mss+40))
win = str(win) win = str(win)
qq = "" qq = ""
if db == p0fr_kdb:
if pkt.payload.flags & 0x10 == 0x10:
# p0fr.fp: "A new quirk, 'K', is introduced to denote
# RST+ACK packets"
qq += "K"
# The two next cases should also be only for p0f*r*, but although
# it's not documented (or I have not noticed), p0f seems to
# support the '0' and 'Q' quirks on any databases (or at the least
# "classical" p0f.fp).
if pkt.payload.seq == pkt.payload.ack:
# p0fr.fp: "A new quirk, 'Q', is used to denote SEQ number
# equal to ACK number."
qq += "Q"
if pkt.payload.seq == 0:
# p0fr.fp: "A new quirk, '0', is used to denote packets
# with SEQ number set to 0."
qq += "0"
if qqP: if qqP:
qq += "P" qq += "P"
if pkt[IP].id == 0: if pkt.id == 0:
qq += "Z" qq += "Z"
if pkt[IP].options != '': if pkt.options != []:
qq += "I" qq += "I"
if pkt[TCP].urgptr != 0: if pkt.payload.urgptr != 0:
qq += "U" qq += "U"
if pkt[TCP].reserved != 0: if pkt.payload.reserved != 0:
qq += "X" qq += "X"
if pkt[TCP].ack != 0: if pkt.payload.ack != 0:
qq += "A" qq += "A"
if qqT: if qqT:
qq += "T" qq += "T"
if pkt[TCP].flags & 40 != 0: if db == p0fo_kdb:
# U or P if pkt.payload.flags & 0x20 != 0:
qq += "F" # U
if not isinstance(pkt[TCP].payload, NoPayload): # p0fo.fp: "PUSH flag is excluded from 'F' quirk checks"
qq += "F"
else:
if pkt.payload.flags & 0x28 != 0:
# U or P
qq += "F"
if db != p0fo_kdb and not isinstance(pkt.payload.payload, NoPayload):
# p0fo.fp: "'D' quirk is not checked for."
qq += "D" qq += "D"
# FIXME : "!" - broken options segment # FIXME : "!" - broken options segment: not handled yet
if qq == "": if qq == "":
qq = "." qq = "."
return (win, return (db, (win, ttl, df, ss, ooo, qq))
ttl,
df,
ss,
ooo,
qq)
def p0f_correl(x,y): def p0f_correl(x,y):
d = 0 d = 0
# wwww can be "*" or "%nn" # wwww can be "*" or "%nn". "Tnn" and "Snn" should work fine with
# the x[0] == y[0] test.
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)) 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))
# ttl # ttl
d += (y[1] >= x[1] and y[1] - x[1] < 32) d += (y[1] >= x[1] and y[1] - x[1] < 32)
for i in [2, 3, 5]: for i in [2, 5]:
d += (x[i] == y[i]) d += (x[i] == y[i] or y[i] == '*')
# '*' has a special meaning for ss
d += x[3] == y[3]
xopt = x[4].split(",") xopt = x[4].split(",")
yopt = y[4].split(",") yopt = y[4].split(",")
if len(xopt) == len(yopt): if len(xopt) == len(yopt):
...@@ -192,31 +256,34 @@ def p0f_correl(x,y): ...@@ -192,31 +256,34 @@ def p0f_correl(x,y):
@conf.commands.register @conf.commands.register
def p0f(pkt): def p0f(pkt):
"""Passive OS fingerprinting: which OS emitted this TCP SYN ? """Passive OS fingerprinting: which OS emitted this TCP packet ?
p0f(packet) -> accuracy, [list of guesses] p0f(packet) -> accuracy, [list of guesses]
""" """
pb = p0f_kdb.get_base() db, sig = packet2p0f(pkt)
if db:
pb = db.get_base()
else:
pb = []
if not pb: if not pb:
warning("p0f base empty.") warning("p0f base empty.")
return [] return []
s = len(pb[0][0]) #s = len(pb[0][0])
r = [] r = []
sig = packet2p0f(pkt)
max = len(sig[4].split(",")) + 5 max = len(sig[4].split(",")) + 5
for b in pb: for b in pb:
d = p0f_correl(sig,b) d = p0f_correl(sig,b)
if d == max: if d == max:
r.append((b[6], b[7], b[1] - pkt[IP].ttl)) r.append((b[6], b[7], b[1] - pkt[IP].ttl))
return r return r
def prnp0f(pkt): def prnp0f(pkt):
# we should print which DB we use
try: try:
r = p0f(pkt) r = p0f(pkt)
except: except:
return return
if r == []: if r == []:
r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt))) + ":?:?]", None) r = ("UNKNOWN", "[" + ":".join(map(str, packet2p0f(pkt)[1])) + ":?:?]", None)
else: else:
r = r[0] r = r[0]
uptime = None uptime = None
...@@ -228,16 +295,16 @@ def prnp0f(pkt): ...@@ -228,16 +295,16 @@ def prnp0f(pkt):
uptime = None uptime = None
res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1]) res = pkt.sprintf("%IP.src%:%TCP.sport% - " + r[0] + " " + r[1])
if uptime is not None: if uptime is not None:
res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n -> %IP.dst%:%TCP.dport%") res += pkt.sprintf(" (up: " + str(uptime/3600) + " hrs)\n -> %IP.dst%:%TCP.dport% (%TCP.flags%)")
else: else:
res += pkt.sprintf("\n -> %IP.dst%:%TCP.dport%") res += pkt.sprintf("\n -> %IP.dst%:%TCP.dport% (%TCP.flags%)")
if r[2] is not None: if r[2] is not None:
res += " (distance " + str(r[2]) + ")" res += " (distance " + str(r[2]) + ")"
print res print res
@conf.commands.register @conf.commands.register
def pkt2uptime(pkt, HZ=100): def pkt2uptime(pkt, HZ=100):
"""Calculate the date the machine which emitted the packet booted using TCP timestamp """Calculate the date the machine which emitted the packet booted using TCP timestamp
pkt2uptime(pkt, [HZ=100])""" pkt2uptime(pkt, [HZ=100])"""
if not isinstance(pkt, Packet): if not isinstance(pkt, Packet):
raise TypeError("Not a TCP packet") raise TypeError("Not a TCP packet")
...@@ -253,4 +320,219 @@ pkt2uptime(pkt, [HZ=100])""" ...@@ -253,4 +320,219 @@ pkt2uptime(pkt, [HZ=100])"""
return t return t
raise TypeError("No timestamp option") raise TypeError("No timestamp option")
def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
extrahops=0, mtu=1500, uptime=None):
"""Modifies pkt so that p0f will think it has been sent by a
specific OS. If osdetails is None, then we randomly pick up a
personality matching osgenre. If osgenre and signature are also None,
we use a local signature (using p0f_getlocalsigs). If signature is
specified (as a tuple), we use the signature.
For now, only TCP Syn packets are supported.
Some specifications of the p0f.fp file are not (yet) implemented."""
pkt = pkt.copy()
#pkt = pkt.__class__(str(pkt))
while pkt.haslayer(IP) and pkt.haslayer(TCP):
pkt = pkt.getlayer(IP)
if isinstance(pkt.payload, TCP):
break
pkt = pkt.payload
if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
raise TypeError("Not a TCP/IP packet")
if uptime is None:
uptime = random.randint(120,100*60*60*24*365)
db = p0f_selectdb(pkt.payload.flags)
if osgenre:
pb = db.get_base()
if pb is None:
pb = []
pb = filter(lambda x: x[6] == osgenre, pb)
if osdetails:
pb = filter(lambda x: x[7] == osdetails, pb)
elif signature:
pb = [signature]
else:
pb = p0f_getlocalsigs()[db]
if db == p0fr_kdb:
# 'K' quirk <=> RST+ACK
if pkt.payload.flags & 0x4 == 0x4:
pb = filter(lambda x: 'K' in x[5], pb)
else:
pb = filter(lambda x: 'K' not in x[5], pb)
if not pb:
raise Scapy_Exception("No match in the p0f database")
pers = pb[random.randint(0, len(pb) - 1)]
# options (we start with options because of MSS)
## TODO: let the options already set if they are valid
options = []
if pers[4] != '.':
for opt in pers[4].split(','):
if opt[0] == 'M':
# MSS might have a maximum size because of window size
# specification
if pers[0][0] == 'S':
maxmss = (2L**16-1) / int(pers[0][1:])
else:
maxmss = (2L**16-1)
# If we have to randomly pick up a value, we cannot use
# scapy RandXXX() functions, because the value has to be
# set in case we need it for the window size value. That's
# why we use random.randint()
if opt[1:] == '*':
options.append(('MSS', random.randint(1,maxmss)))
elif opt[1] == '%':
coef = int(opt[2:])
options.append(('MSS', coef*random.randint(1,maxmss/coef)))
else:
options.append(('MSS', int(opt[1:])))
elif opt[0] == 'W':
if opt[1:] == '*':
options.append(('WScale', RandByte()))
elif opt[1] == '%':
coef = int(opt[2:])
options.append(('WScale', coef*RandNum(min=1,
max=(2L**8-1)/coef)))
else:
options.append(('WScale', int(opt[1:])))
elif opt == 'T0':
options.append(('Timestamp', (0, 0)))
elif opt == 'T':
if 'T' in pers[5]:
# FIXME: RandInt() here does not work (bug (?) in
# TCPOptionsField.m2i often raises "OverflowError:
# long int too large to convert to int" in:
# oval = struct.pack(ofmt, *oval)"
# Actually, this is enough to often raise the error:
# struct.pack('I', RandInt())
options.append(('Timestamp', (uptime, random.randint(1,2**32-1))))
else:
options.append(('Timestamp', (uptime, 0)))
elif opt == 'S':
options.append(('SAckOK', ''))
elif opt == 'N':
options.append(('NOP', None))
elif opt == 'E':
options.append(('EOL', None))
elif opt[0] == '?':
if int(opt[1:]) in TCPOptions[0]:
optname = TCPOptions[0][int(opt[1:])][0]
optstruct = TCPOptions[0][int(opt[1:])][1]
options.append((optname,
struct.unpack(optstruct,
RandString(struct.calcsize(optstruct))._fix())))
else:
options.append((int(opt[1:]), ''))
## FIXME: qqP not handled
else:
warning("unhandled TCP option " + opt)
pkt.payload.options = options
# window size
if pers[0] == '*':
pkt.payload.window = RandShort()
elif pers[0].isdigit():
pkt.payload.window = int(pers[0])
elif pers[0][0] == '%':
coef = int(pers[0][1:])
pkt.payload.window = coef * RandNum(min=1,max=(2L**16-1)/coef)
elif pers[0][0] == 'T':
pkt.payload.window = mtu * int(pers[0][1:])
elif pers[0][0] == 'S':
## needs MSS set
MSS = filter(lambda x: x[0] == 'MSS', options)
if not filter(lambda x: x[0] == 'MSS', options):
raise Scapy_Exception("TCP window value requires MSS, and MSS option not set")
pkt.payload.window = filter(lambda x: x[0] == 'MSS', options)[0][1] * int(pers[0][1:])
else:
raise Scapy_Exception('Unhandled window size specification')
# ttl
pkt.ttl = pers[1]-extrahops
# DF flag
pkt.flags |= (2 * pers[2])
## FIXME: ss (packet size) not handled (how ? may be with D quirk
## if present)
# Quirks
if pers[5] != '.':
for qq in pers[5]:
## FIXME: not handled: P, I, X, !
# T handled with the Timestamp option
if qq == 'Z': pkt.id = 0
elif qq == 'U': pkt.payload.urgptr = RandShort()
elif qq == 'A': pkt.payload.ack = RandInt()
elif qq == 'F':
if db == p0fo_kdb:
pkt.payload.flags |= 0x20 # U
else:
pkt.payload.flags |= RandChoice(8, 32, 40) #P / U / PU
elif qq == 'D' and db != p0fo_kdb:
pkt /= Raw(load=RandString(random.randint(1, 10))) # XXX p0fo.fp
elif qq == 'Q': pkt.payload.seq = pkt.payload.ack
#elif qq == '0': pkt.payload.seq = 0
#if db == p0fr_kdb:
# '0' quirk is actually not only for p0fr.fp (see
# packet2p0f())
if '0' in pers[5]:
pkt.payload.seq = 0
elif pkt.payload.seq == 0:
pkt.payload.seq = RandInt()
while pkt.underlayer:
pkt = pkt.underlayer
return pkt
def p0f_getlocalsigs():
"""This function returns a dictionary of signatures indexed by p0f
db (e.g., p0f_kdb, p0fa_kdb, ...) for the local TCP/IP stack.
You need to have your firewall at least accepting the TCP packets
from/to a high port (30000 <= x <= 40000) on your loopback interface.
Please note that the generated signatures come from the loopback
interface and may (are likely to) be different than those generated on
"normal" interfaces."""
pid = os.fork()
port = random.randint(30000, 40000)
if pid > 0:
# parent: sniff
result = {}
def addresult(res):
# TODO: wildcard window size in some cases? and maybe some
# other values?
if res[0] not in result:
result[res[0]] = [res[1]]
else:
if res[1] not in result[res[0]]:
result[res[0]].append(res[1])
# XXX could we try with a "normal" interface using other hosts
iface = conf.route.route('127.0.0.1')[0]
# each packet is seen twice: S + RA, S + SA + A + FA + A
# XXX are the packets also seen twice on non Linux systems ?
count=14
pl = sniff(iface=iface, filter='tcp and port ' + str(port), count = count, timeout=3)
map(addresult, map(packet2p0f, pl))
os.waitpid(pid,0)
elif pid < 0:
log_runtime.error("fork error")
else:
# child: send
# XXX erk
time.sleep(1)
s1 = socket.socket(socket.AF_INET, type = socket.SOCK_STREAM)
# S & RA
try:
s1.connect(('127.0.0.1', port))
except socket.error:
pass
# S, SA, A, FA, A
s1.bind(('127.0.0.1', port))
s1.connect(('127.0.0.1', port))
# howto: get an RST w/o ACK packet
s1.close()
os._exit(0)
return result
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment