pwny is a touchscreen-driven WiFi penetration testing tool built with Python and Kivy. It runs as a fullscreen kiosk application on a Raspberry Pi 5 with a 480x320 SPI touchscreen (ADS7846), using a USB WiFi adapter for monitor mode, packet injection, deauthentication attacks, WPA handshake capture, brute-force password cracking, and rogue hotspot bridging (Leech AP).
Read more about how and why I built this: https://jndl.dev/blog/pwny
Legal disclaimer: This tool is intended exclusively for authorized security testing and educational purposes. Unauthorized access to computer networks is illegal. Always obtain explicit written permission before testing any network you do not own.
- Project Overview
- Architecture
- Features
- Hardware Requirements
- Software Dependencies
- Installation and Setup
- Technical Details
pwny provides a complete WiFi auditing workflow from a pocket-sized device:
- Scan nearby access points (2.4 GHz, 5 GHz, 6 GHz) using
nmcli - Classify each AP's security (Open, WPA2-PSK/vulnerable, WPA3/SAE, Enterprise)
- Attack a selected target: enter monitor mode on the USB adapter, capture
packets with Scapy's
AsyncSniffer, run continuous deauthentication viaaireplay-ng, and detect EAPOL 4-way handshake frames in real time - Brute force captured WPA handshakes using
aircrack-ngwith dictionary wordlists or exhaustive character-set generation viaitertools.product - Leech a cracked network: connect to the upstream AP as a station on the
built-in
wlan0, create a new hotspot onap0(USB adapter) withhostapddnsmasq, and bridge traffic withnftablesNAT + policy routing
- System controls: restart the kiosk service, reboot, or shut down
The GUI is a Kivy application sized for 480x320 pixels, rendered via SDL2's KMSDRM backend (no X11/Wayland required). Touch input is handled through the ADS7846 SPI touchscreen controller with calibrated coordinate inversion.
/home/admin/pwny/
app.py Kivy GUI: screens, navigation, widgets, periodic refresh
actions.py Core logic: PCAP capture, handshake analysis, brute force
actions_safe.py Safe/non-privileged actions: scanning, selection, PCAP listing
wifi_mon.py Monitor mode management, channel tuning, DFS CAC, deauth
wifi_safe.py nmcli-based AP scanning, security classification, band detection
leech.py Leech AP: upstream STA + downstream hotspot bridging
state.py Shared application state (dataclass singleton)
utils.py System metrics (CPU/RAM/disk/temp), directory helpers, JSON I/O
run-kivy-manual.sh Manual launch script with touchscreen calibration
pcap_info.json Persistent metadata for all captured PCAPs
pcaps/ Directory for saved .pcap files
dicts/ Directory for wordlist files (dictionaries)
+----------+
| app.py | (Kivy GUI, ScreenManager)
+----+-----+
|
+--------------+--------------+
| | |
+-------+----+ +-----+------+ +----+-------+
|actions_safe | | actions | | wifi_mon |
| .py | | .py | | .py |
+------+------+ +-----+-----+ +-----+------+
| | |
+------+------+ | +----+-------+
| wifi_safe.py | | | leech.py |
+------+------+ | +-----+------+
| | |
+---------+----+---------+----+
| |
+-----+----+ +----+-----+
| state.py | | utils.py |
+----------+ +----------+
The main application entry point. Configures a 480x320 fullscreen kiosk window
via Kivy's Config system with KMSDRM/SDL2 rendering. Contains:
KioskApp-- The KivyAppsubclass; creates required directories on startup.Router(ScreenManager) -- A stack-based screen navigator withpush()/back()that instantiates fresh screen instances on each navigation (no stale widget state).MainMenu-- Home screen with buttons for all features. The Attack button dynamically shows the selected AP's SSID and is disabled when no AP is selected. The Reset button stops all active operations (deauth, capture, brute force).AccessPoints-- Scan results displayed in a scrollable list with columns: VULN (color-coded: green=YES, red=NO, yellow=OPEN), SSID, BAND, CH, SIG. Each row is anAPRowwidget with rounded-rectangle backgrounds and an accent bar on selection. Tapping a row selects the AP for attack.Attack-- Monitor mode operations. On screen entry, callsensure_mon()andtune_channel(). Provides buttons for PCAP capture toggle, handshake parsing, continuous deauthentication toggle, and log clearing. Shows live packet/EAPOL counts and auto-stops deauth when a handshake is captured.BruteForce-- Password cracking screen. Select a PCAP and either a dictionary file or configure RNG (character set, min/max length). Shows live progress with tested/total count, percentage, kH/s rate, elapsed time, and ETA. Stores cracked passwords inpcap_info.json.PCAPs-- Browse captured PCAP files sorted by modification time. Displays metadata: SSID, date, EAPOL count, handshake/password status. Tags:[HS]for handshake present,[PW]for password cracked.LeechAP-- Configure and launch a rogue bridging hotspot. Shows upstream AP info and cracked password. Editable fields for leech SSID and password. Live status with client count and uptime.SystemScreen-- Restart kivy-pwny.service, reboot, or shutdown.MetricBar-- A status bar refreshed every 2 seconds showing CPU%, RAM%, Disk%, and SoC temperature.APRow-- Custom widget combiningButtonBehavior+BoxLayoutwith canvas drawing for rounded backgrounds, selection highlight accent bar, and border.modal()/confirm_popup()-- Reusable popup helpers for info/confirm dialogs.
All background operations use threading.Thread(daemon=True) with GUI updates
marshalled back to the main thread via Clock.schedule_once().
Implements PCAP capture, handshake detection, and brute-force attacks:
-
PCAP capture via Scapy's
AsyncSniffer:pcap_start(ap, iface)-- Start sniffing on the monitor interface, filtering packets by the target BSSID (checked across addr1/addr2/addr3 of Dot11 frames). Packets are accumulated in a thread-safe list (_capture_lock).pcap_stop(ap)-- Stop the sniffer. Optionally saves to disk or returns statistics for the UI to decide (e.g., "No handshake -- save anyway?")._pcap_save(ap)-- Write packets topcaps/<SSID>_<BSSID>_<timestamp>.pcapusing Scapy'swrpcap(). Updatespcap_info.jsonwith metadata.pcap_discard()-- Clear captured packets without saving.pcap_packet_count(),pcap_eapol_count()-- Live counters during capture.pcap_handshake_status(bssid)-- Real-time EAPOL breakdown: frames from AP (M1/M3), frames from client (M2/M4), other EAPOL types.
-
EAPOL analysis (
_eapol_breakdown()):- Inspects each EAPOL frame in the capture.
- EAPOL type 3 = EAPOL-Key (WPA 4-way handshake messages).
- Source MAC == BSSID indicates M1 or M3 (from AP).
- Source MAC != BSSID indicates M2 or M4 (from client).
- A valid handshake requires at least one frame from each side.
- Non-Key EAPOL (Start/Logoff/EAP) may indicate WPA-Enterprise.
-
Handshake verification (
check_handshake()):- First pass: Scapy analysis with
_eapol_breakdown(). - Second pass: Definitive check via
aircrack-ng <pcap>withinput='q\n'to read the network listing and parse(N handshake)counts.
- First pass: Scapy analysis with
-
Password lookup (
lookup_password(bssid, ssid)):- Searches
pcap_info.jsonfor any entry matching the BSSID with a cracked password. Falls back to SSID match (same SSID on different bands has the same password but different BSSIDs).
- Searches
-
Dictionary brute force (
run_bruteforce()):- Launches
aircrack-ng -w <wordlist> [-b <bssid>] <pcap>in a background thread. - Reads stdout via
_read_cr_lines()which splits on both\rand\nto catch aircrack-ng's carriage-return progress overwrites. - ANSI escape sequences are stripped via
_ANSI_REregex. - Parses progress: keys tested, k/s rate, percentage.
- Reports to UI every 2 seconds: tested/total, percentage, kH/s, elapsed, ETA.
- Detects
KEY FOUND! [ <password> ]in output.
- Launches
-
RNG brute force (
run_bruteforce_rng()):- Generates passwords using
itertools.product(charset, repeat=length)for each length frommin_lentomax_len. - Pipes passwords to
aircrack-ng -w - -b <bssid> <pcap>via stdin. - A separate reader thread monitors stdout for
KEY FOUND!. - Flushes stdin every 5000 candidates. Reports progress every 2 seconds.
calc_combinations(charset, min_len, max_len)computes total keyspace:sum(len(charset)**i for i in range(min_len, max_len+1)).
- Generates passwords using
-
Character sets (
CHARSETS):lower: a-z (26 chars)upper: A-Z (26 chars)digits: 0-9 (10 chars)special:!@#$%^&*()-_=+[]{}|;:,.<>?/(28 chars)
Non-privileged operations that do not require monitor mode:
do_scan(callback)-- Runsscan_access_points()in a background thread. Results are stored inSTATE.last_scanand passed to the callback.set_selection(ap)/clear_selections()-- ManageSTATEselection fields.list_pcaps()-- Lists.pcap/.pcapngfiles inpcaps/, sorted by modification time (newest first).read_pcap_meta(name)-- Reads metadata for a PCAP frompcap_info.json.write_placeholder_pcap_and_meta(ap)-- Creates an empty PCAP placeholder with metadata (used for defensive/logging mode).
Uses nmcli (NetworkManager CLI) for WiFi scanning without monitor mode:
-
scan_access_points(dedup='ssid_band')-- Runsnmcli dev wifi rescanthen parses terse output with fields: SSID, BSSID, SECURITY, SIGNAL, CHAN, FREQ. Supports both pipe-delimited and colon-delimited nmcli output formats. The colon parser uses a MAC regex sliding window to handle SSIDs containing colons. -
classify_security(sec_field)-- Classifies each AP:- Open/Legacy: empty, NONE, WEP --
is_open=True, vulnerable=False - Enterprise (802.1X): EAP, 802.1X --
vulnerable=False - WPA3/SAE: WPA3, SAE, OWE --
vulnerable=False - WPA2-PSK: WPA2, RSN, PSK, WPA --
vulnerable=True(attackable)
- Open/Legacy: empty, NONE, WEP --
-
_band_from_freq_or_chan()-- Determines band from frequency or channel number:- 2400-2500 MHz / ch 1-14 = 2.4 GHz
- 5170-5895 MHz / ch 32-177 = 5 GHz
- 5925-7125 MHz = 6 GHz
-
Deduplication: By default, groups APs by (SSID, band), keeping the strongest signal. Sort order: vulnerable-secured first, then open, then non-vulnerable.
Manages the USB WiFi adapter for monitor mode, channel tuning, DFS CAC, and deauthentication attacks. Key components:
-
Tool resolution: Finds system binaries (
iw,ip,rfkill,hostapd,aireplay-ng) viashutil.which()with fallback paths in/usr/sbin/,/usr/bin/,/sbin/,/bin/. -
PHY auto-detection (
_detect_usb_phy()):- Scans
/sys/class/ieee80211/*/devicesymlinks for USB-backed PHYs. - Handles USB re-enumeration via
_refresh_phy()(PHY names can change after device disconnect/reconnect). - Can be overridden via
PWN_MON_PHYenvironment variable.
- Scans
-
Monitor interface management:
ensure_mon(default_ch, bw)-- Unblocks rfkill, sets regulatory domain, then callsretune_root().retune_root(ch, bw)-- Destroys all interfaces on the PHY, creates a freshmon1monitor interface, sets the channel, and brings it up. Tries three strategies in order:- Interface-level channel set while DOWN, then bring UP
- Interface-level channel set while UP (required by some Realtek/Mediatek drivers)
- PHY-level channel set as last resort
create_mon_iface(phy, mon_name)-- Creates monitor interface with fallback to converting an existing managed interface to monitor type.
-
DFS channel support:
is_dfs_channel(ch)-- Channels 52-64 and 100-140 are DFS.dfs_cac_timeout_seconds(ch)-- 60s normally; 600s for weather radar channels 120-128.tune_channel(ch, allow_dfs)-- Tries direct monitor tune first (many drivers allow DFS on monitor interfaces without CAC since they are passive). If that fails, performs DFS CAC via hostapd, then retunes. Falls back to nearest non-DFS channel (ch 36) if CAC also fails._perform_cac_on_dfs(ch, bw)-- Creates a temporary AP interface, writes a minimal hostapd config, runs hostapd in foreground, monitors its output forDFS-CAC-COMPLETEDorDFS-RADAR-DETECTED, then cleans up.
-
Deauthentication:
start_deauth(bssid, iface, client)-- Launchesaireplay-ng -0 0 -a <bssid>as a persistent background process. The-0 0flag means continuous deauth (infinite count). A reader thread counts burst lines and logs every 50th burst.stop_deauth()-- Terminates the aireplay-ng process.send_deauths()-- Legacy single-burst deauth (finite count).
-
Scapy initialization (
scapy_init_monitor()):- Forces
PF_PACKETsocket mode (no pcap). - Reloads interface list and sets the default interface to
mon1.
- Forces
-
Diagnostic helpers:
send_probe()-- Injects a deauth frame via Scapysendp().send_test_beacon()-- Injects beacon frames for testing.get_mon_mac()-- Gets MAC from sysfs, Scapy raw, or generates a random LAA MAC.
Creates a transparent WiFi bridge: connects to a cracked upstream AP as a station
on wlan0 (built-in WiFi), then rebroadcasts as a hotspot on ap0 (USB adapter).
Startup sequence (leech_start()):
- Detect USB PHY and tear down any monitor mode interfaces.
- Connect
wlan0to upstream vianmcli dev wifi connect <ssid> password <pw>. - Wait for DHCP IP (polls
nmclifor up to 15 seconds). - Create
ap0AP interface on the USB PHY (iw phy <phy> interface add ap0 type __ap). - Configure
ap0with static IP192.168.4.1/24. - Policy routing: Create routing table 100 so traffic from
192.168.4.0/24is routed through the upstream gateway onwlan0(noteth0). - Enable NAT via nftables: masquerade outgoing traffic on
wlan0, accept all forwarded traffic. - Start dnsmasq: DHCP server on
ap0(range192.168.4.10-192.168.4.200), DNS forwarding to8.8.8.8/8.8.4.4. - Start hostapd: AP on
ap0with configured SSID/password, 802.11g mode, WPA2-PSK (CCMP) if password provided, open if not.
Shutdown sequence (leech_stop()):
- Terminate hostapd and dnsmasq processes.
- Remove nftables NAT table and policy routing rules.
- Disconnect
wlan0from upstream. - Delete
ap0interface.
Status monitoring (leech_status()):
- Reports upstream SSID/IP, leech SSID, uptime, and connected client count
(via
iw dev ap0 station dump).
A singleton AppState dataclass used across all modules:
@dataclass
class AppState:
selected_ap: Optional[Dict] = None # Currently selected target AP
selected_pcap: Optional[str] = None # Currently selected PCAP filename
current_pcap_meta: Optional[Dict] = None # Metadata for selected PCAP
last_scan: List[Dict] = field(default_factory=list) # Latest scan results
monitor_on: bool = False # Monitor mode active
pcap_on: bool = False # PCAP capture active
counter_running: bool = False # Packet counter active
STATE = AppState()system_metrics()-- Returns CPU%, RAM%, disk%, and SoC temperature viapsutil. Temperature is read frompsutil.sensors_temperatures()or the sysfs thermal zone.ensure_dirs(base)-- Createspcaps/anddicts/directories if they don't exist.load_pcap_info(base)/save_pcap_info(info, base)-- JSON I/O forpcap_info.json.
- Uses
nmcli dev wifi rescanfollowed by terse output parsing. - Displays: vulnerability status (color-coded), SSID, band (2.4/5/6 GHz), channel, signal strength (0-100%).
- Security classification:
- Green "YES" = WPA2-PSK (vulnerable to handshake capture + brute force)
- Red "NO" = WPA3/SAE or Enterprise (not attackable with this tool)
- Yellow "OPEN" = Open/WEP (dimmed, no handshake to capture)
- Deduplication by SSID + band (keeps strongest signal per group).
- Sorted: vulnerable networks first, then open, then non-vulnerable.
- Tap to select a target AP for attack.
On entering the Attack screen:
ensure_mon()creates themon1monitor interface on the USB adapter's PHY.tune_channel()tunes to the target AP's channel (with DFS support).scapy_init_monitor()configures Scapy for the monitor interface.
Available operations:
- PCAP ON/OFF: Toggle packet capture. On stop, checks for a WPA handshake. If found, saves automatically. If not, prompts "Save anyway?" or discard.
- Deauth ON/OFF: Toggle continuous deauthentication (
aireplay-ng -0 0). Forces clients to reconnect, generating EAPOL handshake frames. Automatically stops when a handshake is detected. - Parse: Run definitive handshake check on the selected PCAP using aircrack-ng.
- Clear Log: Clear the scrollable log area.
Live info display shows:
- AP details: SSID, BSSID, AUTH type, channel, signal, vulnerability status.
- Capture stats: packet count, EAPOL breakdown (AP frames, client frames, other).
- Handshake status: green "HS CAPTURED" when both AP and client EAPOL-Key frames are present.
- Deauth status: red "DEAUTH ACTIVE" indicator.
Two attack modes for cracking WPA handshakes:
- Select a PCAP with a captured handshake and a wordlist file from
dicts/. - Runs
aircrack-ng -w <wordlist> [-b <bssid>] <pcap>. - Progress parsing: tested keys / total, percentage, kH/s throughput, elapsed time, and estimated time remaining (ETA).
- Wordlist size is counted beforehand for accurate progress.
- Configure character sets via toggle buttons: lowercase (a-z), uppercase (A-Z), digits (0-9), special characters (!@#$%^&*...).
- Set minimum and maximum password length (1-63 characters).
- Shows total combination count before starting.
- Generates all permutations via
itertools.product(charset, repeat=length)and pipes them toaircrack-ng -w -(stdin mode). - Progress: tested/total, percentage, kH/s, elapsed, ETA.
On success:
- Displays the cracked password in a modal dialog.
- Stores the password in
pcap_info.jsonfor the corresponding PCAP entry. - Password is then available across the application (Attack screen, PCAPs screen,
Leech AP screen) via
lookup_password().
- Lists all
.pcapand.pcapngfiles inpcaps/, sorted newest first. - Each entry shows: SSID, capture date, EAPOL count, status tags:
[PW]= password cracked[HS]= handshake present (but not yet cracked)
- Selecting a PCAP shows detailed metadata: SSID, BSSID, date, packet count, EAPOL count, and cracked password (if known).
- Password lookup checks both exact BSSID match and SSID fallback (for dual-band APs sharing the same password with different BSSIDs).
Requires a cracked password for the target AP. Creates a transparent bridge:
- Upstream connection:
wlan0(Pi's built-in WiFi) connects as a station to the cracked AP usingnmcli. - Downstream hotspot:
ap0(USB WiFi adapter) runshostapdbroadcasting a configurable SSID and WPA2 password. - DHCP:
dnsmasqserves IPs in192.168.4.10-200range with DNS forwarding to Google DNS (8.8.8.8, 8.8.4.4). - NAT:
nftablesmasquerade rule on the upstream interface. - Policy routing: Table 100 ensures hotspot client traffic is routed through
wlan0(noteth0or other interfaces).
Configuration fields:
- Leech SSID (default: "pifi")
- Leech Password (min 8 chars for WPA2, or empty for open)
Live status displays: upstream SSID/IP, hotspot SSID, connected client count
(from iw dev ap0 station dump), and uptime.
- Restart Service:
systemctl restart kivy-pwny.service - Reboot:
reboot - Shut Down:
shutdown -h now
All executed via sudo -n (passwordless sudo).
| Component | Details |
|---|---|
| SBC | Raspberry Pi 5 |
| Display | 480x320 SPI touchscreen with ADS7846 touch controller |
| WiFi Adapter | USB WiFi adapter supporting monitor mode and AP mode (e.g., rt2800usb chipset). Must support nl80211 driver. |
| Built-in WiFi | Pi 5's onboard wlan0 (used for Leech AP upstream connection) |
| Storage | microSD card (captures and wordlists stored locally) |
The USB adapter is used for:
- Monitor mode + packet injection (scanning target channels, capturing handshakes)
- AP mode (Leech AP downstream hotspot via
ap0)
The built-in wlan0 is used for:
- Leech AP upstream connection (station mode via NetworkManager)
The code includes fallback strategies for various driver quirks:
- rt2800usb: Primary tested chipset. Supports monitor mode, AP mode, and DFS channels in monitor mode without CAC.
- Realtek/Mediatek: Fallback channel-setting strategy (set while interface is UP).
- Drivers requiring type conversion rather than new interface creation are handled
by the
create_mon_iface()fallback path.
| Package | Purpose |
|---|---|
kivy |
GUI framework (touchscreen kiosk interface) |
scapy |
Packet capture (AsyncSniffer), injection (sendp), PCAP I/O |
psutil |
System metrics (CPU, RAM, disk, temperature) |
| Tool | Package | Purpose |
|---|---|---|
aircrack-ng |
aircrack-ng |
Handshake verification, dictionary brute force |
aireplay-ng |
aircrack-ng |
Deauthentication attacks (continuous + burst) |
iw |
iw |
Interface management, monitor mode, channel setting, PHY info |
ip |
iproute2 |
Interface up/down, IP addressing, routing, policy rules |
nmcli |
network-manager |
WiFi scanning, upstream AP connection |
hostapd |
hostapd |
DFS CAC, Leech AP hotspot |
dnsmasq |
dnsmasq |
DHCP + DNS for Leech AP clients |
nft |
nftables |
NAT masquerade for Leech AP |
sysctl |
procps |
Enable/disable IP forwarding |
rfkill |
util-linux |
Unblock WiFi (optional) |
The application runs as root via the systemd service, but when run manually it
needs passwordless sudo access for privileged commands. Three sudoers files exist
in /etc/sudoers.d/:
pwny-tools-- Grants NOPASSWD access to:iw,ip,rfkill,hostapd,aireplay-ng,pkill,sysctl,nft,nmcli,systemctl,reboot,shutdown,dnsmasqpwny-- Additional pwny-specific permissionskivy_pwny-- Service-related permissions
The code uses sudo -n (non-interactive) for all privileged operations, which
requires these sudoers entries to be configured.
sudo apt-get update
sudo apt-get install -y \
aircrack-ng \
hostapd \
dnsmasq \
iw \
nftables \
network-manager \
python3-pip \
python3-venvpip3 install kivy scapy psutilThe application automatically creates required directories on startup, but you can create them manually:
mkdir -p /home/admin/pwny/pcaps
mkdir -p /home/admin/pwny/dictsPlace wordlist files in /home/admin/pwny/dicts/. Included wordlists:
| File | Size | Description |
|---|---|---|
probable-v2-wpa-top447.txt |
4 KB | Top 447 most probable WPA passwords |
probable-v2-wpa-top4800.txt |
45 KB | Top 4800 probable WPA passwords |
Top204Thousand-WPA-probable-v2.txt |
2 MB | Top 204K probable WPA passwords |
rockyou-wpa.txt |
109 MB | RockYou list filtered for WPA (8+ chars) |
Create /etc/sudoers.d/pwny-tools (mode 0440, owner root:root):
admin ALL=(ALL) NOPASSWD: /usr/sbin/iw, /sbin/ip, /usr/sbin/rfkill, \
/usr/sbin/hostapd, /usr/sbin/aireplay-ng, /usr/bin/pkill, \
/usr/sbin/sysctl, /usr/sbin/nft, /usr/bin/nmcli, \
/bin/systemctl, /sbin/reboot, /sbin/shutdown, /usr/sbin/dnsmasq
Note: Adjust paths based on your system. Use
which <tool>to find exact locations. When running as root (the systemd service does this), sudo is automatically skipped.
The kiosk service is at /etc/systemd/system/kivy-pwny.service:
[Unit]
Description=Kivy Pwny on SPI TFT (KMSDRM)
After=network-online.target getty@tty1.service
Conflicts=getty@tty1.service
[Service]
Type=simple
User=root
ExecStartPre=-/bin/systemctl stop wpa_supplicant
ExecStartPre=-/bin/systemctl stop iwd
ExecStartPre=-/usr/sbin/rfkill unblock wifi
ExecStartPre=-/bin/sh -c '/usr/sbin/iw reg set SK || true'
Environment=PWN_MON_IF=mon1
Environment=PWN_BASE_IF=wlan1
WorkingDirectory=/home/admin/pwny
SupplementaryGroups=video input render
StandardInput=tty
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
Environment=LANG=C.UTF-8
Environment=KIVY_LOG_LEVEL=debug
Environment=SDL_VIDEODRIVER=kmsdrm
Environment=SDL_AUDIODRIVER=dummy
Environment=KIVY_WINDOW=sdl2
Environment=PYTHONUNBUFFERED=1
StandardOutput=journal
StandardError=journal
ExecStart=/usr/local/bin/run-pwny.sh
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetThe launch script (/usr/local/bin/run-pwny.sh) handles:
- Touchscreen calibration (ADS7846 coordinate inversion and range mapping)
- Kivy config generation (
$KIVY_HOME/config.iniwith hidinput provider) - SDL/KMSDRM environment setup
Enable and start:
sudo systemctl enable kivy-pwny.service
sudo systemctl start kivy-pwny.service| Variable | Default | Description |
|---|---|---|
PWN_MON_IF |
mon1 |
Monitor mode interface name |
PWN_BASE_IF |
wlan1 |
Base interface name for USB adapter |
PWN_AP_IF |
ap0 |
AP interface name (DFS CAC, Leech AP) |
PWN_MON_PHY |
(auto-detect) | Force a specific PHY (e.g., phy1). If unset, auto-detects the USB-backed PHY. |
PWN_REGDOM |
SK |
Regulatory domain country code (affects available channels/power) |
PWN_BW |
(none) | Channel bandwidth: HT20, HT40+, HT40-, VHT80 |
The USB WiFi adapter's PHY name (e.g., phy0, phy1, phy2) can change after:
- System boot ordering differences
- USB hub re-enumeration
- Device disconnect/reconnect
The auto-detection algorithm (_detect_usb_phy() in both wifi_mon.py and
leech.py):
- Scans all entries under
/sys/class/ieee80211/. - Resolves the
devicesymlink to its real path. - Checks if the real path contains
/usb(indicating a USB-backed controller). - Returns the first matching PHY name.
_refresh_phy() is called before each major operation (monitor setup, channel
tune) to handle runtime PHY changes. If PWN_MON_PHY is set, auto-detection
is bypassed.
DFS (Dynamic Frequency Selection) channels (52-64, 100-140 in the 5 GHz band) require Channel Availability Check (CAC) before transmission. The tool handles this in three stages:
-
Direct monitor tune: Many drivers (including rt2800usb) allow setting DFS channels on monitor interfaces without CAC, since monitor mode is passive (receive-only) with injection not requiring CAC clearance.
-
CAC via hostapd: If direct tune fails, a temporary AP interface is created and hostapd is launched with a minimal DFS-capable config:
ieee80211d=1,ieee80211h=1(802.11d/h required for DFS)hw_mode=a(5 GHz)- Monitors hostapd output for
DFS-CAC-COMPLETED,AP-ENABLED, orDFS-RADAR-DETECTED - CAC timeout: 65 seconds normally, 600 seconds for weather radar channels (120-128)
- On completion, hostapd is terminated, the AP interface is cleaned up, and the monitor interface is recreated on the now-cleared channel.
-
Fallback to non-DFS: If CAC fails (radar detected or timeout), falls back to channel 36 (UNII-1, no DFS required).
The WPA 4-way handshake consists of four EAPOL-Key frames:
| Message | Direction | Content |
|---|---|---|
| M1 | AP -> Client | ANonce |
| M2 | Client -> AP | SNonce + MIC |
| M3 | AP -> Client | GTK + MIC |
| M4 | Client -> AP | ACK |
Detection logic in _eapol_breakdown():
- Filters for EAPOL frames with
type == 3(EAPOL-Key). - Checks
Dot11.addr2(source MAC):- If source == BSSID: frame is from AP (M1 or M3).
- If source != BSSID: frame is from client (M2 or M4).
- A handshake is considered captured when there is at least one frame from each
side (
key_from_ap > 0 and key_from_client > 0). - Non-Key EAPOL (type != 3) is counted separately and may indicate WPA-Enterprise authentication (EAP packets) which is not crackable with PSK brute force.
The live capture UI shows the breakdown in real time (e.g., "3 AP, 2 CLI, 1 other") and turns green with "HS CAPTURED" when a handshake is detected.
Definitive verification is done via aircrack-ng which parses the full handshake
and reports the count in its network listing output.
aircrack-ng uses \r (carriage return) to overwrite progress lines in-place and
ANSI escape codes for terminal formatting. The tool handles both:
Carriage-return line reading (_read_cr_lines()):
- Reads raw bytes from the process stdout file descriptor.
- Splits on both
\rand\n(handles\r\npairs correctly). - Yields each line segment for real-time progress parsing.
ANSI stripping (_ANSI_RE):
re.compile(r'\x1b\[[0-9;]*[A-Za-z]|\x1b\[\??[0-9;]*[a-zA-Z]')Strips all CSI escape sequences (colors, cursor movement, etc.) from output.
Progress parsing: Matches patterns like:
12345 keys tested (800.12 k/s)-- keys tested and rate12345 / 67890 keys-- tested / total with both formatsKEY FOUND! [ password123 ]-- success detection
All background operations (scanning, capture, brute force, leech setup) run in
threading.Thread(daemon=True) to avoid blocking the Kivy event loop. GUI
updates from these threads are marshalled via:
Clock.schedule_once(lambda *_: self.add_log(text), 0)This schedules the update to run on the next Kivy main loop iteration, which is the only safe way to modify Kivy widgets from non-main threads. The pattern is used consistently throughout:
- Scan completion callbacks
- Log messages from background threads
- Brute force progress updates
- Leech AP start/stop status updates
on_donecallbacks for password discovery
Periodic UI refresh is handled via Clock.schedule_interval() with varying
rates:
- MetricBar: every 2.0 seconds
- Main menu: every 0.5 seconds
- Attack screen: every 1.0 seconds
- Brute force screen: every 1.0 seconds
- PCAPs screen: every 1.5 seconds
- Leech AP status: every 2.0 seconds
All PCAP metadata is persisted in /home/admin/pwny/pcap_info.json. Each entry
is keyed by the PCAP filename:
{
"SSID_BSSID_YYYYMMDD_HHMMSS.pcap": {
"ssid": "NetworkName",
"bssid": "cc:ce:1e:a1:30:d9",
"date": "20260216_102607",
"lat": null,
"lng": null,
"password": "crackedPassword123",
"packets": 2821,
"eapol": 12,
"has_hs": true,
"notes": "Captured 2821 pkts, 12 EAPOL."
}
}| Field | Type | Description |
|---|---|---|
ssid |
string | Network SSID |
bssid |
string | AP BSSID (MAC address) |
date |
string | Capture timestamp (YYYYMMDD_HHMMSS) |
lat/lng |
null | GPS coordinates (reserved, not yet implemented) |
password |
string/null | Cracked WPA password (null if not yet cracked) |
packets |
int | Total packets captured |
eapol |
int | EAPOL frame count |
has_hs |
bool | Whether a handshake was detected at capture time |
notes |
string | Human-readable summary |
The password field is populated by run_bruteforce() / run_bruteforce_rng()
on successful key recovery. lookup_password() searches this file by BSSID
(exact match) or SSID (fallback for dual-band APs).
The Leech AP creates a full network bridge without Linux bridging (brctl),
instead using routing + NAT:
Network topology:
[Internet] <-> [Upstream AP] <-(WiFi)-> [wlan0: STA] <-(routing)-> [ap0: AP] <-(WiFi)-> [Clients]
Pi 5
IP addressing:
wlan0: DHCP from upstream (e.g.,192.168.178.x/24)ap0: Static192.168.4.1/24- Clients: DHCP from dnsmasq (
192.168.4.10-192.168.4.200)
nftables NAT rules (written to /tmp/leech-nat.nft and loaded atomically):
table ip leech_nat {
chain postrouting {
type nat hook postrouting priority 100;
oifname "wlan0" masquerade
}
chain forward {
type filter hook forward priority 0; policy accept;
}
}
Policy routing (table 100):
ip rule add from 192.168.4.0/24 table 100
ip route add default via <upstream_gw> dev wlan0 table 100
This ensures hotspot client traffic is always routed through wlan0 to the
upstream AP, even if the Pi has other default routes (e.g., via eth0).
IP forwarding:
sysctl -w net.ipv4.ip_forward=1
dnsmasq configuration (/tmp/dnsmasq-leech.conf):
interface=ap0
bind-dynamic
dhcp-range=192.168.4.10,192.168.4.200,255.255.255.0,24h
dhcp-option=option:router,192.168.4.1
dhcp-option=option:dns-server,192.168.4.1
no-resolv
server=8.8.8.8
server=8.8.4.4
log-queries
log-dhcp
hostapd configuration (/tmp/hostapd-leech.conf):
interface=ap0
driver=nl80211
ssid=<leech_ssid>
hw_mode=g
channel=6
ieee80211n=1
wmm_enabled=1
auth_algs=1
macaddr_acl=0
wpa=2
wpa_key_mgmt=WPA-PSK
wpa_passphrase=<leech_password>
rsn_pairwise=CCMP
Teardown restores the system: kills processes, removes NAT table and policy
routes, disables IP forwarding, disconnects from upstream, and deletes ap0.
<SSID>_<BSSID_no_colons>_<YYYYMMDD_HHMMSS>.pcap
Example: FRITZ!Box_6490_Cable_ccce1ea130d9_20260216_102607.pcap
- Spaces in SSID are replaced with underscores.
- BSSID colons are stripped.
- Timestamp is in local time.
The launch script (/usr/local/bin/run-pwny.sh) auto-detects the ADS7846
touchscreen event node from /proc/bus/input/devices and writes a Kivy config
with calibrated coordinate ranges:
[input]
ads = hidinput,/dev/input/eventN,min_abs_x=199,max_abs_x=3780,min_abs_y=348,max_abs_y=3837,invert_x=1,invert_y=1,min_pressure=12Both X and Y axes are inverted to match the physical screen orientation on the Pi.
View service logs:
journalctl -u kivy-pwny.service -fManual launch for debugging (with console output):
cd /home/admin/pwny
bash run-kivy-manual.shThe wifi_mon module logs to stderr at DEBUG level with the [wifi_mon] prefix.
All iw/ip commands and their return codes are logged for troubleshooting
interface and channel issues.