Skip to content

Commit 9d20c92

Browse files
authored
Merge pull request #1023 from saturn597/p0f_impersonate
Use existing TCP opts in p0f_impersonate
2 parents 2facfe5 + 04bd9af commit 9d20c92

File tree

3 files changed

+119
-18
lines changed

3 files changed

+119
-18
lines changed

scapy/modules/p0f.py

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from scapy.error import warning, Scapy_Exception, log_runtime
2323
from scapy.volatile import RandInt, RandByte, RandChoice, RandNum, RandShort, RandString
2424
from scapy.sendrecv import sniff
25+
from scapy.modules import six
2526
from scapy.modules.six.moves import map, range
2627
if conf.route is None:
2728
# unused import, only to initialize conf.route
@@ -359,10 +360,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
359360

360361
if not isinstance(pkt, IP) or not isinstance(pkt.payload, TCP):
361362
raise TypeError("Not a TCP/IP packet")
362-
363-
if uptime is None:
364-
uptime = random.randint(120,100*60*60*24*365)
365-
363+
366364
db = p0f_selectdb(pkt.payload.flags)
367365
if osgenre:
368366
pb = db.get_base()
@@ -386,50 +384,89 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
386384
pers = pb[random.randint(0, len(pb) - 1)]
387385

388386
# options (we start with options because of MSS)
389-
## TODO: let the options already set if they are valid
387+
# Take the options already set as "hints" to use in the new packet if we
388+
# can. MSS, WScale and Timestamp can all be wildcarded in a signature, so
389+
# we'll use the already-set values if they're valid integers.
390+
orig_opts = dict(pkt.payload.options)
391+
int_only = lambda val: val if isinstance(val, six.integer_types) else None
392+
mss_hint = int_only(orig_opts.get('MSS'))
393+
wscale_hint = int_only(orig_opts.get('WScale'))
394+
ts_hint = [int_only(o) for o in orig_opts.get('Timestamp', (None, None))]
395+
390396
options = []
391397
if pers[4] != '.':
392398
for opt in pers[4].split(','):
393399
if opt[0] == 'M':
394400
# MSS might have a maximum size because of window size
395401
# specification
396402
if pers[0][0] == 'S':
397-
maxmss = (2**16-1) / int(pers[0][1:])
403+
maxmss = (2**16-1) // int(pers[0][1:])
398404
else:
399405
maxmss = (2**16-1)
406+
# disregard hint if out of range
407+
if mss_hint and not 0 <= mss_hint <= maxmss:
408+
mss_hint = None
400409
# If we have to randomly pick up a value, we cannot use
401410
# scapy RandXXX() functions, because the value has to be
402411
# set in case we need it for the window size value. That's
403412
# why we use random.randint()
404413
if opt[1:] == '*':
405-
options.append(('MSS', random.randint(1,maxmss)))
414+
if mss_hint is not None:
415+
options.append(('MSS', mss_hint))
416+
else:
417+
options.append(('MSS', random.randint(1, maxmss)))
406418
elif opt[1] == '%':
407419
coef = int(opt[2:])
408-
options.append(('MSS', coef*random.randint(1,maxmss/coef)))
420+
if mss_hint is not None and mss_hint % coef == 0:
421+
options.append(('MSS', mss_hint))
422+
else:
423+
options.append((
424+
'MSS', coef*random.randint(1, maxmss//coef)))
409425
else:
410426
options.append(('MSS', int(opt[1:])))
411427
elif opt[0] == 'W':
428+
if wscale_hint and not 0 <= wscale_hint < 2**8:
429+
wscale_hint = None
412430
if opt[1:] == '*':
413-
options.append(('WScale', RandByte()))
431+
if wscale_hint is not None:
432+
options.append(('WScale', wscale_hint))
433+
else:
434+
options.append(('WScale', RandByte()))
414435
elif opt[1] == '%':
415436
coef = int(opt[2:])
416-
options.append(('WScale', coef*RandNum(min=1,
417-
max=(2**8-1)/coef)))
437+
if wscale_hint is not None and wscale_hint % coef == 0:
438+
options.append(('WScale', wscale_hint))
439+
else:
440+
options.append((
441+
'WScale', coef*RandNum(min=1, max=(2**8-1)//coef)))
418442
else:
419443
options.append(('WScale', int(opt[1:])))
420444
elif opt == 'T0':
421445
options.append(('Timestamp', (0, 0)))
422446
elif opt == 'T':
423-
if 'T' in pers[5]:
447+
# Determine first timestamp.
448+
if uptime is not None:
449+
ts_a = uptime
450+
elif ts_hint[0] and 0 < ts_hint[0] < 2**32:
451+
# Note: if first ts is 0, p0f registers it as "T0" not "T",
452+
# hence we don't want to use the hint if it was 0.
453+
ts_a = ts_hint[0]
454+
else:
455+
ts_a = random.randint(120, 100*60*60*24*365)
456+
# Determine second timestamp.
457+
if 'T' not in pers[5]:
458+
ts_b = 0
459+
elif ts_hint[1] and 0 < ts_hint[1] < 2**32:
460+
ts_b = ts_hint[1]
461+
else:
424462
# FIXME: RandInt() here does not work (bug (?) in
425463
# TCPOptionsField.m2i often raises "OverflowError:
426464
# long int too large to convert to int" in:
427465
# oval = struct.pack(ofmt, *oval)"
428466
# Actually, this is enough to often raise the error:
429467
# struct.pack('I', RandInt())
430-
options.append(('Timestamp', (uptime, random.randint(1,2**32-1))))
431-
else:
432-
options.append(('Timestamp', (uptime, 0)))
468+
ts_b = random.randint(1, 2**32-1)
469+
options.append(('Timestamp', (ts_a, ts_b)))
433470
elif opt == 'S':
434471
options.append(('SAckOK', ''))
435472
elif opt == 'N':
@@ -457,15 +494,15 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
457494
pkt.payload.window = int(pers[0])
458495
elif pers[0][0] == '%':
459496
coef = int(pers[0][1:])
460-
pkt.payload.window = coef * RandNum(min=1,max=(2**16-1)/coef)
497+
pkt.payload.window = coef * RandNum(min=1, max=(2**16-1)//coef)
461498
elif pers[0][0] == 'T':
462499
pkt.payload.window = mtu * int(pers[0][1:])
463500
elif pers[0][0] == 'S':
464501
## needs MSS set
465502
mss = [x for x in options if x[0] == 'MSS']
466503
if not mss:
467504
raise Scapy_Exception("TCP window value requires MSS, and MSS option not set")
468-
pkt.payload.window = filter(lambda x: x[0] == 'MSS', options)[0][1] * int(pers[0][1:])
505+
pkt.payload.window = mss[0][1] * int(pers[0][1:])
469506
else:
470507
raise Scapy_Exception('Unhandled window size specification')
471508

@@ -487,7 +524,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None,
487524
if db == p0fo_kdb:
488525
pkt.payload.flags |= 0x20 # U
489526
else:
490-
pkt.payload.flags |= RandChoice(8, 32, 40) #P / U / PU
527+
pkt.payload.flags |= random.choice([8, 32, 40]) # P/U/PU
491528
elif qq == 'D' and db != p0fo_kdb:
492529
pkt /= conf.raw_layer(load=RandString(random.randint(1, 10))) # XXX p0fo.fp
493530
elif qq == 'Q': pkt.payload.seq = pkt.payload.ack

scapy/volatile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def __rsub__(self, other):
121121
return other - self._fix()
122122
def __mul__(self, other):
123123
return self._fix() * other
124+
def __rmul__(self, other):
125+
return other * self._fix()
124126
def __floordiv__(self, other):
125127
return self._fix() / other
126128
__div__ = __floordiv__

test/p0f.uts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
% Tests for Scapy's p0f module.
2+
3+
~ p0f
4+
5+
6+
############
7+
############
8+
+ Basic p0f module tests
9+
10+
= Module loading
11+
load_module('p0f')
12+
13+
14+
############
15+
############
16+
+ Tests for p0f_impersonate
17+
18+
# XXX: a lot of pieces of p0f_impersonate don't have tests yet.
19+
20+
= Impersonate when window size must be multiple of some integer
21+
sig = ('%467', 64, 1, 60, 'M*,W*', '.', 'Phony Sys', '1.0')
22+
pkt = p0f_impersonate(IP()/TCP(), signature=sig)
23+
assert pkt.payload.window % 467 == 0
24+
25+
= Handle unusual flags ("F") quirk
26+
sig = ('1024', 64, 0, 60, 'W*', 'F', 'Phony Sys', '1.0')
27+
pkt = p0f_impersonate(IP()/TCP(), signature=sig)
28+
assert (pkt.payload.flags & 40) in (8, 32, 40)
29+
30+
= Use valid option values from original packet
31+
sig = ('S4', 64, 1, 60, 'M*,W*,T', '.', 'Phony Sys', '1.0')
32+
opts = [('MSS', 1400), ('WScale', 3), ('Timestamp', (97256, 0))]
33+
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
34+
assert pkt.payload.options == opts
35+
36+
= Use valid option values when multiples required
37+
sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0')
38+
opts = [('MSS', 37*15), ('WScale', 19*12)]
39+
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
40+
assert pkt.payload.options == opts
41+
42+
= Discard non-multiple option values when multiples required
43+
sig = ('S4', 64, 1, 60, 'M%37,W%19', '.', 'Phony Sys', '1.0')
44+
opts = [('MSS', 37*15 + 1), ('WScale', 19*12 + 1)]
45+
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
46+
assert pkt.payload.options[0][1] % 37 == 0
47+
assert pkt.payload.options[1][1] % 19 == 0
48+
49+
= Discard bad timestamp values
50+
sig = ('S4', 64, 1, 60, 'M*,T', '.', 'Phony Sys', '1.0')
51+
opts = [('Timestamp', (0, 1000))]
52+
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
53+
# since option is "T" and not "T0":
54+
assert pkt.payload.options[1][1][0] > 0
55+
# since T quirk is not present:
56+
assert pkt.payload.options[1][1][1] == 0
57+
58+
= Discard 2nd timestamp of 0 if "T" quirk is present
59+
sig = ('S4', 64, 1, 60, 'M*,T', 'T', 'Phony Sys', '1.0')
60+
opts = [('Timestamp', (54321, 0))]
61+
pkt = p0f_impersonate(IP()/TCP(options=opts), signature=sig)
62+
assert pkt.payload.options[1][1][1] > 0

0 commit comments

Comments
 (0)