From 5d2855760859030b2cd36b27e42a36f2b95877f0 Mon Sep 17 00:00:00 2001 From: Simone Margaritelli Date: Sun, 13 Oct 2019 17:24:47 +0200 Subject: [PATCH] new: using pwngrid for mesh advertising (we got rid of scapy loading times) --- bin/pwnagotchi | 6 +- builder/pwnagotchi.yml | 6 +- pwnagotchi/agent.py | 32 +----- pwnagotchi/grid.py | 97 ++++++++++++++++ pwnagotchi/mesh/__init__.py | 4 - pwnagotchi/mesh/advertise.py | 176 ----------------------------- pwnagotchi/mesh/peer.py | 30 +++-- pwnagotchi/mesh/utils.py | 96 +++++++++++----- pwnagotchi/mesh/wifi.py | 37 ------ pwnagotchi/plugins/default/grid.py | 79 ++----------- 10 files changed, 197 insertions(+), 366 deletions(-) create mode 100644 pwnagotchi/grid.py delete mode 100644 pwnagotchi/mesh/advertise.py delete mode 100644 pwnagotchi/mesh/wifi.py diff --git a/bin/pwnagotchi b/bin/pwnagotchi index 297bce6..a7d3c97 100755 --- a/bin/pwnagotchi +++ b/bin/pwnagotchi @@ -2,10 +2,10 @@ if __name__ == '__main__': import argparse import time - import os import logging import pwnagotchi + import pwnagtochi.grid as grid import pwnagotchi.utils as utils import pwnagotchi.plugins as plugins @@ -64,7 +64,7 @@ if __name__ == '__main__': display.on_manual_mode(agent.last_session) time.sleep(1) - if Agent.is_connected(): + if grid.is_connected(): plugins.on('internet_available', agent) else: @@ -102,7 +102,7 @@ if __name__ == '__main__': # affect ours ... neat ^_^ agent.next_epoch() - if Agent.is_connected(): + if grid.is_connected(): plugins.on('internet_available', agent) except Exception as e: diff --git a/builder/pwnagotchi.yml b/builder/pwnagotchi.yml index 219948e..8079d60 100644 --- a/builder/pwnagotchi.yml +++ b/builder/pwnagotchi.yml @@ -34,7 +34,7 @@ url: "https://github.com/bettercap/bettercap/releases/download/v2.25/bettercap_linux_armv6l_2.25.zip" ui: "https://github.com/bettercap/ui/releases/download/v1.3.0/ui.zip" pwngrid: - url: "https://github.com/evilsocket/pwngrid/releases/download/v1.6.3/pwngrid_linux_armv6l_v1.6.3.zip" + url: "https://github.com/evilsocket/pwngrid/releases/download/v1.7.1/pwngrid_linux_armv6l_1.7.1.zip" apt: hold: - firmware-atheros @@ -497,12 +497,12 @@ Description=pwngrid peer service. Documentation=https://pwnagotchi.ai Wants=network.target - After=network.target + After=bettercap.service [Service] Type=simple PermissionsStartOnly=true - ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -wait -log /var/log/pwngrid-peer.log + ExecStart=/usr/bin/pwngrid -keys /etc/pwnagotchi -address 127.0.0.1:8666 -wait -log /var/log/pwngrid-peer.log -iface mon0 Restart=always RestartSec=30 diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index b5c876f..41680df 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -2,8 +2,6 @@ import time import json import os import re -import socket -from datetime import datetime import logging import _thread @@ -42,15 +40,6 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): if not os.path.exists(config['bettercap']['handshakes']): os.makedirs(config['bettercap']['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 @@ -193,7 +182,7 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): 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 ()) + self._epoch.observe(aps, list(self._peers.values())) return self._access_points def get_access_points(self): @@ -274,19 +263,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): 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']) - self._advertiser.update({ - 'pwnd_run': run_handshakes, - 'pwnd_tot': tot_handshakes, - 'uptime': pwnagotchi.uptime(), - 'epoch': self._epoch.epoch}) - def _update_peers(self): - peer = self._advertiser.closest_peer() - tot = self._advertiser.num_peers() - self._view.set_closest_peer(peer, tot) + self._view.set_closest_peer(self._closest_peer, len(self._peers)) def _save_recovery_data(self): logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE) @@ -331,10 +309,8 @@ class Agent(Client, AsyncAdvertiser, AsyncTrainer): s = self.session() self._update_uptime(s) - if self._advertiser is not None: - self._update_advertisement(s) - self._update_peers() - + self._update_advertisement(s) + self._update_peers() self._update_counters() try: diff --git a/pwnagotchi/grid.py b/pwnagotchi/grid.py new file mode 100644 index 0000000..93bc08a --- /dev/null +++ b/pwnagotchi/grid.py @@ -0,0 +1,97 @@ +import subprocess +import socket +import requests +import json +import logging + +import pwnagotchi + +# pwngrid-peer is running on port 8666 +API_ADDRESS = "http://127.0.0.1:8666/api/v1" + + +def is_connected(): + try: + socket.create_connection(("www.google.com", 80)) + return True + except OSError: + pass + return False + + +def call(path, obj=None): + url = '%s%s' % (API_ADDRESS, path) + if obj is None: + r = requests.get(url, headers=None) + else: + r = requests.post(url, headers=None, json=obj) + + if r.status_code != 200: + raise Exception("(status %d) %s" % (r.status_code, r.text)) + return r.json() + + +def advertise(enabled=True): + return call("/mesh/%s" % 'true' if enabled else 'false') + + +def set_advertisement_data(data): + return call("/mesh/data", obj=data) + + +def peers(): + return call("/mesh/peers") + + +def closest_peer(): + all = peers() + return all[0] if len(all) else None + + +def update_data(last_session): + brain = {} + try: + with open('/root/brain.json') as fp: + brain = json.load(fp) + except: + pass + + data = { + 'session': { + 'duration': last_session.duration, + 'epochs': last_session.epochs, + 'train_epochs': last_session.train_epochs, + 'avg_reward': last_session.avg_reward, + 'min_reward': last_session.min_reward, + 'max_reward': last_session.max_reward, + 'deauthed': last_session.deauthed, + 'associated': last_session.associated, + 'handshakes': last_session.handshakes, + 'peers': last_session.peers, + }, + 'uname': subprocess.getoutput("uname -a"), + 'brain': brain, + 'version': pwnagotchi.version + } + + logging.debug("updating grid data: %s" % data) + + call("/data", data) + + +def report_ap(essid, bssid): + try: + call("/report/ap", { + 'essid': essid, + 'bssid': bssid, + }) + return True + except Exception as e: + logging.exception("error while reporting ap %s(%s)" % (essid, bssid)) + + return False + + +def inbox(page=1, with_pager=False): + obj = call("/inbox?p=%d" % page) + return obj["messages"] if not with_pager else obj diff --git a/pwnagotchi/mesh/__init__.py b/pwnagotchi/mesh/__init__.py index e1c033f..e69de29 100644 --- a/pwnagotchi/mesh/__init__.py +++ b/pwnagotchi/mesh/__init__.py @@ -1,4 +0,0 @@ -import os - -def new_session_id(): - return ':'.join(['%02x' % b for b in os.urandom(6)]) diff --git a/pwnagotchi/mesh/advertise.py b/pwnagotchi/mesh/advertise.py deleted file mode 100644 index a4f88b7..0000000 --- a/pwnagotchi/mesh/advertise.py +++ /dev/null @@ -1,176 +0,0 @@ -import time -import json -import _thread -import threading -import logging -from scapy.all import Dot11, Dot11Elt, RadioTap, sendp, sniff - -import pwnagotchi.ui.faces as faces - -import pwnagotchi.mesh.wifi as wifi -from pwnagotchi.mesh import new_session_id -from pwnagotchi.mesh.peer import Peer - - -def _dummy_peer_cb(peer): - pass - - -class Advertiser(object): - MAX_STALE_TIME = 300 - - def __init__(self, iface, name, version, identity, period=0.3, data={}): - self._iface = iface - self._period = period - self._running = False - self._stopped = threading.Event() - self._peers_lock = threading.Lock() - self._adv_lock = threading.Lock() - self._new_peer_cb = _dummy_peer_cb - self._lost_peer_cb = _dummy_peer_cb - self._peers = {} - self._frame = None - self._me = Peer(new_session_id(), 0, 0, { - 'name': name, - 'version': version, - 'identity': identity, - 'face': faces.FRIEND, - 'pwnd_run': 0, - 'pwnd_tot': 0, - 'uptime': 0, - 'epoch': 0, - 'data': data - }) - self.update() - - def update(self, values={}): - with self._adv_lock: - for field, value in values.items(): - self._me.adv[field] = value - self._frame = wifi.encapsulate(payload=json.dumps(self._me.adv), addr_from=self._me.session_id) - - def on_peer(self, new_cb, lost_cb): - self._new_peer_cb = new_cb - self._lost_peer_cb = lost_cb - - def on_face_change(self, old, new): - self.update({'face': new}) - - def start(self): - self._running = True - _thread.start_new_thread(self._sender, ()) - _thread.start_new_thread(self._listener, ()) - _thread.start_new_thread(self._pruner, ()) - - def num_peers(self): - with self._peers_lock: - return len(self._peers) - - def peers(self): - with self._peers_lock: - return list(self._peers.values()) - - def closest_peer(self): - closest = None - with self._peers_lock: - for ident, peer in self._peers.items(): - if closest is None or peer.is_closer(closest): - closest = peer - return closest - - def stop(self): - self._running = False - self._stopped.set() - - def _sender(self): - logging.info("started advertiser thread (period:%s sid:%s) ..." % (str(self._period), self._me.session_id)) - while self._running: - try: - sendp(self._frame, iface=self._iface, verbose=False, count=1, inter=self._period) - except OSError as ose: - logging.warning("non critical issue while sending advertising packet: %s" % ose) - except Exception as e: - logging.exception("error") - time.sleep(self._period) - - def _on_advertisement(self, src_session_id, channel, rssi, adv): - ident = adv['identity'] - with self._peers_lock: - if ident not in self._peers: - peer = Peer(src_session_id, channel, rssi, adv) - logging.info("detected unit %s (v%s) on channel %d (%s dBm) [sid:%s pwnd_tot:%d uptime:%d]" % ( \ - peer.full_name(), - peer.version(), - channel, - rssi, - src_session_id, - peer.pwnd_total(), - peer.uptime())) - - self._peers[ident] = peer - self._new_peer_cb(peer) - else: - self._peers[ident].update(src_session_id, channel, rssi, adv) - - def _parse_identity(self, radio, dot11, dot11elt): - payload = b'' - while dot11elt: - payload += dot11elt.info - dot11elt = dot11elt.payload.getlayer(Dot11Elt) - - if payload != b'': - adv = json.loads(payload) - self._on_advertisement( \ - dot11.addr3, - wifi.freq_to_channel(radio.Channel), - radio.dBm_AntSignal, - adv) - - def _is_broadcasted_advertisement(self, dot11): - # dst bcast + protocol signature + not ours - return dot11 is not None and \ - dot11.addr1 == wifi.BroadcastAddress and \ - dot11.addr2 == wifi.SignatureAddress and \ - dot11.addr3 != self._me.session_id - - def _is_frame_for_us(self, dot11): - # dst is us + protocol signature + not ours (why would we send a frame to ourself anyway?) - return dot11 is not None and \ - dot11.addr1 == self._me.session_id and \ - dot11.addr2 == wifi.SignatureAddress and \ - dot11.addr3 != self._me.session_id - - def _on_packet(self, p): - dot11 = p.getlayer(Dot11) - - if self._is_broadcasted_advertisement(dot11): - try: - dot11elt = p.getlayer(Dot11Elt) - if dot11elt.ID == wifi.Dot11ElemID_Whisper: - self._parse_identity(p[RadioTap], dot11, dot11elt) - - else: - raise Exception("unknown frame id %d" % dot11elt.ID) - - except Exception as e: - logging.exception("error decoding packet from %s" % dot11.addr3) - - def _listener(self): - # logging.info("started advertisements listener ...") - expr = "type mgt subtype beacon and ether src %s" % wifi.SignatureAddress - sniff(iface=self._iface, filter=expr, prn=self._on_packet, store=0, stop_filter=lambda x: self._stopped.isSet()) - - def _pruner(self): - while self._running: - time.sleep(10) - with self._peers_lock: - stale = [] - for ident, peer in self._peers.items(): - inactive_for = peer.inactive_for() - if inactive_for >= Advertiser.MAX_STALE_TIME: - logging.info("peer %s lost (inactive for %ds)" % (peer.full_name(), inactive_for)) - self._lost_peer_cb(peer) - stale.append(ident) - - for ident in stale: - del self._peers[ident] diff --git a/pwnagotchi/mesh/peer.py b/pwnagotchi/mesh/peer.py index 0d3b061..14f0057 100644 --- a/pwnagotchi/mesh/peer.py +++ b/pwnagotchi/mesh/peer.py @@ -1,32 +1,28 @@ import time import logging -import pwnagotchi.mesh.wifi as wifi import pwnagotchi.ui.faces as faces class Peer(object): - def __init__(self, sid, channel, rssi, adv): + def __init__(self, obj): self.first_seen = time.time() self.last_seen = self.first_seen - self.session_id = sid - self.last_channel = channel - self.presence = [0] * wifi.NumChannels - self.adv = adv - self.rssi = rssi - self.presence[channel - 1] = 1 + self.session_id = obj['session_id'] + self.last_channel = obj['channel'] + self.rssi = obj['rssi'] + self.adv = obj['advertisement'] - def update(self, sid, channel, rssi, adv): - if self.name() != adv['name']: - logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), adv['name'])) + def update(self, new): + if self.name() != new.name(): + logging.info("peer %s changed name: %s -> %s" % (self.full_name(), self.name(), new.name())) - if self.session_id != sid: - logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, sid)) + if self.session_id != new.session_id: + logging.info("peer %s changed session id: %s -> %s" % (self.full_name(), self.session_id, new.session_id)) - self.presence[channel - 1] += 1 - self.adv = adv - self.rssi = rssi - self.session_id = sid + self.adv = new.adv + self.rssi = new.rssi + self.session_id = new.session_id self.last_seen = time.time() def inactive_for(self): diff --git a/pwnagotchi/mesh/utils.py b/pwnagotchi/mesh/utils.py index 3a67ed9..d28f04d 100644 --- a/pwnagotchi/mesh/utils.py +++ b/pwnagotchi/mesh/utils.py @@ -2,7 +2,11 @@ import _thread import logging import pwnagotchi +import pwnagotchi.utils as utils +import pwnagotchi.ui.faces as faces import pwnagotchi.plugins as plugins +import pwnagotchi.grid as grid +from pwnagotchi.mesh.peer import Peer class AsyncAdvertiser(object): @@ -10,38 +14,76 @@ class AsyncAdvertiser(object): self._config = config self._view = view self._keypair = keypair - self._advertiser = None + self._advertisement = { + 'name': pwnagotchi.name(), + 'version': pwnagotchi.version, + 'identity': self._keypair.fingerprint, + 'face': faces.FRIEND, + 'pwnd_run': 0, + 'pwnd_tot': 0, + 'uptime': 0, + 'epoch': 0, + 'policy': self._config['personality'] + } + self._peers = {} + self._closest_peer = None - def keypair(self): - return self._keypair + _thread.start_new_thread(self._adv_poller, ()) + + def _update_advertisement(self, s): + self._advertisement['pwnd_run'] = len(self._handshakes) + self._advertisement['pwnd_tot'] = utils.total_unique_handshakes(self._config['bettercap']['handshakes']) + self._advertisement['uptime'] = pwnagotchi.uptime() + self._advertisement['epoch'] = self._epoch.epoch + grid.set_advertisement_data(self._advertisement) def start_advertising(self): - _thread.start_new_thread(self._adv_worker, ()) - - def _adv_worker(self): - # this will take some time due to scapy being slow to be imported ... - from pwnagotchi.mesh.advertise import Advertiser - - self._advertiser = Advertiser( - self._config['main']['iface'], - pwnagotchi.name(), - pwnagotchi.version, - self._keypair.fingerprint, - period=0.3, - data=self._config['personality']) - - self._advertiser.on_peer(self._on_new_unit, self._on_lost_unit) - if self._config['personality']['advertise']: - self._advertiser.start() - self._view.on_state_change('face', self._advertiser.on_face_change) + grid.set_advertisement_data(self._advertisement) + grid.advertise(True) + self._view.on_state_change('face', self._on_face_change) else: logging.warning("advertising is disabled") - def _on_new_unit(self, peer): - self._view.on_new_peer(peer) - plugins.on('peer_detected', self, peer) + def _on_face_change(self, old, new): + self._advertisement['face'] = new + grid.set_advertisement_data(self._advertisement) - def _on_lost_unit(self, peer): - self._view.on_lost_peer(peer) - plugins.on('peer_lost', self, peer) + def _adv_poller(self): + while True: + logging.debug("polling pwngrid-peer for peers ...") + + try: + grid_peers = grid.peers() + new_peers = {} + + self._closest_peer = None + for obj in grid_peers: + peer = Peer(obj) + new_peers[peer.identity()] = peer + if self._closest_peer is None: + self._closest_peer = peer + + # check who's gone + to_delete = [] + for ident, peer in self._peers.items(): + if ident not in new_peers: + self._view.on_lost_peer(peer) + plugins.on('peer_lost', self, peer) + to_delete.append(ident) + + for ident in to_delete: + del self._peers[ident] + + for ident, peer in new_peers: + # check who's new + if ident not in self._peers: + self._peers[ident] = peer + self._view.on_new_peer(peer) + plugins.on('peer_detected', self, peer) + # update the rest + else: + self._peers[ident].update(peer) + + except Exception as e: + logging.error("error while polling pwngrid-peer: %s" % e) diff --git a/pwnagotchi/mesh/wifi.py b/pwnagotchi/mesh/wifi.py deleted file mode 100644 index 6fe231e..0000000 --- a/pwnagotchi/mesh/wifi.py +++ /dev/null @@ -1,37 +0,0 @@ -SignatureAddress = 'de:ad:be:ef:de:ad' -BroadcastAddress = 'ff:ff:ff:ff:ff:ff' -Dot11ElemID_Whisper = 222 -NumChannels = 140 - -def freq_to_channel(freq): - if freq <= 2472: - return int(((freq - 2412) / 5) + 1) - elif freq == 2484: - return int(14) - elif 5035 <= freq <= 5865: - return int(((freq - 5035) / 5) + 7) - else: - return 0 - - -def encapsulate(payload, addr_from, addr_to=BroadcastAddress): - from scapy.all import Dot11, Dot11Beacon, Dot11Elt, RadioTap - - radio = RadioTap() - dot11 = Dot11(type=0, subtype=8, addr1=addr_to, addr2=SignatureAddress, addr3=addr_from) - beacon = Dot11Beacon(cap='ESS') - frame = radio / dot11 / beacon - - data_size = len(payload) - data_left = data_size - data_off = 0 - chunk_size = 255 - - while data_left > 0: - sz = min(chunk_size, data_left) - chunk = payload[data_off: data_off + sz] - frame /= Dot11Elt(ID=Dot11ElemID_Whisper, info=chunk, len=sz) - data_off += sz - data_left -= sz - - return frame diff --git a/pwnagotchi/plugins/default/grid.py b/pwnagotchi/plugins/default/grid.py index 516a6a2..4b62a55 100644 --- a/pwnagotchi/plugins/default/grid.py +++ b/pwnagotchi/plugins/default/grid.py @@ -6,11 +6,9 @@ __description__ = 'This plugin signals the unit cryptographic identity and list import os import logging -import requests import glob -import json -import subprocess -import pwnagotchi + +import pwnagotchi.grid as grid import pwnagotchi.utils as utils from pwnagotchi.ui.components import LabeledValue from pwnagotchi.ui.view import BLACK @@ -65,74 +63,13 @@ def is_excluded(what): return False -def grid_call(path, obj=None): - # pwngrid-peer is running on port 8666 - api_address = 'http://127.0.0.1:8666/api/v1%s' % path - if obj is None: - r = requests.get(api_address, headers=None) - else: - r = requests.post(api_address, headers=None, json=obj) - - if r.status_code != 200: - raise Exception("(status %d) %s" % (r.status_code, r.text)) - return r.json() - - -def grid_update_data(last_session): - brain = {} - try: - with open('/root/brain.json') as fp: - brain = json.load(fp) - except: - pass - - data = { - 'session': { - 'duration': last_session.duration, - 'epochs': last_session.epochs, - 'train_epochs': last_session.train_epochs, - 'avg_reward': last_session.avg_reward, - 'min_reward': last_session.min_reward, - 'max_reward': last_session.max_reward, - 'deauthed': last_session.deauthed, - 'associated': last_session.associated, - 'handshakes': last_session.handshakes, - 'peers': last_session.peers, - }, - 'uname': subprocess.getoutput("uname -a"), - 'brain': brain, - 'version': pwnagotchi.version - } - - logging.debug("updating grid data: %s" % data) - - grid_call("/data", data) - - -def grid_report_ap(essid, bssid): - try: - grid_call("/report/ap", { - 'essid': essid, - 'bssid': bssid, - }) - return True - except Exception as e: - logging.exception("error while reporting ap %s(%s)" % (essid, bssid)) - - return False - - -def grid_inbox(): - return grid_call("/inbox")["messages"] - - def on_ui_update(ui): new_value = ' %d (%d)' % (UNREAD_MESSAGES, TOTAL_MESSAGES) if not ui.has_element('mailbox') and TOTAL_MESSAGES > 0: if ui.is_inky(): - pos=(80, 0) + pos = (80, 0) else: - pos=(100,0) + pos = (100, 0) ui.add_element('mailbox', LabeledValue(color=BLACK, label='MSG', value=new_value, position=pos, @@ -147,7 +84,7 @@ def on_internet_available(agent): logging.debug("internet available") try: - grid_update_data(agent.last_session) + grid.update_data(agent.last_session) except Exception as e: logging.error("error connecting to the pwngrid-peer service: %s" % e) return @@ -155,13 +92,13 @@ def on_internet_available(agent): try: logging.debug("checking mailbox ...") - messages = grid_inbox() + messages = grid.inbox() TOTAL_MESSAGES = len(messages) UNREAD_MESSAGES = len([m for m in messages if m['seen_at'] is None]) if TOTAL_MESSAGES: on_ui_update(agent.view()) - logging.debug( " %d unread messages of %d total" % (UNREAD_MESSAGES, TOTAL_MESSAGES)) + logging.debug(" %d unread messages of %d total" % (UNREAD_MESSAGES, TOTAL_MESSAGES)) logging.debug("checking pcaps") @@ -189,7 +126,7 @@ def on_internet_available(agent): if is_excluded(essid) or is_excluded(bssid): logging.debug("not reporting %s due to exclusion filter" % pcap_file) - elif grid_report_ap(essid, bssid): + elif grid.report_ap(essid, bssid): reported.append(net_id) REPORT.update(data={'reported': reported}) else: