patronus_fati 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/patronus_fati/bit_helper.rb +56 -0
- data/lib/patronus_fati/consts.rb +18 -0
- data/lib/patronus_fati/data_models/access_point.rb +31 -2
- data/lib/patronus_fati/data_models/ssid.rb +4 -0
- data/lib/patronus_fati/presence.rb +2 -12
- data/lib/patronus_fati/version.rb +1 -1
- data/lib/patronus_fati.rb +1 -0
- data/spec/patronus_fati/bit_helper_spec.rb +58 -0
- data/spec/patronus_fati/data_models/access_point_spec.rb +19 -4
- data/spec/patronus_fati/data_models/ssid_spec.rb +15 -0
- data/spec/patronus_fati/presence_spec.rb +3 -3
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29f01b90c143b565522531ec08eb46baaa467b3e
|
4
|
+
data.tar.gz: 797ee832481e9ad310001f22d8db6ef438eb0131
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1cfc2d362d834b03c9753eea64bbbc477d81330e3e2f4d6fe8c2cc025965e0f4151ce5d49a460c8a998feb91c3861e6b7af72bfaecdbb070542839c24673301
|
7
|
+
data.tar.gz: 7847043863e93749ef3a072bbcec8fa87352fa017a4bfe4c775af8dbece4b709d63da351ca9290044bd98b45acd472f1dd31ab229d5447e1aaa12e22ec53bb76
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module PatronusFati
|
2
|
+
module BitHelper
|
3
|
+
# Count the consecutive number of bits in the provided number
|
4
|
+
#
|
5
|
+
# @param [Fixnum] num
|
6
|
+
# @return [Fixnum]
|
7
|
+
def self.count_consecutive_bits(num)
|
8
|
+
count = 0
|
9
|
+
while num != 0
|
10
|
+
num = (num & (num << 1))
|
11
|
+
count += 1
|
12
|
+
end
|
13
|
+
count
|
14
|
+
end
|
15
|
+
|
16
|
+
# This is a bit of an odd algorithm. It needs to find the length of the
|
17
|
+
# longest common bits between n number of bit fields (in the context of
|
18
|
+
# this program that is 64 bit numbers).
|
19
|
+
#
|
20
|
+
# The algorithm works by iterating over each bit field , and for each of
|
21
|
+
# them calculating a reference bit field that is the combination of all
|
22
|
+
# uncompared bit strings (those further down the list, prior ones will have
|
23
|
+
# already been checked).
|
24
|
+
#
|
25
|
+
# By comparing the current bit list against the reference we can see where
|
26
|
+
# that bit was present with at least one other bit from the other bit
|
27
|
+
# field. Translating that into this domain specific context, was our SSID
|
28
|
+
# announced at the same time as any other SSID for that minute (bit)?
|
29
|
+
#
|
30
|
+
# Once we have the comparison we can count the longest consecutive bits
|
31
|
+
# between the current field and the reference field to find out how long
|
32
|
+
# any given SSID was simultaneously transmitting as any other on the same
|
33
|
+
# access point.
|
34
|
+
#
|
35
|
+
# This algorithm effectively has a computational cost of almost n^2 which
|
36
|
+
# could potentially get nasty in the event of a huge number of active SSIDs
|
37
|
+
# on a single AP. An upper limit should likely be decided on and in the
|
38
|
+
# event it is reached an alternative method should likely be used (in our
|
39
|
+
# case assuming it's broadcasting multiple is likely fine for these kinds
|
40
|
+
# of floods).
|
41
|
+
#
|
42
|
+
# @param [Array<Fixnum>] bit_list
|
43
|
+
# @return [Boolean]
|
44
|
+
def self.largest_bit_overlap(bit_list)
|
45
|
+
bit_list.map.with_index do |bits, i|
|
46
|
+
# We're at the end of the list there are no bits to compare
|
47
|
+
next 0 if bit_list.length == (i + 1)
|
48
|
+
# Build a reference bit string of all the bit fields we haven't
|
49
|
+
# compared this SSID to yet
|
50
|
+
reference = bit_list[(i + 1)..-1].inject { |ref, bits| ref | bits }
|
51
|
+
# Find the common bits between the reference and this bit string
|
52
|
+
count_consecutive_bits(reference & bits)
|
53
|
+
end.max
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/patronus_fati/consts.rb
CHANGED
@@ -79,6 +79,14 @@ module PatronusFati
|
|
79
79
|
dirtyChildren: (1 << 3),
|
80
80
|
}.freeze
|
81
81
|
|
82
|
+
# This is how many tracked intervals that need to be seen overlapping before
|
83
|
+
# we consider an access point as transmitting multiple SSIDs. The length of
|
84
|
+
# this is dependent on the length of presence intervals. The value of
|
85
|
+
# INTERVAL_DURATION determines the length of one interval.
|
86
|
+
#
|
87
|
+
# @see PatronusFati::INTERVAL_DURATION
|
88
|
+
SIMULTANEOUS_SSID_THRESHOLD = 2
|
89
|
+
|
82
90
|
# Number of seconds before we consider an access point as offline
|
83
91
|
AP_EXPIRATION = 300
|
84
92
|
|
@@ -102,6 +110,16 @@ module PatronusFati
|
|
102
110
|
(1 << 2) => 'WPS_LOCKED',
|
103
111
|
}
|
104
112
|
|
113
|
+
# How many seconds do each of our windows last
|
114
|
+
WINDOW_LENGTH = 3600
|
115
|
+
|
116
|
+
# How many intervals do we break each of our windows into? This must be
|
117
|
+
# less than 64.
|
118
|
+
WINDOW_INTERVALS = 60
|
119
|
+
|
120
|
+
# How long each interval will last in seconds
|
121
|
+
INTERVAL_DURATION = WINDOW_LENGTH / WINDOW_INTERVALS
|
122
|
+
|
105
123
|
Error = Class.new(StandardError)
|
106
124
|
DisconnectError = Class.new(PatronusFati::Error)
|
107
125
|
ParseError = Class.new(PatronusFati::Error)
|
@@ -12,7 +12,16 @@ module PatronusFati
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def active_ssids
|
15
|
-
|
15
|
+
return unless ssids
|
16
|
+
# If there is any active SSIDs return them
|
17
|
+
active = ssids.select { |_, v| v.active? }
|
18
|
+
return active unless active.empty?
|
19
|
+
|
20
|
+
# If there are no active SSIDs try and find the most recently seen SSID
|
21
|
+
# and report that as still active. Still return an empty set if there
|
22
|
+
# are no SSIDs.
|
23
|
+
most_recent = ssids.sort_by { |_, v| v.presence.last_visible || 0 }.last
|
24
|
+
most_recent ? Hash[[most_recent]] : {}
|
16
25
|
end
|
17
26
|
|
18
27
|
def add_client(mac)
|
@@ -53,6 +62,25 @@ module PatronusFati
|
|
53
62
|
mark_synced
|
54
63
|
end
|
55
64
|
|
65
|
+
def broadcasting_multiple?
|
66
|
+
return false unless ssids
|
67
|
+
return false if active_ssids.count == 1
|
68
|
+
|
69
|
+
presences = active_ssids.map(&:presence)
|
70
|
+
# This check becomes very expensive at larger numbers, if we get too
|
71
|
+
# high just short circuit and assume that yes there are simultaneous
|
72
|
+
# SSIDs being transmitted. This is likely a sign of a malicious device.
|
73
|
+
return true if presences.length >= 100
|
74
|
+
|
75
|
+
current_presence_bits = presences.map { |p| p.current_presence.bits }
|
76
|
+
return true if PatronusFati::BitHelper.largest_bit_overlap(current_presence_bits) >= SIMULTANEOUS_SSID_THRESHOLD
|
77
|
+
|
78
|
+
last_presence_bits = presences.map { |p| p.last_presence.bits }
|
79
|
+
return true if PatronusFati::BitHelper.largest_bit_overlap(last_presence_bits) >= SIMULTANEOUS_SSID_THRESHOLD
|
80
|
+
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
56
84
|
def cleanup_ssids
|
57
85
|
return if ssids.nil? || ssids.select { |_, v| v.presence.dead? }.empty?
|
58
86
|
|
@@ -72,10 +100,11 @@ module PatronusFati
|
|
72
100
|
def full_state
|
73
101
|
state = local_attributes.merge({
|
74
102
|
active: active?,
|
103
|
+
broadcasting_multiple: broadcasting_multiple?,
|
75
104
|
connected_clients: client_macs,
|
76
105
|
vendor: vendor
|
77
106
|
})
|
78
|
-
state[:ssids] = active_ssids.values.map(&:
|
107
|
+
state[:ssids] = active_ssids.values.map(&:full_state) if ssids
|
79
108
|
state
|
80
109
|
end
|
81
110
|
|
@@ -5,16 +5,6 @@ module PatronusFati
|
|
5
5
|
class Presence
|
6
6
|
attr_accessor :current_presence, :first_seen, :last_presence, :window_start
|
7
7
|
|
8
|
-
# How many seconds do each of our windows last
|
9
|
-
WINDOW_LENGTH = 3600
|
10
|
-
|
11
|
-
# How many intervals do we break each of our windows into? This must be
|
12
|
-
# less than 64.
|
13
|
-
WINDOW_INTERVALS = 60
|
14
|
-
|
15
|
-
# How long each interval will last in seconds
|
16
|
-
INTERVAL_DURATION = WINDOW_LENGTH / WINDOW_INTERVALS
|
17
|
-
|
18
8
|
# Translate a timestamp relative to the provided reference window into an
|
19
9
|
# appropriate bit within our bit field.
|
20
10
|
def bit_for_time(reference_window, timestamp)
|
@@ -112,7 +102,7 @@ module PatronusFati
|
|
112
102
|
rotate_presence
|
113
103
|
|
114
104
|
return false unless (lv = last_visible)
|
115
|
-
unix_time <=
|
105
|
+
unix_time <= lv
|
116
106
|
end
|
117
107
|
|
118
108
|
# Returns the duration in seconds of how long the specific object was
|
@@ -122,7 +112,7 @@ module PatronusFati
|
|
122
112
|
# of the last interval, which makes logical sense (1 bit set is 1 interval
|
123
113
|
# duration, not zero seconds).
|
124
114
|
def visible_time
|
125
|
-
(last_visible + INTERVAL_DURATION) - first_seen if first_seen
|
115
|
+
(last_visible + INTERVAL_DURATION) - first_seen if first_seen && last_visible
|
126
116
|
end
|
127
117
|
end
|
128
118
|
end
|
data/lib/patronus_fati.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe(PatronusFati::BitHelper) do
|
4
|
+
context '#count_consecutive_bits' do
|
5
|
+
let(:test_set) do
|
6
|
+
[
|
7
|
+
{ bits: "00000000", answer: 0 },
|
8
|
+
{ bits: "11111111", answer: 8 },
|
9
|
+
{ bits: "11001111", answer: 4 },
|
10
|
+
{ bits: "10101010", answer: 1 }
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should correctly count varying number of consecutive bits in a number' do
|
15
|
+
test_set.each do |set|
|
16
|
+
num = set[:bits].to_i(2)
|
17
|
+
expect(described_class.count_consecutive_bits(num)).to eql(set[:answer])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context '#largest_bit_overlap' do
|
23
|
+
let(:test_set) do
|
24
|
+
[
|
25
|
+
{
|
26
|
+
bit_strings: [
|
27
|
+
"000000000000000000000000",
|
28
|
+
"000011100000111000001110",
|
29
|
+
"001111000011110000111100",
|
30
|
+
"111111111111111111111111"
|
31
|
+
],
|
32
|
+
answer: 4
|
33
|
+
},
|
34
|
+
{
|
35
|
+
bit_strings: [
|
36
|
+
"0000"
|
37
|
+
],
|
38
|
+
answer: 0
|
39
|
+
},
|
40
|
+
{
|
41
|
+
bit_strings: [
|
42
|
+
"1111111111111111",
|
43
|
+
"1111111111111111",
|
44
|
+
"0000000000000000"
|
45
|
+
],
|
46
|
+
answer: 16
|
47
|
+
}
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should correctly find the longest run of overlapping bits' do
|
52
|
+
test_set.each do |set|
|
53
|
+
nums = set[:bit_strings].map { |bs| bs.to_i(2) }
|
54
|
+
expect(described_class.largest_bit_overlap(nums)).to eql(set[:answer])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -11,12 +11,27 @@ RSpec.describe(PatronusFati::DataModels::AccessPoint) do
|
|
11
11
|
expect(subject.active_ssids).to be_kind_of(Hash)
|
12
12
|
end
|
13
13
|
|
14
|
-
it 'should not include inactive SSIDs' do
|
14
|
+
it 'should not include inactive SSIDs when an active SSID is present' do
|
15
|
+
inactive_ssid = double(PatronusFati::DataModels::Ssid)
|
16
|
+
expect(inactive_ssid).to receive(:active?).and_return(false)
|
17
|
+
|
18
|
+
active_ssid = double(PatronusFati::DataModels::Ssid)
|
19
|
+
expect(active_ssid).to receive(:active?).and_return(true)
|
20
|
+
|
21
|
+
subject.ssids = { tmp: inactive_ssid, tmp2: active_ssid }
|
22
|
+
expect(subject.active_ssids.values).to_not include(inactive_ssid)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should include the last inactive SSID when no active SSIDs are present' do
|
26
|
+
presence = double(PatronusFati::Presence)
|
27
|
+
expect(presence).to receive(:last_visible).and_return(Time.now.to_i)
|
28
|
+
|
15
29
|
ssid = double(PatronusFati::DataModels::Ssid)
|
16
30
|
expect(ssid).to receive(:active?).and_return(false)
|
31
|
+
expect(ssid).to receive(:presence).and_return(presence)
|
17
32
|
|
18
33
|
subject.ssids = { tmp: ssid }
|
19
|
-
expect(subject.active_ssids.values).
|
34
|
+
expect(subject.active_ssids.values).to include(ssid)
|
20
35
|
end
|
21
36
|
|
22
37
|
it 'should include active SSIDs' do
|
@@ -118,8 +133,8 @@ RSpec.describe(PatronusFati::DataModels::AccessPoint) do
|
|
118
133
|
it 'should include the attributes of active ssids' do
|
119
134
|
subject.ssids = {}
|
120
135
|
ssid_dbl = double(PatronusFati::DataModels::Ssid)
|
121
|
-
expect(subject).to receive(:active_ssids).and_return({ pnt: ssid_dbl })
|
122
|
-
expect(ssid_dbl).to receive(:
|
136
|
+
expect(subject).to receive(:active_ssids).and_return({ pnt: ssid_dbl }).twice
|
137
|
+
expect(ssid_dbl).to receive(:full_state).and_return('data')
|
123
138
|
expect(subject.full_state[:ssids]).to eql(['data'])
|
124
139
|
end
|
125
140
|
end
|
@@ -5,6 +5,21 @@ RSpec.describe(PatronusFati::DataModels::Ssid) do
|
|
5
5
|
|
6
6
|
it_behaves_like 'a common stateful model'
|
7
7
|
|
8
|
+
context '#full_state' do
|
9
|
+
it 'should include all the local attributes' do
|
10
|
+
expect(subject).to receive(:local_attributes).and_return(test: :data)
|
11
|
+
expect(subject.full_state[:test]).to eql(:data)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should include the last time it was seen' do
|
15
|
+
presence_dbl = double(PatronusFati::Presence)
|
16
|
+
expect(presence_dbl).to receive(:last_visible).and_return(1234)
|
17
|
+
|
18
|
+
expect(subject).to receive(:presence).and_return(presence_dbl)
|
19
|
+
expect(subject.full_state[:last_visible]).to eql(1234)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
8
23
|
context '#initialize' do
|
9
24
|
it 'should initialize the local attributes with the essid' do
|
10
25
|
expect(subject.local_attributes.keys).to include(:essid)
|
@@ -28,7 +28,7 @@ RSpec.describe(PatronusFati::Presence) do
|
|
28
28
|
|
29
29
|
context '#rotate_presence' do
|
30
30
|
let(:old_window_start) do
|
31
|
-
subject.current_window_start - (2 * PatronusFati::
|
31
|
+
subject.current_window_start - (2 * PatronusFati::WINDOW_LENGTH)
|
32
32
|
end
|
33
33
|
|
34
34
|
it 'should not modify the last_presence if we\'re still in the window' do
|
@@ -71,14 +71,14 @@ RSpec.describe(PatronusFati::Presence) do
|
|
71
71
|
|
72
72
|
it 'should return the time when the last_visible is in the current window' do
|
73
73
|
# Note: bit 1 == minute 0, this which is why these two numbers differ
|
74
|
-
time = subject.current_window_start + (22 * PatronusFati::
|
74
|
+
time = subject.current_window_start + (22 * PatronusFati::INTERVAL_DURATION)
|
75
75
|
subject.current_presence.set_bit(23)
|
76
76
|
expect(subject.last_visible).to eql(time)
|
77
77
|
end
|
78
78
|
|
79
79
|
it 'should return the time when the last_visisble is in the last window' do
|
80
80
|
# Note: bit 1 == minute 0, this which is why these two numbers differ
|
81
|
-
time = subject.last_window_start + (1 * PatronusFati::
|
81
|
+
time = subject.last_window_start + (1 * PatronusFati::INTERVAL_DURATION)
|
82
82
|
subject.last_presence.set_bit(2)
|
83
83
|
expect(subject.last_visible).to eql(time)
|
84
84
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: patronus_fati
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sam Stelfox
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: louis
|
@@ -142,6 +142,7 @@ files:
|
|
142
142
|
- kismet_configs/patronus_fati_kismet.conf
|
143
143
|
- lib/patronus_fati.rb
|
144
144
|
- lib/patronus_fati/bit_field.rb
|
145
|
+
- lib/patronus_fati/bit_helper.rb
|
145
146
|
- lib/patronus_fati/cap_struct.rb
|
146
147
|
- lib/patronus_fati/connection.rb
|
147
148
|
- lib/patronus_fati/consts.rb
|
@@ -204,6 +205,7 @@ files:
|
|
204
205
|
- lib/patronus_fati/version.rb
|
205
206
|
- patronus_fati.gemspec
|
206
207
|
- spec/patronus_fati/bit_field_spec.rb
|
208
|
+
- spec/patronus_fati/bit_helper_spec.rb
|
207
209
|
- spec/patronus_fati/data_models/access_point_spec.rb
|
208
210
|
- spec/patronus_fati/data_models/client_spec.rb
|
209
211
|
- spec/patronus_fati/data_models/connection_spec.rb
|
@@ -232,12 +234,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
232
234
|
version: '0'
|
233
235
|
requirements: []
|
234
236
|
rubyforge_project:
|
235
|
-
rubygems_version: 2.6.
|
237
|
+
rubygems_version: 2.6.12
|
236
238
|
signing_key:
|
237
239
|
specification_version: 4
|
238
240
|
summary: A ruby implementation of the Kismet client protocol.
|
239
241
|
test_files:
|
240
242
|
- spec/patronus_fati/bit_field_spec.rb
|
243
|
+
- spec/patronus_fati/bit_helper_spec.rb
|
241
244
|
- spec/patronus_fati/data_models/access_point_spec.rb
|
242
245
|
- spec/patronus_fati/data_models/client_spec.rb
|
243
246
|
- spec/patronus_fati/data_models/connection_spec.rb
|