Files
pwnagotchi/pwnagotchi/agent.py

505 lines
19 KiB
Python
Raw Normal View History

2019-09-19 15:15:46 +02:00
import time
import json
import os
import re
import logging
2020-04-03 19:01:40 +02:00
import asyncio
2019-09-19 15:15:46 +02:00
import _thread
import pwnagotchi
2019-10-04 00:26:00 +02:00
import pwnagotchi.utils as utils
import pwnagotchi.plugins as plugins
from pwnagotchi.ui.web.server import Server
from pwnagotchi.automata import Automata
from pwnagotchi.log import LastSession
from pwnagotchi.bettercap import Client
2019-09-30 21:22:01 +02:00
from pwnagotchi.mesh.utils import AsyncAdvertiser
2019-09-19 15:15:46 +02:00
from pwnagotchi.ai.train import AsyncTrainer
RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def __init__(self, view, config, keypair):
2019-09-19 15:15:46 +02:00
Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'],
config['bettercap']['port'],
config['bettercap']['username'],
config['bettercap']['password'])
Automata.__init__(self, config, view)
AsyncAdvertiser.__init__(self, config, view, keypair)
2019-09-19 15:15:46 +02:00
AsyncTrainer.__init__(self, config)
self._started_at = time.time()
2019-12-18 18:58:28 +01:00
self._filter = None if not config['main']['filter'] else re.compile(config['main']['filter'])
2019-09-19 15:15:46 +02:00
self._current_channel = 0
self._tot_aps = 0
self._aps_on_channel = 0
2019-10-04 00:26:00 +02:00
self._supported_channels = utils.iface_channels(config['main']['iface'])
2019-09-19 15:15:46 +02:00
self._view = view
self._view.set_agent(self)
self._web_ui = Server(self, config['ui'])
2019-09-19 15:15:46 +02:00
self._access_points = []
self._last_pwnd = None
self._history = {}
self._handshakes = {}
self.last_session = LastSession(self._config)
self.mode = 'auto'
2019-09-19 15:15:46 +02:00
if not os.path.exists(config['bettercap']['handshakes']):
os.makedirs(config['bettercap']['handshakes'])
2020-01-16 18:52:40 +01:00
logging.info("%s@%s (v%s)", pwnagotchi.name(), self.fingerprint(), pwnagotchi.__version__)
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s", plugin.__class__.__name__, plugin.__version__)
def config(self):
return self._config
def view(self):
return self._view
2019-09-21 18:52:00 +02:00
def supported_channels(self):
return self._supported_channels
2019-09-19 15:15:46 +02:00
def setup_events(self):
logging.info("connecting to %s ...", self.url)
2019-09-19 15:15:46 +02:00
for tag in self._config['bettercap']['silence']:
try:
self.run('events.ignore %s' % tag, verbose_errors=False)
2020-04-03 19:01:40 +02:00
except Exception:
2019-09-19 15:15:46 +02:00
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'])
2019-09-19 15:15:46 +02:00
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 ...")
2019-09-19 15:15:46 +02:00
self.run('!%s' % mon_start_cmd)
else:
logging.info("waiting for monitor interface %s ...", mon_iface)
2019-09-19 15:15:46 +02:00
time.sleep(1)
logging.info("supported channels: %s", self._supported_channels)
logging.info("handshakes will be collected inside %s", self._config['bettercap']['handshakes'])
2019-09-19 15:15:46 +02:00
self._reset_wifi_settings()
wifi_running = self.is_module_running('wifi')
if wifi_running and restart:
logging.debug("restarting wifi module ...")
2019-10-04 12:22:16 +02:00
self.restart_module('wifi.recon')
2019-09-19 15:15:46 +02:00
self.run('wifi.clear')
elif not wifi_running:
logging.debug("starting wifi module ...")
2019-10-04 12:22:16 +02:00
self.start_module('wifi.recon')
2019-09-19 15:15:46 +02:00
self.start_advertising()
def _wait_bettercap(self):
while True:
try:
2020-04-03 19:01:40 +02:00
_s = self.session()
return
2020-04-03 19:01:40 +02:00
except Exception:
logging.info("waiting for bettercap API to be available ...")
time.sleep(1)
2019-10-04 00:11:31 +02:00
def start(self):
self.start_ai()
self._wait_bettercap()
2019-10-04 00:11:31 +02:00
self.setup_events()
self.set_starting()
self.start_monitor_mode()
self.start_event_polling()
2020-04-03 19:01:40 +02:00
self.start_session_fetcher()
2019-10-04 00:11:31 +02:00
# print initial stats
self.next_epoch()
self.set_ready()
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
self.run('wifi.recon.channel clear')
else:
logging.debug("RECON %ds ON CHANNELS %s", recon_time, ','.join(map(str, channels)))
2019-09-19 15:15:46 +02:00
try:
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
2019-09-19 15:15:46 +02:00
except Exception as e:
2020-04-03 19:01:40 +02:00
logging.exception("Error while setting wifi.recon.channels (%s)", e)
2019-09-19 15:15:46 +02:00
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, list(self._peers.values()))
2019-09-19 15:15:46 +02:00
return self._access_points
def get_access_points(self):
whitelist = self._config['main']['whitelist']
aps = []
try:
s = self.session()
plugins.on("unfiltered_ap_list", self, s['wifi']['aps'])
2019-09-19 15:15:46 +02:00
for ap in s['wifi']['aps']:
if ap['encryption'] == '' or ap['encryption'] == 'OPEN':
continue
elif ap['hostname'] not in whitelist \
and ap['mac'].lower() not in whitelist \
and ap['mac'][:8].lower() not in whitelist:
2019-09-19 15:15:46 +02:00
if self._filter_included(ap):
aps.append(ap)
except Exception as e:
2020-04-03 19:01:40 +02:00
logging.exception("Error while getting acces points (%s)", e)
2019-09-19 15:15:46 +02:00
aps.sort(key=lambda ap: ap['channel'])
return self.set_access_points(aps)
def get_total_aps(self):
return self._tot_aps
def get_aps_on_channel(self):
return self._aps_on_channel
def get_current_channel(self):
return self._current_channel
2019-09-19 15:15:46 +02:00
def get_access_points_by_channel(self):
aps = self.get_access_points()
channels = self._config['personality']['channels']
grouped = {}
2019-09-22 12:08:11 +02:00
# group by channel
2019-09-19 15:15:46 +02:00
for ap in aps:
ch = ap['channel']
# if we're sticking to a channel, skip anything
# which is not on that channel
2019-12-13 19:29:44 +01:00
if channels and ch not in channels:
2019-09-19 15:15:46 +02:00
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:
2023-08-29 23:48:07 +02:00
return ap, sta
return ap, {'mac': station_mac, 'vendor': ''}
2019-09-19 15:15:46 +02:00
return None
def _update_uptime(self, s):
secs = pwnagotchi.uptime()
2019-10-04 00:26:00 +02:00
self._view.set('uptime', utils.secs_to_hhmmss(secs))
# self._view.set('epoch', '%04d' % self._epoch.epoch)
2019-09-19 15:15:46 +02:00
def _update_counters(self):
self._tot_aps = len(self._access_points)
2019-09-19 15:15:46 +02:00
tot_stas = sum(len(ap['clients']) for ap in self._access_points)
if self._current_channel == 0:
self._view.set('aps', '%d' % self._tot_aps)
2019-09-19 15:15:46 +02:00
self._view.set('sta', '%d' % tot_stas)
else:
self._aps_on_channel = len([ap for ap in self._access_points if ap['channel'] == self._current_channel])
2019-09-19 15:15:46 +02:00
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)' % (self._aps_on_channel, self._tot_aps))
2019-09-19 15:15:46 +02:00
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)
2019-10-04 00:26:00 +02:00
tot = utils.total_unique_handshakes(self._config['bettercap']['handshakes'])
2019-09-19 15:15:46 +02:00
txt = '%d (%d)' % (len(self._handshakes), tot)
if self._last_pwnd is not None:
2023-08-29 23:48:07 +02:00
txt += ' [%s]' % self._last_pwnd[:11] # So it doesn't overlap with fix_brcmfmac_plugin
2019-09-19 15:15:46 +02:00
self._view.set('shakes', txt)
if new_shakes > 0:
self._view.on_handshakes(new_shakes)
def _update_peers(self):
self._view.set_closest_peer(self._closest_peer, len(self._peers))
2019-09-19 15:15:46 +02:00
def _reboot(self):
self.set_rebooting()
self._save_recovery_data()
pwnagotchi.reboot()
2019-09-19 15:15:46 +02:00
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
os.unlink(RECOVERY_DATA_FILE)
except:
if not no_exceptions:
raise
2020-04-03 19:01:40 +02:00
def start_session_fetcher(self):
2020-04-17 22:31:30 +02:00
_thread.start_new_thread(self._fetch_stats, ())
2020-04-03 19:01:40 +02:00
def _fetch_stats(self):
2019-09-19 15:15:46 +02:00
while True:
try:
s = self.session()
except Exception as err:
logging.error("[agent:_fetch_stats] self.session: %s" % repr(err))
try:
self._update_uptime(s)
except Exception as err:
logging.error("[agent:_fetch_stats] self.update_uptimes: %s" % repr(err))
try:
self._update_advertisement(s)
except Exception as err:
logging.error("[agent:_fetch_stats] self.update_advertisements: %s" % repr(err))
try:
self._update_peers()
except Exception as err:
logging.error("[agent:_fetch_stats] self.update_peers: %s" % repr(err))
try:
self._update_counters()
except Exception as err:
logging.error("[agent:_fetch_stats] self.update_counters: %s" % repr(err))
try:
self._update_handshakes(0)
except Exception as err:
logging.error("[agent:_fetch_stats] self.update_handshakes: %s" % repr(err))
time.sleep(5)
2020-04-03 19:01:40 +02:00
async def _on_event(self, msg):
found_handshake = False
jmsg = json.loads(msg)
2020-04-17 22:31:30 +02:00
# give plugins access to the events
try:
plugins.on('bcap_%s' % re.sub(r"[^a-z0-9_]+", "_", jmsg['tag'].lower()), self, jmsg)
except Exception as err:
logging.error("Processing event: %s" % err)
2020-04-03 19:01:40 +02:00
if jmsg['tag'] == 'wifi.client.handshake':
filename = jmsg['data']['file']
sta_mac = jmsg['data']['station']
ap_mac = jmsg['data']['ap']
key = "%s -> %s" % (sta_mac, ap_mac)
if key not in self._handshakes:
self._handshakes[key] = jmsg
2020-04-17 22:31:30 +02:00
s = self.session()
2020-04-03 19:01:40 +02:00
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, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
ap['channel'],
ap['rssi'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor'])
plugins.on('handshake', self, filename, ap, sta)
found_handshake = True
self._update_handshakes(1 if found_handshake else 0)
def _event_poller(self, loop):
self._load_recovery_data()
self.run('events.clear')
2019-09-19 15:15:46 +02:00
2020-04-03 19:01:40 +02:00
while True:
logging.debug("[agent:_event_poller] polling events ...")
2019-09-19 15:15:46 +02:00
try:
2020-04-03 19:01:40 +02:00
loop.create_task(self.start_websocket(self._on_event))
loop.run_forever()
logging.debug("[agent:_event_poller] loop loop loop")
2020-04-03 19:01:40 +02:00
except Exception as ex:
logging.debug("[agent:_event_poller] Error while polling via websocket (%s)", ex)
2019-09-19 15:15:46 +02:00
def start_event_polling(self):
2020-04-03 19:01:40 +02:00
# start a thread and pass in the mainloop
2020-04-16 17:35:41 +02:00
_thread.start_new_thread(self._event_poller, (asyncio.get_event_loop(),))
2020-04-03 19:01:40 +02:00
2019-09-19 15:15:46 +02:00
def is_module_running(self, module):
s = self.session()
for m in s['modules']:
if m['name'] == module:
return m['running']
return False
2019-10-04 12:22:16 +02:00
def start_module(self, module):
2019-09-19 15:15:46 +02:00
self.run('%s on' % module)
2019-10-04 12:22:16 +02:00
def restart_module(self, module):
2019-09-19 15:15:46 +02:00
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 associate(self, ap, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping assoc(%s)", ap['mac'])
2019-09-19 15:15:46 +02:00
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], %d dBm...",
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi'])
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
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'])
2019-09-19 15:15:46 +02:00
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, %d dBm ...",
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi'])
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
return
# if in the previous loop no client stations has been deauthenticated
2019-09-22 12:08:11 +02:00
# and only association frames have been sent, we don't need to wait
2019-09-19 15:15:46 +02:00
# 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)
2019-09-19 15:15:46 +02:00
self.wait_for(wait)
if verbose and self._epoch.any_activity:
logging.info("CHANNEL %d", channel)
2019-09-19 15:15:46 +02:00
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)
2019-09-19 15:15:46 +02:00
except Exception as e:
2020-04-03 19:01:40 +02:00
logging.error("Error while setting channel (%s)", e)