trackerjacker 2.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trackerjacker/__init__.py +11 -0
- trackerjacker/__main__.py +339 -0
- trackerjacker/common.py +6 -0
- trackerjacker/config_management.py +293 -0
- trackerjacker/dot11_frame.py +87 -0
- trackerjacker/dot11_mapper.py +348 -0
- trackerjacker/dot11_tracker.py +323 -0
- trackerjacker/ieee_mac_vendor_db.py +23 -0
- trackerjacker/linux_device_management.py +254 -0
- trackerjacker/macos_device_management.py +300 -0
- trackerjacker/oui.txt +39051 -0
- trackerjacker/plugin_parser.py +82 -0
- trackerjacker/plugins/__init__.py +0 -0
- trackerjacker/plugins/foxhunt.py +92 -0
- trackerjacker/version.py +1 -0
- trackerjacker-2.0.6.dist-info/METADATA +350 -0
- trackerjacker-2.0.6.dist-info/RECORD +21 -0
- trackerjacker-2.0.6.dist-info/WHEEL +5 -0
- trackerjacker-2.0.6.dist-info/entry_points.txt +2 -0
- trackerjacker-2.0.6.dist-info/licenses/LICENSE +21 -0
- trackerjacker-2.0.6.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# pylint: disable=C0111, C0103, W0703, R0902, R0903, R0912, R0913, R0914, R0915, C0413
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import json
|
|
8
|
+
import errno
|
|
9
|
+
import pprint
|
|
10
|
+
import logging
|
|
11
|
+
import platform
|
|
12
|
+
import traceback
|
|
13
|
+
|
|
14
|
+
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
|
|
15
|
+
import scapy.all as scapy
|
|
16
|
+
|
|
17
|
+
from . import config_management
|
|
18
|
+
from . import dot11_frame
|
|
19
|
+
from . import dot11_mapper
|
|
20
|
+
from . import dot11_tracker
|
|
21
|
+
from . import plugin_parser
|
|
22
|
+
from . import ieee_mac_vendor_db
|
|
23
|
+
from .common import TJException
|
|
24
|
+
|
|
25
|
+
if platform.system() == 'Linux':
|
|
26
|
+
from . import linux_device_management as device_management
|
|
27
|
+
elif platform.system() == 'Darwin':
|
|
28
|
+
from . import macos_device_management as device_management
|
|
29
|
+
|
|
30
|
+
LOG_NAME_TO_LEVEL = {'DEBUG': 10, 'INFO': 20, 'WARNING': 30, 'ERROR': 40, 'CRITICAL': 50}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_logger(log_path=None, log_level_str='INFO'):
|
|
34
|
+
logger = logging.getLogger('trackerjacker')
|
|
35
|
+
formatter = logging.Formatter('%(asctime)s: (%(levelname)s): %(message)s')
|
|
36
|
+
|
|
37
|
+
if log_path:
|
|
38
|
+
log_handler = logging.FileHandler(log_path)
|
|
39
|
+
log_handler.setFormatter(formatter)
|
|
40
|
+
logger.addHandler(log_handler)
|
|
41
|
+
|
|
42
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
43
|
+
stderr_handler.setLevel(logging.ERROR)
|
|
44
|
+
stderr_handler.setFormatter(logging.Formatter('%(message)s'))
|
|
45
|
+
logger.addHandler(stderr_handler)
|
|
46
|
+
else:
|
|
47
|
+
log_handler = logging.StreamHandler(sys.stdout)
|
|
48
|
+
log_handler.setFormatter(logging.Formatter('%(message)s'))
|
|
49
|
+
logger.addHandler(log_handler)
|
|
50
|
+
|
|
51
|
+
log_level = LOG_NAME_TO_LEVEL.get(log_level_str.upper(), 20)
|
|
52
|
+
logger.setLevel(log_level)
|
|
53
|
+
return logger
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TrackerJacker:
|
|
57
|
+
def __init__(self,
|
|
58
|
+
logger=None,
|
|
59
|
+
iface=None,
|
|
60
|
+
channels_to_monitor=None,
|
|
61
|
+
channel_switch_scheme='default',
|
|
62
|
+
time_per_channel=2,
|
|
63
|
+
display_matching_packets=False,
|
|
64
|
+
display_all_packets=False,
|
|
65
|
+
# map args
|
|
66
|
+
do_map=True,
|
|
67
|
+
map_file='wifi_map.yaml',
|
|
68
|
+
map_save_interval=10, # seconds
|
|
69
|
+
# track args
|
|
70
|
+
do_track=False,
|
|
71
|
+
threshold=None,
|
|
72
|
+
power=None,
|
|
73
|
+
devices_to_watch=(),
|
|
74
|
+
aps_to_watch=(),
|
|
75
|
+
threshold_window=10,
|
|
76
|
+
trigger_plugin=None,
|
|
77
|
+
plugin_config=None,
|
|
78
|
+
trigger_command=None,
|
|
79
|
+
trigger_cooldown=30, # seconds
|
|
80
|
+
beep_on_trigger=False,
|
|
81
|
+
remove_unseen_devices=False):
|
|
82
|
+
|
|
83
|
+
self.iface = iface
|
|
84
|
+
self.do_map = do_map
|
|
85
|
+
self.do_track = do_track
|
|
86
|
+
self.map_file = map_file
|
|
87
|
+
self.map_save_interval = map_save_interval
|
|
88
|
+
self.display_matching_packets = display_matching_packets
|
|
89
|
+
self.display_all_packets = display_all_packets
|
|
90
|
+
self.mac_vendor_db = ieee_mac_vendor_db.MacVendorDB()
|
|
91
|
+
|
|
92
|
+
if logger:
|
|
93
|
+
self.logger = logger
|
|
94
|
+
else:
|
|
95
|
+
self.logger = make_logger()
|
|
96
|
+
|
|
97
|
+
# Even if we are not in map mode, we still need to build the map for tracking purposes
|
|
98
|
+
self.dot11_map = None
|
|
99
|
+
if self.do_map:
|
|
100
|
+
self.map_last_save = time.time()
|
|
101
|
+
|
|
102
|
+
# Try to load map
|
|
103
|
+
self.logger.info('Map output file: %s', self.map_file)
|
|
104
|
+
if os.path.exists(self.map_file):
|
|
105
|
+
self.dot11_map = dot11_mapper.Dot11Map.load_from_file(self.map_file, remove_unseen_devices)
|
|
106
|
+
if self.dot11_map:
|
|
107
|
+
self.logger.info('Loaded %d devices and %d ssids from %s',
|
|
108
|
+
len(self.dot11_map.devices),
|
|
109
|
+
len(self.dot11_map.ssid_to_access_point),
|
|
110
|
+
self.map_file)
|
|
111
|
+
else:
|
|
112
|
+
self.logger.warning('Specified map file not found - creating new map file.')
|
|
113
|
+
|
|
114
|
+
if not self.dot11_map:
|
|
115
|
+
self.dot11_map = dot11_mapper.Dot11Map(remove_unseen_devices=remove_unseen_devices)
|
|
116
|
+
|
|
117
|
+
self.dot11_map.window = threshold_window
|
|
118
|
+
|
|
119
|
+
if channel_switch_scheme == 'default':
|
|
120
|
+
if self.do_map:
|
|
121
|
+
channel_switch_scheme = 'round_robin'
|
|
122
|
+
else: # track mode
|
|
123
|
+
channel_switch_scheme = 'traffic_based'
|
|
124
|
+
|
|
125
|
+
print(f'devices_to_watch = {devices_to_watch}, aps_to_watch = {aps_to_watch}')
|
|
126
|
+
self.devices_to_watch_set = set([dev['mac'].lower() for dev in devices_to_watch if 'mac' in dev])
|
|
127
|
+
self.aps_to_watch_set = set([ap['bssid'].lower() for ap in aps_to_watch if 'bssid' in ap])
|
|
128
|
+
|
|
129
|
+
if len(self.devices_to_watch_set) > 0:
|
|
130
|
+
print(f'Tracking devices: {self.devices_to_watch_set}')
|
|
131
|
+
if len(self.aps_to_watch_set) > 0:
|
|
132
|
+
print(f'Tracking Access Points: {self.aps_to_watch_set}')
|
|
133
|
+
|
|
134
|
+
if self.do_track:
|
|
135
|
+
# Build trigger hit function
|
|
136
|
+
if trigger_plugin:
|
|
137
|
+
trigger_plugin = config_management.get_real_plugin_path(trigger_plugin)
|
|
138
|
+
parsed_trigger_plugin = plugin_parser.parse_trigger_plugin(trigger_plugin, plugin_config)
|
|
139
|
+
else:
|
|
140
|
+
parsed_trigger_plugin = None
|
|
141
|
+
|
|
142
|
+
self.dot11_tracker = dot11_tracker.Dot11Tracker(self.logger,
|
|
143
|
+
threshold,
|
|
144
|
+
power,
|
|
145
|
+
devices_to_watch,
|
|
146
|
+
aps_to_watch,
|
|
147
|
+
parsed_trigger_plugin,
|
|
148
|
+
trigger_command,
|
|
149
|
+
trigger_cooldown,
|
|
150
|
+
threshold_window,
|
|
151
|
+
beep_on_trigger,
|
|
152
|
+
self.dot11_map)
|
|
153
|
+
|
|
154
|
+
self.iface_manager = device_management.Dot11InterfaceManager(iface,
|
|
155
|
+
self.logger,
|
|
156
|
+
channels_to_monitor,
|
|
157
|
+
channel_switch_scheme,
|
|
158
|
+
time_per_channel)
|
|
159
|
+
|
|
160
|
+
def process_packet(self, pkt):
|
|
161
|
+
try:
|
|
162
|
+
if pkt.haslayer(scapy.Dot11):
|
|
163
|
+
looking_for_specifics_and_none_found = self.aps_to_watch_set or self.devices_to_watch_set
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
frame = dot11_frame.Dot11Frame(pkt,
|
|
167
|
+
int(self.iface_manager.current_channel),
|
|
168
|
+
iface=self.iface_manager.iface)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
# Thank you DEF CON (https://github.com/secdev/scapy/issues/1552)
|
|
171
|
+
self.logger.warning('Error decoding Dot11Frame: %s', e)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if self.do_map:
|
|
175
|
+
self.log_newly_found(frame)
|
|
176
|
+
|
|
177
|
+
if self.display_all_packets:
|
|
178
|
+
print('\t', pkt.summary())
|
|
179
|
+
|
|
180
|
+
# See if any APs we care about (if we're looking for specific APs)
|
|
181
|
+
if self.aps_to_watch_set:
|
|
182
|
+
if frame.bssid not in self.aps_to_watch_set:
|
|
183
|
+
looking_for_specifics_and_none_found = False
|
|
184
|
+
|
|
185
|
+
# See if any MACs we care about (if we're looking for specific MACs)
|
|
186
|
+
if self.devices_to_watch_set:
|
|
187
|
+
matched_macs = self.devices_to_watch_set & frame.macs
|
|
188
|
+
if matched_macs:
|
|
189
|
+
looking_for_specifics_and_none_found = False
|
|
190
|
+
|
|
191
|
+
# Display matched packets (if specified)
|
|
192
|
+
if self.display_matching_packets and not self.display_all_packets:
|
|
193
|
+
print('\t', pkt.summary())
|
|
194
|
+
|
|
195
|
+
# If we are looking for specific APs or Devices and none are found, no further processing needed
|
|
196
|
+
if looking_for_specifics_and_none_found:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# If map mode enabled, do it. Note that we don't exclude non-matching MACs from the mapping
|
|
200
|
+
# (which is why this isn't under the 'if matched_matcs' block).
|
|
201
|
+
# Note: we update the map whether do_map is true or false since it's used for tracking; just don't save map
|
|
202
|
+
self.dot11_map.add_frame(frame)
|
|
203
|
+
if self.do_map:
|
|
204
|
+
if time.time() - self.map_last_save >= self.map_save_interval:
|
|
205
|
+
self.dot11_map.save_to_file(self.map_file)
|
|
206
|
+
self.map_last_save = time.time()
|
|
207
|
+
|
|
208
|
+
if self.do_track:
|
|
209
|
+
self.dot11_tracker.add_frame(frame, pkt)
|
|
210
|
+
|
|
211
|
+
# Update device tracking (for traffic-based)
|
|
212
|
+
self.iface_manager.add_frame(frame)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self.logger.debug('Error in process_packet: {e}')
|
|
215
|
+
|
|
216
|
+
def log_newly_found(self, frame):
|
|
217
|
+
# Log newly-found things
|
|
218
|
+
if frame.ssid and frame.bssid not in self.dot11_map.access_points.keys():
|
|
219
|
+
self.logger.info('SSID found: %s, BSSID: %s, Channel: %d', frame.ssid, frame.bssid, frame.channel)
|
|
220
|
+
|
|
221
|
+
new_macs = [mac for mac in frame.macs
|
|
222
|
+
if mac not in (self.dot11_map.devices.keys() |
|
|
223
|
+
self.dot11_map.access_points.keys() |
|
|
224
|
+
dot11_mapper.MACS_TO_IGNORE)]
|
|
225
|
+
for mac in new_macs:
|
|
226
|
+
if mac: # The frame can be crafted to include a null mac
|
|
227
|
+
self.logger.info('MAC found: %s, Channel: %d', mac, frame.channel)
|
|
228
|
+
|
|
229
|
+
def start(self):
|
|
230
|
+
self.logger.debug('Starting monitoring on %s', self.iface_manager.iface)
|
|
231
|
+
self.iface_manager.start()
|
|
232
|
+
while True:
|
|
233
|
+
try:
|
|
234
|
+
# macOS
|
|
235
|
+
if platform.system() == 'Darwin':
|
|
236
|
+
self.logger.warning('macOS support is pre-alpha - many improvements coming soon')
|
|
237
|
+
scapy.sniff(iface=self.iface_manager.iface, monitor=True, prn=self.process_packet, store=0, count=0)
|
|
238
|
+
break
|
|
239
|
+
# linux
|
|
240
|
+
else:
|
|
241
|
+
# For versions of scapy that don't provide the exceptions kwarg
|
|
242
|
+
scapy.sniff(iface=self.iface_manager.iface, prn=self.process_packet, store=0, count=0)
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
except TJException:
|
|
246
|
+
raise
|
|
247
|
+
except (OSError, IOError):
|
|
248
|
+
self.logger.error(traceback.format_exc())
|
|
249
|
+
self.logger.info('Sniffer error occurred. Restarting sniffer in 3 seconds...')
|
|
250
|
+
time.sleep(3)
|
|
251
|
+
|
|
252
|
+
def stop(self):
|
|
253
|
+
self.iface_manager.stop()
|
|
254
|
+
|
|
255
|
+
if self.do_map:
|
|
256
|
+
# Flush map to disk
|
|
257
|
+
self.dot11_map.save_to_file(self.map_file)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def do_simple_tasks_if_specified(args):
|
|
261
|
+
if args.version:
|
|
262
|
+
from .version import __version__
|
|
263
|
+
print('trackerjacker {}'.format(__version__))
|
|
264
|
+
sys.exit(0)
|
|
265
|
+
elif args.do_enable_monitor_mode:
|
|
266
|
+
if not args.iface:
|
|
267
|
+
raise TJException('You must specify the interface with the -i paramter')
|
|
268
|
+
device_management.monitor_mode_on(args.iface)
|
|
269
|
+
print('Enabled monitor mode on {}'.format(args.iface))
|
|
270
|
+
sys.exit(0)
|
|
271
|
+
elif args.do_disable_monitor_mode:
|
|
272
|
+
if not args.iface:
|
|
273
|
+
raise TJException('You must specify the interface with the -i paramter')
|
|
274
|
+
device_management.monitor_mode_off(args.iface)
|
|
275
|
+
print('Disabled monitor mode on {}'.format(args.iface))
|
|
276
|
+
sys.exit(0)
|
|
277
|
+
elif args.mac_lookup:
|
|
278
|
+
vendor = ieee_mac_vendor_db.MacVendorDB().lookup(args.mac_lookup)
|
|
279
|
+
if vendor:
|
|
280
|
+
print(vendor)
|
|
281
|
+
else:
|
|
282
|
+
print('Vendor for {} not found'.format(args.mac_lookup), file=sys.stderr)
|
|
283
|
+
sys.exit(0)
|
|
284
|
+
elif args.print_default_config:
|
|
285
|
+
print(json.dumps(config_management.DEFAULT_CONFIG, indent=4, sort_keys=True))
|
|
286
|
+
sys.exit(0)
|
|
287
|
+
elif args.set_channel:
|
|
288
|
+
if not args.iface:
|
|
289
|
+
raise TJException('You must specify the interface with the -i paramter')
|
|
290
|
+
channel = args.set_channel[0]
|
|
291
|
+
device_management.switch_to_channel(args.iface, channel)
|
|
292
|
+
print('Set channel to {} on {}'.format(channel, args.iface))
|
|
293
|
+
sys.exit(0)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def main():
|
|
297
|
+
argparse_args = config_management.get_arg_parser().parse_args()
|
|
298
|
+
|
|
299
|
+
# Some command-line args specify to just perform a simple task and then exit
|
|
300
|
+
try:
|
|
301
|
+
do_simple_tasks_if_specified(argparse_args)
|
|
302
|
+
except TJException as e:
|
|
303
|
+
print('Error: {}'.format(e), file=sys.stderr)
|
|
304
|
+
sys.exit(1)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
config = config_management.build_config(argparse_args)
|
|
308
|
+
except TJException as e:
|
|
309
|
+
print('{}'.format(e))
|
|
310
|
+
sys.exit(1)
|
|
311
|
+
|
|
312
|
+
if config['log_level'] == 'DEBUG':
|
|
313
|
+
print('Config:')
|
|
314
|
+
pprint.pprint(config)
|
|
315
|
+
|
|
316
|
+
# Setup logger
|
|
317
|
+
logger = make_logger(config.pop('log_path'), config.pop('log_level'))
|
|
318
|
+
|
|
319
|
+
# Any actual trackerjacker usage requires root (monitor mode in scapy requires it)
|
|
320
|
+
if not os.getuid() == 0:
|
|
321
|
+
print('trackerjacker requires r00t!', file=sys.stderr)
|
|
322
|
+
sys.exit(errno.EPERM)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
tj = TrackerJacker(**dict(config, **{'logger': logger})) # pylint: disable=E1123
|
|
326
|
+
tj.start()
|
|
327
|
+
except TJException as e:
|
|
328
|
+
logger.critical('Error: %s', e)
|
|
329
|
+
except KeyboardInterrupt:
|
|
330
|
+
print('Stopping...')
|
|
331
|
+
finally:
|
|
332
|
+
try:
|
|
333
|
+
tj.stop()
|
|
334
|
+
except UnboundLocalError:
|
|
335
|
+
# Exception was thrown in TrackerJacker initializer, so 'tj' doesn't exist
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
if __name__ == '__main__':
|
|
339
|
+
main()
|
trackerjacker/common.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# pylint: disable=C0111, C0103, W0703, R0902, R0903, R0912, R0913, R0914, R0915, C0413
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import argparse
|
|
8
|
+
from .common import TJException
|
|
9
|
+
from . import plugin_parser
|
|
10
|
+
|
|
11
|
+
# Default config
|
|
12
|
+
DEFAULT_CONFIG = {'log_path': None,
|
|
13
|
+
'log_level': 'INFO',
|
|
14
|
+
'iface': None,
|
|
15
|
+
'devices_to_watch': [],
|
|
16
|
+
'aps_to_watch': [],
|
|
17
|
+
'threshold': None,
|
|
18
|
+
'power': None,
|
|
19
|
+
'threshold_window': 10,
|
|
20
|
+
'do_map': True,
|
|
21
|
+
'do_track': False,
|
|
22
|
+
'map_file': 'wifi_map.yaml',
|
|
23
|
+
'map_save_interval': 10,
|
|
24
|
+
'trigger_plugin': None,
|
|
25
|
+
'plugin_config': None,
|
|
26
|
+
'trigger_command': None,
|
|
27
|
+
'trigger_cooldown': 30,
|
|
28
|
+
'channels_to_monitor': None,
|
|
29
|
+
'channel_switch_scheme': 'default',
|
|
30
|
+
'time_per_channel': 0.5,
|
|
31
|
+
'display_matching_packets': False,
|
|
32
|
+
'display_all_packets': False,
|
|
33
|
+
'beep_on_trigger': False,
|
|
34
|
+
'remove_unseen_devices': False}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_arg_parser():
|
|
38
|
+
"""Returns the configured argparse object."""
|
|
39
|
+
parser = argparse.ArgumentParser()
|
|
40
|
+
# Modes
|
|
41
|
+
parser.add_argument('--map', action='store_true', dest='do_map',
|
|
42
|
+
help='Map mode - output map to wifi_map.yaml')
|
|
43
|
+
parser.add_argument('--track', action='store_true', dest='do_track',
|
|
44
|
+
help='Track mode')
|
|
45
|
+
parser.add_argument('--remove-unseen-devices', action='store_true', dest='remove_unseen_devices',
|
|
46
|
+
help='If enabled, map will forget about devices not seen for over five minutes')
|
|
47
|
+
parser.add_argument('--monitor-mode-on', action='store_true', dest='do_enable_monitor_mode',
|
|
48
|
+
help='Enables monitor mode on the specified interface and exit')
|
|
49
|
+
parser.add_argument('--monitor-mode-off', action='store_true', dest='do_disable_monitor_mode',
|
|
50
|
+
help='Disables monitor mode on the specified interface and exit')
|
|
51
|
+
parser.add_argument('--set-channel', metavar='CHANNEL', dest='set_channel', nargs=1,
|
|
52
|
+
help='Set the specified wireless interface to the specified channel and exit')
|
|
53
|
+
parser.add_argument('--mac-lookup', type=str, dest='mac_lookup',
|
|
54
|
+
help='Lookup the vendor of the specified MAC address and exit')
|
|
55
|
+
parser.add_argument('--print-default-config', action='store_true', dest='print_default_config',
|
|
56
|
+
help='Print boilerplate config file and exit')
|
|
57
|
+
|
|
58
|
+
# Normal switches
|
|
59
|
+
parser.add_argument('-v', '--version', action='store_true', dest='version',
|
|
60
|
+
help='Display trackerjacker version')
|
|
61
|
+
parser.add_argument('-i', '--interface', type=str, dest='iface',
|
|
62
|
+
help='Network interface to use; if empty, try to find monitor inferface')
|
|
63
|
+
parser.add_argument('-m', '--macs', type=str, dest='devices_to_watch',
|
|
64
|
+
help='MAC(s) to track; comma separated for multiple')
|
|
65
|
+
parser.add_argument('-a', '--access-points', type=str, dest='aps_to_watch',
|
|
66
|
+
help='Access point(s) to track - specified by BSSID; comma separated for multiple')
|
|
67
|
+
parser.add_argument('--channels-to-monitor', type=str, dest='channels_to_monitor',
|
|
68
|
+
help='Channels to monitor; comma separated for multiple')
|
|
69
|
+
parser.add_argument('--channel-switch-scheme', type=str, dest='channel_switch_scheme',
|
|
70
|
+
help='Options: "round_robin" or "traffic_based"', default='default')
|
|
71
|
+
parser.add_argument('--time-per-channel', type=float, dest='time_per_channel',
|
|
72
|
+
help='Seconds spent on each channel before hopping')
|
|
73
|
+
parser.add_argument('-w', '--time-window', type=int, dest='threshold_window',
|
|
74
|
+
help='Time window (in seconds) which alert threshold is applied to')
|
|
75
|
+
parser.add_argument('--map-save-interval', type=float, dest='map_save_interval',
|
|
76
|
+
help='Number of seconds between saving the wifi map to disk')
|
|
77
|
+
parser.add_argument('--threshold', type=int, dest='threshold',
|
|
78
|
+
help='Default data threshold (unless overridden on a per-dev basis) for triggering')
|
|
79
|
+
parser.add_argument('--power', type=int, dest='power',
|
|
80
|
+
help='Default power threshold (unless overridden on a per-dev basis) for triggering')
|
|
81
|
+
parser.add_argument('--plugin', type=str, dest='trigger_plugin',
|
|
82
|
+
help='Python trigger plugin file path; for more information')
|
|
83
|
+
parser.add_argument('--trigger-plugin', type=str, dest='trigger_plugin',
|
|
84
|
+
help='Python trigger plugin file path; for more information')
|
|
85
|
+
parser.add_argument('--plugin-config', type=str, dest='plugin_config',
|
|
86
|
+
help='Config to pass to python trigger plugin. Must be a python dict or json obj.')
|
|
87
|
+
parser.add_argument('--trigger-command', type=str, dest='trigger_command',
|
|
88
|
+
help='Command to execute upon alert')
|
|
89
|
+
parser.add_argument('--trigger-cooldown', type=str, dest='trigger_cooldown',
|
|
90
|
+
help='Time in seconds between trigger executions for a particular device')
|
|
91
|
+
parser.add_argument('--display-all-packets', action='store_true', dest='display_all_packets',
|
|
92
|
+
help='If true, displays all packets matching filters')
|
|
93
|
+
parser.add_argument('--beep-on-trigger', action='store_true', dest='beep_on_trigger',
|
|
94
|
+
help='If enabled, beep each time a trigger hits (off by default)')
|
|
95
|
+
parser.add_argument('--map-file', type=str, dest='map_file', default='wifi_map.yaml',
|
|
96
|
+
help='File path to which to output wifi map; default: wifi_map.yaml')
|
|
97
|
+
parser.add_argument('--log-path', type=str, dest='log_path', default=None,
|
|
98
|
+
help='Log path; default is stdout')
|
|
99
|
+
parser.add_argument('--log-level', type=str, dest='log_level', default='INFO',
|
|
100
|
+
help='Log level; Options: DEBUG, INFO, WARNING, ERROR, CRITICAL')
|
|
101
|
+
parser.add_argument('-c', '--config', type=str, dest='config',
|
|
102
|
+
help='Path to config json file; For example config file, use --print-default-config')
|
|
103
|
+
return parser
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_command_line_watch_list(watch_str):
|
|
107
|
+
"""Parse string that represents devices to watch.
|
|
108
|
+
|
|
109
|
+
Valid examples:
|
|
110
|
+
* aa:bb:cc:dd:ee:ff
|
|
111
|
+
- Threshold of 1 for the given MAC address
|
|
112
|
+
* aa:bb:cc:dd:ee:ff,11:22:33:44:55:66
|
|
113
|
+
- This means look for any traffic from either address
|
|
114
|
+
* aa:bb:cc:dd:ee:ff=1337, 11:22:33:44:55:66=1000
|
|
115
|
+
- This means look for 1337 bytes for the first address, and 1000 for the second
|
|
116
|
+
* my_ssid, 11:22:33:44:55:66=1000
|
|
117
|
+
- This means look for 1 byte from my_ssid or 1000 for the second
|
|
118
|
+
* 11:22:33:44:55:66=-30
|
|
119
|
+
- This means trigger if 11:22:33:44:55:66 is seen at a power level >= -30dBm (negative value implies power)
|
|
120
|
+
|
|
121
|
+
Returns dict in this format:
|
|
122
|
+
{'aa:bb:cc:dd:ee:ff': {'threshold': 100, 'power': None},
|
|
123
|
+
'11:22:33:44:55:66': {'threshold': None, 'power': -30}}
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
watch_list = [i.strip() for i in watch_str.split(',')]
|
|
127
|
+
watch_dict = {}
|
|
128
|
+
|
|
129
|
+
for watch_part in watch_list:
|
|
130
|
+
power = None
|
|
131
|
+
threshold = None
|
|
132
|
+
|
|
133
|
+
if '=' in watch_part:
|
|
134
|
+
# dev_id is a MAC, BSSID, or SSID
|
|
135
|
+
dev_id, val = [i.strip() for i in watch_part.split('=')]
|
|
136
|
+
try:
|
|
137
|
+
val = int(val)
|
|
138
|
+
except ValueError:
|
|
139
|
+
# Can't parse with "dev_id=threshold" formula, so assume '=' sign was part of ssid
|
|
140
|
+
dev_id = watch_part
|
|
141
|
+
|
|
142
|
+
if val > 0:
|
|
143
|
+
threshold = val
|
|
144
|
+
else:
|
|
145
|
+
power = val
|
|
146
|
+
else:
|
|
147
|
+
dev_id = watch_part
|
|
148
|
+
|
|
149
|
+
watch_dict[dev_id] = {'threshold': threshold, 'power': power}
|
|
150
|
+
|
|
151
|
+
return watch_dict
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def determine_watch_list(to_watch_from_args,
|
|
155
|
+
to_watch_from_config,
|
|
156
|
+
generic_threshold,
|
|
157
|
+
generic_power):
|
|
158
|
+
"""Builds the list of devices to watch.
|
|
159
|
+
|
|
160
|
+
Coalesces the to_watch list from the command-line arguments, the config file,
|
|
161
|
+
and the the general threshold, power, and trigger command. The main idea here is to look
|
|
162
|
+
for the config values set on a per-device basis, and prioritize those, but if they are not there,
|
|
163
|
+
fall back to the "generic_*" version. And if those have not been specified, fall back to defaults.
|
|
164
|
+
|
|
165
|
+
Example input:
|
|
166
|
+
to_watch_from_args = 'aa:bb:cc:dd:ee:ff=1337, 11:22:33:44:55:66=100, ff:ee:dd:cc:bb:aa',
|
|
167
|
+
to_watch_from_config = {}
|
|
168
|
+
generic_threshold = 1337
|
|
169
|
+
generic_power = None
|
|
170
|
+
|
|
171
|
+
Returns a dict in this format:
|
|
172
|
+
{
|
|
173
|
+
'aa:bb:cc:dd:ee:ff': {'threshold': None, 'power': -40},
|
|
174
|
+
'11:22:33:44:55:66': {'threshold': 100, 'power': None},
|
|
175
|
+
'ff:ee:dd:cc:bb:aa': {'threshold': 1337, 'power': None}
|
|
176
|
+
}
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
# Converts from cli param format like: "aa:bb:cc:dd:ee:ff=-40,11:22:33:44:55:66=100' to a map like:
|
|
180
|
+
# {'aa:bb:cc:dd:ee:ff': {'threshold': None, 'power': -40},
|
|
181
|
+
# '11:22:33:44:55:66': {'threshold': 100, 'power': None}
|
|
182
|
+
if to_watch_from_args:
|
|
183
|
+
to_watch_from_args = parse_command_line_watch_list(to_watch_from_args)
|
|
184
|
+
|
|
185
|
+
if not to_watch_from_args:
|
|
186
|
+
to_watch_from_args = {}
|
|
187
|
+
if not to_watch_from_config:
|
|
188
|
+
to_watch_from_config = {}
|
|
189
|
+
|
|
190
|
+
watch_config_dict = {}
|
|
191
|
+
for dev_id in to_watch_from_args.keys() | to_watch_from_config.keys():
|
|
192
|
+
watch_entry = {'threshold': None,
|
|
193
|
+
'power': None}
|
|
194
|
+
watch_entry['threshold'] = (to_watch_from_args.get(dev_id, {}).get('threshold', None) or
|
|
195
|
+
to_watch_from_config.get(dev_id, {}).get('threshold', None))
|
|
196
|
+
watch_entry['power'] = (to_watch_from_args.get(dev_id, {}).get('power', None) or
|
|
197
|
+
to_watch_from_config.get(dev_id, {}).get('power', None))
|
|
198
|
+
|
|
199
|
+
if not watch_entry['threshold'] and not watch_entry['power']:
|
|
200
|
+
if generic_threshold and not generic_power:
|
|
201
|
+
watch_entry['threshold'] = generic_threshold
|
|
202
|
+
elif generic_power:
|
|
203
|
+
watch_entry['power'] = generic_power
|
|
204
|
+
else:
|
|
205
|
+
watch_entry['threshold'] = 1
|
|
206
|
+
|
|
207
|
+
watch_config_dict[dev_id] = watch_entry
|
|
208
|
+
|
|
209
|
+
return watch_config_dict
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_config(args):
|
|
213
|
+
"""Builds the config from the command-line args, config file, and defaults."""
|
|
214
|
+
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
215
|
+
devices_from_config = {}
|
|
216
|
+
aps_from_config = {}
|
|
217
|
+
|
|
218
|
+
if args.config:
|
|
219
|
+
try:
|
|
220
|
+
with open(args.config, 'r') as f:
|
|
221
|
+
config_from_file = json.loads(f.read())
|
|
222
|
+
|
|
223
|
+
# If there are any keys defined in the config file not allowed, error out
|
|
224
|
+
invalid_keys = set(config_from_file.keys()) - set(config.keys())
|
|
225
|
+
if invalid_keys:
|
|
226
|
+
raise TJException('Invalid keys found in config file: {}'.format(invalid_keys))
|
|
227
|
+
|
|
228
|
+
devices_from_config = config_from_file.pop('devices_to_watch', {})
|
|
229
|
+
aps_from_config = config_from_file.pop('aps_to_watch', {})
|
|
230
|
+
|
|
231
|
+
config.update(config_from_file)
|
|
232
|
+
print('Loaded configuration from {}'.format(args.config))
|
|
233
|
+
|
|
234
|
+
except (IOError, OSError, json.decoder.JSONDecodeError) as e:
|
|
235
|
+
raise TJException('Error loading config file ({}): {}'.format(args.config, e))
|
|
236
|
+
|
|
237
|
+
non_config_args = {'config', 'devices_to_watch', 'aps_to_watch', 'do_enable_monitor_mode', 'version',
|
|
238
|
+
'do_disable_monitor_mode', 'set_channel', 'print_default_config', 'mac_lookup'}
|
|
239
|
+
|
|
240
|
+
config_from_args = vars(args)
|
|
241
|
+
config_from_args = {k: v for k, v in config_from_args.items()
|
|
242
|
+
if v is not None and k not in non_config_args}
|
|
243
|
+
|
|
244
|
+
# Config from args trumps default or config file
|
|
245
|
+
config.update(config_from_args)
|
|
246
|
+
|
|
247
|
+
# Allow any plugins to override config
|
|
248
|
+
if config['trigger_plugin']:
|
|
249
|
+
trigger_plugin_path = get_real_plugin_path(config['trigger_plugin'])
|
|
250
|
+
parsed_trigger_plugin = plugin_parser.parse_trigger_plugin(trigger_plugin_path,
|
|
251
|
+
config['plugin_config'],
|
|
252
|
+
parse_only=True)
|
|
253
|
+
|
|
254
|
+
# Allow plugin to override any config parameters
|
|
255
|
+
if 'config' in parsed_trigger_plugin:
|
|
256
|
+
trigger_config = parsed_trigger_plugin['config']
|
|
257
|
+
config.update(**trigger_config)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
config['trigger_cooldown'] = float(config['trigger_cooldown'])
|
|
261
|
+
except ValueError:
|
|
262
|
+
raise TJException('trigger_cooldown must be a number')
|
|
263
|
+
|
|
264
|
+
# If we're in track mode and no other threshold info is set, default to a 1 byte data threshold
|
|
265
|
+
if config['do_track']:
|
|
266
|
+
if not config['threshold'] and not config['power']:
|
|
267
|
+
config['threshold'] = 1
|
|
268
|
+
|
|
269
|
+
config['devices_to_watch'] = determine_watch_list(args.devices_to_watch,
|
|
270
|
+
devices_from_config,
|
|
271
|
+
config['threshold'],
|
|
272
|
+
config['power'])
|
|
273
|
+
|
|
274
|
+
config['aps_to_watch'] = determine_watch_list(args.aps_to_watch,
|
|
275
|
+
aps_from_config,
|
|
276
|
+
config['threshold'],
|
|
277
|
+
config['power'])
|
|
278
|
+
|
|
279
|
+
if args.channels_to_monitor:
|
|
280
|
+
channels_to_monitor = args.channels_to_monitor.split(',')
|
|
281
|
+
config['channels_to_monitor'] = channels_to_monitor
|
|
282
|
+
|
|
283
|
+
return config
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def get_real_plugin_path(trigger_plugin):
|
|
287
|
+
if not trigger_plugin.lower().endswith('.py') and '/' not in trigger_plugin:
|
|
288
|
+
possible_builtin_path = os.path.join(os.path.dirname(__file__),
|
|
289
|
+
'plugins',
|
|
290
|
+
'{}.py'.format(trigger_plugin))
|
|
291
|
+
if os.path.exists(possible_builtin_path):
|
|
292
|
+
trigger_plugin = possible_builtin_path
|
|
293
|
+
return trigger_plugin
|