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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0cb6f174187ef3f5adfa38bc95f3a6492aec44e3
4
- data.tar.gz: 8bc31ce133c6bd96416e1e32c1211c19d0e0e5f7
3
+ metadata.gz: 29f01b90c143b565522531ec08eb46baaa467b3e
4
+ data.tar.gz: 797ee832481e9ad310001f22d8db6ef438eb0131
5
5
  SHA512:
6
- metadata.gz: 881e15810f934ba0595c198a02f3d5de671a444bcc58c8719975b9de9fb070e79ac886f548e0b4b17b0701584fd05800d7063bc8971efaa7b2538582a692fb12
7
- data.tar.gz: 38487f6aee94ea8e48badf6a811b4d10fa8fbbd48db1c4ebb4393d753e19f60d7c55269405cb89ad7c4bb688bdb62400315643ade808ec27d3cf4af2c8d01b48
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
@@ -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
- ssids.select { |_, v| v.active? } if ssids
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(&:local_attributes) if ssids
107
+ state[:ssids] = active_ssids.values.map(&:full_state) if ssids
79
108
  state
80
109
  end
81
110
 
@@ -21,6 +21,10 @@ module PatronusFati
21
21
  }
22
22
  end
23
23
 
24
+ def full_state
25
+ { last_visible: presence.last_visible }.merge(local_attributes)
26
+ end
27
+
24
28
  def update(attrs)
25
29
  attrs.each do |k, v|
26
30
  next unless LOCAL_ATTRIBUTE_KEYS.include?(k)
@@ -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 <= last_visible
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
@@ -1,3 +1,3 @@
1
1
  module PatronusFati
2
- VERSION = '1.2.2'
2
+ VERSION = '1.3.0'
3
3
  end
data/lib/patronus_fati.rb CHANGED
@@ -13,6 +13,7 @@ require 'louis'
13
13
  require 'patronus_fati/consts'
14
14
  require 'patronus_fati/version'
15
15
 
16
+ require 'patronus_fati/bit_helper'
16
17
  require 'patronus_fati/cap_struct'
17
18
  require 'patronus_fati/connection'
18
19
  require 'patronus_fati/event_handler'
@@ -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).to_not include(ssid)
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(:local_attributes).and_return('data')
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::Presence::WINDOW_LENGTH)
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::Presence::INTERVAL_DURATION)
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::Presence::INTERVAL_DURATION)
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.2.2
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-06-05 00:00:00.000000000 Z
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.10
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