Simone Margaritelli cbf7511e17 misc: refactored code to work as a setup.py based package
Signed-off-by: Simone Margaritelli <evilsocket@gmail.com>
2019-10-05 23:23:31 +02:00

550 lines
21 KiB
Python

import time
import json
import os
import re
import socket
from datetime import datetime
import logging
import _thread
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.bettercap import Client
from pwnagotchi.mesh.utils import AsyncAdvertiser
from pwnagotchi.ai.train import AsyncTrainer
RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
class Agent(Client, AsyncAdvertiser, AsyncTrainer):
def __init__(self, view, config):
Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'],
config['bettercap']['port'],
config['bettercap']['username'],
config['bettercap']['password'])
AsyncAdvertiser.__init__(self, config, view)
AsyncTrainer.__init__(self, config)
self._started_at = time.time()
self._filter = None if config['main']['filter'] is None else re.compile(config['main']['filter'])
self._current_channel = 0
self._supported_channels = utils.iface_channels(config['main']['iface'])
self._view = view
self._access_points = []
self._last_pwnd = None
self._history = {}
self._handshakes = {}
@staticmethod
def is_connected():
try:
socket.create_connection(("www.google.com", 80))
return True
except OSError:
pass
return False
def config(self):
return self._config
def supported_channels(self):
return self._supported_channels
def set_starting(self):
self._view.on_starting()
def set_ready(self):
plugins.on('ready', self)
def set_free_channel(self, channel):
self._view.on_free_channel(channel)
plugins.on('free_channel', self, channel)
def set_bored(self):
self._view.on_bored()
plugins.on('bored', self)
def set_sad(self):
self._view.on_sad()
plugins.on('sad', self)
def set_excited(self):
self._view.on_excited()
plugins.on('excited', self)
def set_lonely(self):
self._view.on_lonely()
plugins.on('lonely', self)
def set_rebooting(self):
self._view.on_rebooting()
plugins.on('rebooting', self)
def setup_events(self):
logging.info("connecting to %s ..." % self.url)
for tag in self._config['bettercap']['silence']:
try:
self.run('events.ignore %s' % tag, verbose_errors=False)
except Exception as e:
pass
def _reset_wifi_settings(self):
mon_iface = self._config['main']['iface']
self.run('set wifi.interface %s' % mon_iface)
self.run('set wifi.ap.ttl %d' % self._config['personality']['ap_ttl'])
self.run('set wifi.sta.ttl %d' % self._config['personality']['sta_ttl'])
self.run('set wifi.rssi.min %d' % self._config['personality']['min_rssi'])
self.run('set wifi.handshakes.file %s' % self._config['bettercap']['handshakes'])
self.run('set wifi.handshakes.aggregate false')
def start_monitor_mode(self):
mon_iface = self._config['main']['iface']
mon_start_cmd = self._config['main']['mon_start_cmd']
restart = not self._config['main']['no_restart']
has_mon = False
while has_mon is False:
s = self.session()
for iface in s['interfaces']:
if iface['name'] == mon_iface:
logging.info("found monitor interface: %s" % iface['name'])
has_mon = True
break
if has_mon is False:
if mon_start_cmd is not None and mon_start_cmd != '':
logging.info("starting monitor interface ...")
self.run('!%s' % mon_start_cmd)
else:
logging.info("waiting for monitor interface %s ..." % mon_iface)
time.sleep(1)
logging.info("supported channels: %s" % self._supported_channels)
logging.info("handshakes will be collected inside %s" % self._config['bettercap']['handshakes'])
self._reset_wifi_settings()
wifi_running = self.is_module_running('wifi')
if wifi_running and restart:
logging.debug("restarting wifi module ...")
self.restart_module('wifi.recon')
self.run('wifi.clear')
elif not wifi_running:
logging.debug("starting wifi module ...")
self.start_module('wifi.recon')
self.start_advertising()
def start(self):
self.start_ai()
self.setup_events()
self.set_starting()
self.start_monitor_mode()
self.start_event_polling()
# print initial stats
self.next_epoch()
self.set_ready()
def wait_for(self, t, sleeping=True):
plugins.on('sleep' if sleeping else 'wait', self, t)
self._view.wait(t, sleeping)
self._epoch.track(sleep=True, inc=t)
def check_channels(self, channels):
busy_channels = [ch for ch, aps in channels]
# if we're hopping and no filter is configured
if self._config['personality']['channels'] == [] and self._config['main']['filter'] is None:
# check if any of the non overlapping channels is free
for ch in self._epoch.non_overlapping_channels:
if ch not in busy_channels:
self._epoch.non_overlapping_channels[ch] += 1
logging.info("channel %d is free from %d epochs" % (ch, self._epoch.non_overlapping_channels[ch]))
elif self._epoch.non_overlapping_channels[ch] > 0:
self._epoch.non_overlapping_channels[ch] -= 1
# report any channel that has been free for at least 3 epochs
for ch, num_epochs_free in self._epoch.non_overlapping_channels.items():
if num_epochs_free >= 3:
logging.info("channel %d has been free for %d epochs" % (ch, num_epochs_free))
self.set_free_channel(ch)
def recon(self):
recon_time = self._config['personality']['recon_time']
max_inactive = self._config['personality']['max_inactive_scale']
recon_mul = self._config['personality']['recon_inactive_multiplier']
channels = self._config['personality']['channels']
if self._epoch.inactive_for >= max_inactive:
recon_time *= recon_mul
self._view.set('channel', '*')
if not channels:
self._current_channel = 0
logging.debug("RECON %ds" % recon_time)
self.run('wifi.recon.channel clear')
else:
logging.debug("RECON %ds ON CHANNELS %s" % (recon_time, ','.join(map(str, channels))))
try:
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
except Exception as e:
logging.exception("error")
self.wait_for(recon_time, sleeping=False)
def _filter_included(self, ap):
return self._filter is None or \
self._filter.match(ap['hostname']) is not None or \
self._filter.match(ap['mac']) is not None
def set_access_points(self, aps):
self._access_points = aps
plugins.on('wifi_update', self, aps)
self._epoch.observe(aps, self._advertiser.peers() if self._advertiser is not None else ())
return self._access_points
def get_access_points(self):
whitelist = self._config['main']['whitelist']
aps = []
try:
s = self.session()
for ap in s['wifi']['aps']:
if ap['hostname'] not in whitelist:
if self._filter_included(ap):
aps.append(ap)
except Exception as e:
logging.exception("error")
aps.sort(key=lambda ap: ap['channel'])
return self.set_access_points(aps)
def get_access_points_by_channel(self):
aps = self.get_access_points()
channels = self._config['personality']['channels']
grouped = {}
# group by channel
for ap in aps:
ch = ap['channel']
# if we're sticking to a channel, skip anything
# which is not on that channel
if channels != [] and ch not in channels:
continue
if ch not in grouped:
grouped[ch] = [ap]
else:
grouped[ch].append(ap)
# sort by more populated channels
return sorted(grouped.items(), key=lambda kv: len(kv[1]), reverse=True)
def _find_ap_sta_in(self, station_mac, ap_mac, session):
for ap in session['wifi']['aps']:
if ap['mac'] == ap_mac:
for sta in ap['clients']:
if sta['mac'] == station_mac:
return (ap, sta)
return (ap, {'mac': station_mac, 'vendor': ''})
return None
def _update_uptime(self, s):
secs = time.time() - self._started_at
self._view.set('uptime', utils.secs_to_hhmmss(secs))
self._view.set('epoch', '%04d' % self._epoch.epoch)
def _update_counters(self):
tot_aps = len(self._access_points)
tot_stas = sum(len(ap['clients']) for ap in self._access_points)
if self._current_channel == 0:
self._view.set('aps', '%d' % tot_aps)
self._view.set('sta', '%d' % tot_stas)
else:
aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
stas_on_channel = sum(
[len(ap['clients']) for ap in self._access_points if ap['channel'] == self._current_channel])
self._view.set('aps', '%d (%d)' % (aps_on_channel, tot_aps))
self._view.set('sta', '%d (%d)' % (stas_on_channel, tot_stas))
def _update_handshakes(self, new_shakes=0):
if new_shakes > 0:
self._epoch.track(handshake=True, inc=new_shakes)
tot = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
txt = '%d (%d)' % (len(self._handshakes), tot)
if self._last_pwnd is not None:
txt += ' [%s]' % self._last_pwnd[:20]
self._view.set('shakes', txt)
if new_shakes > 0:
self._view.on_handshakes(new_shakes)
def _update_advertisement(self, s):
run_handshakes = len(self._handshakes)
tot_handshakes = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
started = s['started_at'].split('.')[0]
started = datetime.strptime(started, '%Y-%m-%dT%H:%M:%S')
started = time.mktime(started.timetuple())
self._advertiser.update({ \
'pwnd_run': run_handshakes,
'pwnd_tot': tot_handshakes,
'uptime': time.time() - started,
'epoch': self._epoch.epoch})
def _update_peers(self):
peer = self._advertiser.closest_peer()
self._view.set_closest_peer(peer)
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
with open(RECOVERY_DATA_FILE, 'w') as fp:
data = {
'started_at': self._started_at,
'epoch': self._epoch.epoch,
'history': self._history,
'handshakes': self._handshakes,
'last_pwnd': self._last_pwnd
}
json.dump(data, fp)
def _load_recovery_data(self, delete=True, no_exceptions=True):
try:
with open(RECOVERY_DATA_FILE, 'rt') as fp:
data = json.load(fp)
logging.info("found recovery data: %s" % data)
self._started_at = data['started_at']
self._epoch.epoch = data['epoch']
self._handshakes = data['handshakes']
self._history = data['history']
self._last_pwnd = data['last_pwnd']
if delete:
logging.info("deleting %s" % RECOVERY_DATA_FILE)
os.unlink(RECOVERY_DATA_FILE)
except:
if not no_exceptions:
raise
def _event_poller(self):
self._load_recovery_data()
self.run('events.clear')
logging.debug("event polling started ...")
while True:
time.sleep(1)
new_shakes = 0
s = self.session()
self._update_uptime(s)
if self._advertiser is not None:
self._update_advertisement(s)
self._update_peers()
self._update_counters()
try:
for h in [e for e in self.events() if e['tag'] == 'wifi.client.handshake']:
filename = h['data']['file']
sta_mac = h['data']['station']
ap_mac = h['data']['ap']
key = "%s -> %s" % (sta_mac, ap_mac)
if key not in self._handshakes:
self._handshakes[key] = h
new_shakes += 1
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
if ap_and_station is None:
logging.warning("!!! captured new handshake: %s !!!" % key)
self._last_pwnd = ap_mac
plugins.on('handshake', self, filename, ap_mac, sta_mac)
else:
(ap, sta) = ap_and_station
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
'hostname'] != '<hidden>' else ap_mac
logging.warning("!!! captured new handshake on channel %d: %s (%s) -> %s [%s (%s)] !!!" % ( \
ap['channel'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor']))
plugins.on('handshake', self, filename, ap, sta)
except Exception as e:
logging.exception("error")
finally:
self._update_handshakes(new_shakes)
def start_event_polling(self):
_thread.start_new_thread(self._event_poller, ())
def is_module_running(self, module):
s = self.session()
for m in s['modules']:
if m['name'] == module:
return m['running']
return False
def start_module(self, module):
self.run('%s on' % module)
def restart_module(self, module):
self.run('%s off; %s on' % (module, module))
def _has_handshake(self, bssid):
for key in self._handshakes:
if bssid.lower() in key:
return True
return False
def _should_interact(self, who):
if self._has_handshake(who):
return False
elif who not in self._history:
self._history[who] = 1
return True
else:
self._history[who] += 1
return self._history[who] < self._config['personality']['max_interactions']
def _on_miss(self, who):
logging.info("it looks like %s is not in range anymore :/" % who)
self._epoch.track(miss=True)
self._view.on_miss(who)
def _on_error(self, who, e):
error = "%s" % e
# when we're trying to associate or deauth something that is not in range anymore
# (if we are moving), we get the following error from bettercap:
# error 400: 50:c7:bf:2e:d3:37 is an unknown BSSID or it is in the association skip list.
if 'is an unknown BSSID' in error:
self._on_miss(who)
else:
logging.error("%s" % e)
def associate(self, ap, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping assoc(%s)" % ap['mac'])
return
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
self._view.on_assoc(ap)
try:
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients]..." % ( \
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients'])))
self.run('wifi.assoc %s' % ap['mac'])
self._epoch.track(assoc=True)
except Exception as e:
self._on_error(ap['mac'], e)
plugins.on('association', self, ap)
if throttle > 0:
time.sleep(throttle)
self._view.on_normal()
def deauth(self, ap, sta, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping deauth(%s)" % sta['mac'])
return
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
self._view.on_deauth(sta)
try:
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d ..." % (
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel']))
self.run('wifi.deauth %s' % sta['mac'])
self._epoch.track(deauth=True)
except Exception as e:
self._on_error(sta['mac'], e)
plugins.on('deauthentication', self, ap, sta)
if throttle > 0:
time.sleep(throttle)
self._view.on_normal()
def set_channel(self, channel, verbose=True):
if self.is_stale():
logging.debug("recon is stale, skipping set_channel(%d)" % channel)
return
# if in the previous loop no client stations has been deauthenticated
# and only association frames have been sent, we don't need to wait
# very long before switching channel as we don't have to wait for
# such client stations to reconnect in order to sniff the handshake.
wait = 0
if self._epoch.did_deauth:
wait = self._config['personality']['hop_recon_time']
elif self._epoch.did_associate:
wait = self._config['personality']['min_recon_time']
if channel != self._current_channel:
if self._current_channel != 0 and wait > 0:
if verbose:
logging.info("waiting for %ds on channel %d ..." % (wait, self._current_channel))
else:
logging.debug("waiting for %ds on channel %d ..." % (wait, self._current_channel))
self.wait_for(wait)
if verbose and self._epoch.any_activity:
logging.info("CHANNEL %d" % channel)
try:
self.run('wifi.recon.channel %d' % channel)
self._current_channel = channel
self._epoch.track(hop=True)
self._view.set('channel', '%d' % channel)
plugins.on('channel_hop', self, channel)
except Exception as e:
logging.error("error: %s" % e)
def is_stale(self):
return self._epoch.num_missed > self._config['personality']['max_misses_for_recon']
def any_activity(self):
return self._epoch.any_activity
def _reboot(self):
self.set_rebooting()
self._save_recovery_data()
logging.warning("rebooting the system ...")
os.system("/usr/bin/sync")
os.system("/usr/sbin/shutdown -r now")
def next_epoch(self):
was_stale = self.is_stale()
did_miss = self._epoch.num_missed
self._epoch.next()
# after X misses during an epoch, set the status to lonely
if was_stale:
logging.warning("agent missed %d interactions -> lonely" % did_miss)
self.set_lonely()
# after X times being bored, the status is set to sad
elif self._epoch.inactive_for >= self._config['personality']['sad_num_epochs']:
logging.warning("%d epochs with no activity -> sad" % self._epoch.inactive_for)
self.set_sad()
# after X times being inactive, the status is set to bored
elif self._epoch.inactive_for >= self._config['personality']['bored_num_epochs']:
logging.warning("%d epochs with no activity -> bored" % self._epoch.inactive_for)
self.set_bored()
# after X times being active, the status is set to happy / excited
elif self._epoch.active_for >= self._config['personality']['excited_num_epochs']:
logging.warning("%d epochs with activity -> excited" % self._epoch.active_for)
self.set_excited()
plugins.on('epoch', self, self._epoch.epoch - 1, self._epoch.data())
if self._epoch.blind_for >= self._config['main']['mon_max_blind_epochs']:
logging.critical("%d epochs without visible access points -> rebooting ..." % self._epoch.blind_for)
self._reboot()
self._epoch.blind_for = 0