Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use existing TCP opts in p0f_impersonate #1023

Merged
merged 2 commits into from
Jan 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 55 additions & 18 deletions scapy/modules/p0f.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from scapy.error import warning, Scapy_Exception, log_runtime
from scapy.volatile import RandInt, RandByte, RandChoice, RandNum, RandShort, RandString
from scapy.sendrecv import sniff
from scapy.modules import six
from scapy.modules.six.moves import map, range
if conf.route is None:
# unused import, only to initialize conf.route
Expand Down Expand Up @@ -359,10 +360,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,

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()
Expand All @@ -386,50 +384,89 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
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
# Take the options already set as "hints" to use in the new packet if we
# can. MSS, WScale and Timestamp can all be wildcarded in a signature, so
# we'll use the already-set values if they're valid integers.
orig_opts = dict(pkt.payload.options)
int_only = lambda val: val if isinstance(val, six.integer_types) else None
mss_hint = int_only(orig_opts.get('MSS'))
wscale_hint = int_only(orig_opts.get('WScale'))
ts_hint = [int_only(o) for o in orig_opts.get('Timestamp', (None, None))]

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 = (2**16-1) / int(pers[0][1:])
maxmss = (2**16-1) // int(pers[0][1:])
else:
maxmss = (2**16-1)
# disregard hint if out of range
if mss_hint and not 0 <= mss_hint <= maxmss:
mss_hint = None
# 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)))
if mss_hint is not None:
options.append(('MSS', mss_hint))
else:
options.append(('MSS', random.randint(1, maxmss)))
elif opt[1] == '%':
coef = int(opt[2:])
options.append(('MSS', coef*random.randint(1,maxmss/coef)))
if mss_hint is not None and mss_hint % coef == 0:
options.append(('MSS', mss_hint))
else:
options.append((
'MSS', coef*random.randint(1, maxmss//coef)))
else:
options.append(('MSS', int(opt[1:])))
elif opt[0] == 'W':
if wscale_hint and not 0 <= wscale_hint < 2**8:
wscale_hint = None
if opt[1:] == '*':
options.append(('WScale', RandByte()))
if wscale_hint is not None:
options.append(('WScale', wscale_hint))
else:
options.append(('WScale', RandByte()))
elif opt[1] == '%':
coef = int(opt[2:])
options.append(('WScale', coef*RandNum(min=1,
max=(2**8-1)/coef)))
if wscale_hint is not None and wscale_hint % coef == 0:
options.append(('WScale', wscale_hint))
else:
options.append((
'WScale', coef*RandNum(min=1, max=(2**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]:
# Determine first timestamp.
if uptime is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but shouldn't we always set ts_a = ts_hint[0] when ts_hint[0] and 0 < ts_hint[0] < 2**32?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean instead of setting ts_a to uptime when uptime is set?

To me it seems more intuitive to use the value explicitly passed as an argument (uptime), rather than overriding it with the original packet's timestamp. If the caller wanted to use the value in the original packet, I wouldn't expect them to call the function with uptime=x.

Also this way the behavior is more consistent with the way it was before these changes. The first timestamp will be equal to uptime if uptime was passed, as before. An alternative if we aren't worried about consistency might be remove the uptime argument, since people can just set the timestamp in the original packet.

But for now I've kept it the way I had it. I could definitely be wrong, though, so please let me know if you do prefer it the other way.

ts_a = uptime
elif ts_hint[0] and 0 < ts_hint[0] < 2**32:
# Note: if first ts is 0, p0f registers it as "T0" not "T",
# hence we don't want to use the hint if it was 0.
ts_a = ts_hint[0]
else:
ts_a = random.randint(120, 100*60*60*24*365)
# Determine second timestamp.
if 'T' not in pers[5]:
ts_b = 0
elif ts_hint[1] and 0 < ts_hint[1] < 2**32:
ts_b = ts_hint[1]
else:
# 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)))
ts_b = random.randint(1, 2**32-1)
options.append(('Timestamp', (ts_a, ts_b)))
elif opt == 'S':
options.append(('SAckOK', ''))
elif opt == 'N':
Expand Down Expand Up @@ -457,15 +494,15 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
pkt.payload.window = int(pers[0])
elif pers[0][0] == '%':
coef = int(pers[0][1:])
pkt.payload.window = coef * RandNum(min=1,max=(2**16-1)/coef)
pkt.payload.window = coef * RandNum(min=1, max=(2**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 = [x for x in options if x[0] == 'MSS']
if not mss:
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:])
pkt.payload.window = mss[0][1] * int(pers[0][1:])
else:
raise Scapy_Exception('Unhandled window size specification')

Expand All @@ -487,7 +524,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
if db == p0fo_kdb:
pkt.payload.flags |= 0x20 # U
else:
pkt.payload.flags |= RandChoice(8, 32, 40) #P / U / PU
pkt.payload.flags |= random.choice([8, 32, 40]) # P/U/PU
elif qq == 'D' and db != p0fo_kdb:
pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) # XXX p0fo.fp
elif qq == 'Q': pkt.payload.seq = pkt.payload.ack
Expand Down
2 changes: 2 additions & 0 deletions scapy/volatile.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def __rsub__(self, other):
return other - self._fix()
def __mul__(self, other):
return self._fix() * other
def __rmul__(self, other):
return other * self._fix()
def __floordiv__(self, other):
return self._fix() / other
__div__ = __floordiv__
Expand Down
62 changes: 62 additions & 0 deletions test/p0f.uts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
% Tests for Scapy's p0f module.

~ p0f


############
############
+ Basic p0f module tests

= Module loading
load_module('p0f')


############
############
+ Tests for p0f_impersonate

# XXX: a lot of pieces of p0f_impersonate don't have tests yet.

= Impersonate when window size must be multiple of some integer
sig = ('%467', 64, 1, 60, 'M*,W*', '.', 'Phony Sys', '1.0')
pkt = p0f_impersonate(IP()/TCP(), signature=sig)
assert pkt.payload.window % 467 == 0

= Handle unusual flags ("F") quirk
sig = ('1024', 64, 0, 60, 'W*', 'F', 'Phony Sys', '1.0')
pkt = p0f_impersonate(IP()/TCP(), signature=sig)
assert (pkt.payload.flags & 40) in (8, 32, 40)

= Use valid option values from original packet
sig = ('S4', 64, 1, 60, 'M*,W*,T', '.', 'Phony Sys', '1.0')
opts = [('MSS', 1400), ('WScale', 3), ('Timestamp', (97256, 0))]
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
assert pkt.payload.options == opts

= Use valid option values when multiples required
sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0')
opts = [('MSS', 37*15), ('WScale', 19*12)]
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
assert pkt.payload.options == opts

= Discard non-multiple option values when multiples required
sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0')
opts = [('MSS', 37*15 + 1), ('WScale', 19*12 + 1)]
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
assert pkt.payload.options[0][1] % 37 == 0
assert pkt.payload.options[1][1] % 19 == 0

= Discard bad timestamp values
sig = ('S4', 64, 1, 60, 'M*,T', '.', 'Phony Sys', '1.0')
opts = [('Timestamp', (0, 1000))]
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
# since option is "T" and not "T0":
assert pkt.payload.options[1][1][0] > 0
# since T quirk is not present:
assert pkt.payload.options[1][1][1] == 0

= Discard 2nd timestamp of 0 if "T" quirk is present
sig = ('S4', 64, 1, 60, 'M*,T', 'T', 'Phony Sys', '1.0')
opts = [('Timestamp', (54321, 0))]
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
assert pkt.payload.options[1][1][1] > 0