Contents

Reviving my Pwnagotchi after a year in a drawer

My pwnagotchi sat in a drawer for a year with a dead battery and outdated firmware. The e-ink face was frozen mid-smile. Time to fix that.

Pwnagotchi in 30 seconds

Pwnagotchi is an AI running on a Raspberry Pi that captures WPA handshakes from nearby WiFi networks. It uses reinforcement learning (A2C) to get better at choosing targets over time. Built on top of bettercap, has a tiny e-ink display with a Tamagotchi face, and runs fully autonomous on battery. You put it in a backpack, walk around, and it does its thing.

Hardware

ComponentDetails
BoardRaspberry Pi Zero 2 W
DisplayWaveshare 2.13" V4 e-ink (250x122)
RTCDS3231 on I2C, battery-backed
BatteryWaveshare UPS HAT (C)
GPSVK-172 USB dongle (u-blox 7)

Everything stacked through the 40-pin GPIO header passthrough. No free GPIO pins left, which killed my plan for a UART GPS module – had to go USB dongle instead.

Software

Running Pwnagotchi 2.8.9 from the jayofelony fork on Debian 12 bookworm. Kernel pinned to 6.6.20.

Why pinned? Because the stock kernel ships a 138KB brcmfmac driver that doesn’t support monitor mode. The patched one is 582KB. I learned this the fun way – ran apt upgrade, watched the driver get replaced, spent an hour wondering why nothing works anymore.

Never run apt upgrade on this thing. Auto-update plugin is disabled too.

Side note: PEP 668 on Debian 12 means you can’t pip install anything without --break-system-packages. Pwnagotchi 2.9.x requires a full reflash to Trixie, not a simple upgrade. So 2.8.9 it is.

Things that broke and how I fixed them

RTC not loading at boot. Spent way too long on this one. The DS3231 overlay was in the [pi5] section of config.txt. Pi Zero 2W needs it in [all]. Two characters of difference, an hour of debugging. The kind of thing that makes you question your life choices.

hwclock.service is masked. The standard systemd service for hardware clock sync is masked in this image. No explanation why. Wrote a custom service that writes system time to RTC on shutdown:

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/true
ExecStop=/sbin/hwclock -w

DNS through VPN. My Mac routes through a corporate VPN, and standard NAT with UDP forwarding just dies on utun interfaces. No error, no logs, just silence. The fix was running dnsmasq on the Mac side as a local DNS proxy:

sudo dnsmasq --no-daemon --listen-address="$MAC_IP" --bind-interfaces \
    --no-resolv --server="$DNS_SERVER" --log-queries

Took me longer to figure out than I’d like to admit.

What it captures

The device runs on battery, autonomous. The AI handles channel hopping and decides when to deauth or associate with nearby access points. Captured handshakes go to:

  • wpa-sec.stanev.org – free WPA handshake cracking
  • onlinehashcrack.com – cloud cracking with a dashboard
  • wigle.net – global WiFi map with GPS coordinates

It also participates in PwnGrid – a mesh network where pwnagotchis discover each other over the air.

Management scripts

I manage the device from my Mac over USB. Wrote a few scripts because typing the same SSH commands got old fast.

connect.sh – finds USB ethernet, assigns IP, SSHes in:

#!/bin/bash
set -e

PI_IP="10.0.0.2"
MAC_IP="10.0.0.1"
SSH_KEY="$HOME/.ssh/pwnagotchi"

for iface in en5 en6 en7 en8; do
    if ifconfig "$iface" 2>/dev/null | grep -q "status: active"; then
        IFACE="$iface"
        break
    fi
done

[ -z "$IFACE" ] && echo "No USB Ethernet interface found." && exit 1

if ! ifconfig "$IFACE" | grep -q "$MAC_IP"; then
    sudo ifconfig "$IFACE" "$MAC_IP" netmask 255.255.255.0 up
fi

for i in $(seq 1 10); do
    ping -c 1 -W 1 "$PI_IP" &>/dev/null && break
    [ "$i" -eq 10 ] && echo "Pwnagotchi not reachable" && exit 1
    sleep 1
done

ssh -i "$SSH_KEY" pi@"$PI_IP"

share-internet.sh – NAT + dnsmasq, works over VPN:

#!/bin/bash
set -e

PI_IP="10.0.0.2"
MAC_IP="10.0.0.1"

OUT_IFACE=$(route -n get default 2>/dev/null | grep interface | awk '{print $2}')
DNS_SERVER=$(scutil --dns | grep 'nameserver\[0\]' | head -1 | awk '{print $3}')

sudo sysctl -w net.inet.ip.forwarding=1
echo "nat on $OUT_IFACE from 10.0.0.0/24 to any -> ($OUT_IFACE)" | sudo pfctl -ef -

sudo dnsmasq --no-daemon --listen-address="$MAC_IP" --bind-interfaces \
    --no-resolv --server="$DNS_SERVER" --log-queries

sync-time.sh – pushes Mac’s UTC to Pi and hardware RTC:

#!/bin/bash
set -e
PI_IP="10.0.0.2"
SSH_KEY="$HOME/.ssh/pwnagotchi"
CURRENT_UTC=$(date -u '+%Y-%m-%d %H:%M:%S')

ssh -i "$SSH_KEY" pi@"$PI_IP" \
    "sudo date -u -s '$CURRENT_UTC' && sudo hwclock -w && echo 'RTC synced:' && sudo hwclock -r"

backup.sh – config, plugins, AI brain, handshakes:

#!/bin/bash
set -e
PI_IP="10.0.0.2"
SSH_KEY="$HOME/.ssh/pwnagotchi"
DATE=$(date +%Y%m%d)
BACKUP_DIR="./backups/$DATE"
mkdir -p "$BACKUP_DIR"

scp -i "$SSH_KEY" pi@"$PI_IP":/etc/pwnagotchi/config.toml "$BACKUP_DIR/"

ssh -i "$SSH_KEY" pi@"$PI_IP" "sudo tar czf /tmp/pwnagotchi-backup.tar.gz /etc/pwnagotchi/"
scp -i "$SSH_KEY" pi@"$PI_IP":/tmp/pwnagotchi-backup.tar.gz "$BACKUP_DIR/"

ssh -i "$SSH_KEY" pi@"$PI_IP" "sudo tar czf /tmp/custom-plugins.tar.gz /usr/local/share/pwnagotchi/custom-plugins/"
scp -i "$SSH_KEY" pi@"$PI_IP":/tmp/custom-plugins.tar.gz "$BACKUP_DIR/"

scp -i "$SSH_KEY" pi@"$PI_IP":/root/brain.nn "$BACKUP_DIR/" 2>/dev/null || true

ssh -i "$SSH_KEY" pi@"$PI_IP" "sudo tar czf /tmp/handshakes.tar.gz /root/handshakes/" 2>/dev/null
scp -i "$SSH_KEY" pi@"$PI_IP":/tmp/handshakes.tar.gz "$BACKUP_DIR/" 2>/dev/null || true

Clock plugin

The built-in clock options didn’t show the format I wanted. 28 lines of Python fixed that:

from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
import pwnagotchi.ui.fonts as fonts
import pwnagotchi.plugins as plugins
import datetime

class PwnClock(plugins.Plugin):
    __author__ = "originally https://github.com/LoganMD redone by NeonLightning"
    __version__ = "1.0.4"
    __license__ = "GPL3"
    __description__ = "Clock/Calendar for pwnagotchi"

    def on_loaded(self):
        pass

    def on_ui_setup(self, ui):
        ui.add_element("clock", LabeledValue(
            color=BLACK, label="", value="--/--  --:--",
            position=(148, 109),
            label_font=fonts.Small, text_font=fonts.Small))

    def on_ui_update(self, ui):
        now = datetime.datetime.now()
        ui.set("clock", now.strftime("%d/%m %H:%M"))

    def on_unload(self, ui):
        with ui._lock:
            ui.remove_element("clock")

PwnGrid

If your pwnagotchi meets mine over the air, you’ll see gumeniukcom. Or look me up directly:

opwngrid.xyz/search/f4548ccf636ea7a2fa2b642ea99db5d32592a49a8944903aa53b6e8b38ae8490

What’s next

VK-172 GPS dongle is on its way. It connects via micro-USB OTG – same port used for Mac connection, so GPS only works in autonomous mode. Fine by me. Once it arrives, handshakes get geotagged and fed into wigle.net.

The whole setup is in a git repo. Config, plugins, scripts, docs. Because if it’s not in git, it doesn’t exist.