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
| Component | Details |
|---|---|
| Board | Raspberry Pi Zero 2 W |
| Display | Waveshare 2.13" V4 e-ink (250x122) |
| RTC | DS3231 on I2C, battery-backed |
| Battery | Waveshare UPS HAT (C) |
| GPS | VK-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 -wDNS 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-queriesTook 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-queriessync-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 || trueClock 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.