Skip to content

xdsai/pwny

Repository files navigation

pwny -- WiFi Pentesting Kiosk for Raspberry Pi

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.


Table of Contents

  1. Project Overview
  2. Architecture
  3. Features
  4. Hardware Requirements
  5. Software Dependencies
  6. Installation and Setup
  7. Technical Details

Project Overview

pwny provides a complete WiFi auditing workflow from a pocket-sized device:

  1. Scan nearby access points (2.4 GHz, 5 GHz, 6 GHz) using nmcli
  2. Classify each AP's security (Open, WPA2-PSK/vulnerable, WPA3/SAE, Enterprise)
  3. Attack a selected target: enter monitor mode on the USB adapter, capture packets with Scapy's AsyncSniffer, run continuous deauthentication via aireplay-ng, and detect EAPOL 4-way handshake frames in real time
  4. Brute force captured WPA handshakes using aircrack-ng with dictionary wordlists or exhaustive character-set generation via itertools.product
  5. Leech a cracked network: connect to the upstream AP as a station on the built-in wlan0, create a new hotspot on ap0 (USB adapter) with hostapd
    • dnsmasq, and bridge traffic with nftables NAT + policy routing
  6. 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.


Architecture

File-by-File Breakdown

/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)

Module Interaction Diagram

                        +----------+
                        |  app.py  |  (Kivy GUI, ScreenManager)
                        +----+-----+
                             |
              +--------------+--------------+
              |              |              |
      +-------+----+  +-----+------+  +----+-------+
      |actions_safe |  |  actions   |  |  wifi_mon  |
      |  .py        |  |    .py     |  |    .py     |
      +------+------+  +-----+-----+  +-----+------+
             |              |              |
      +------+------+      |         +----+-------+
      | wifi_safe.py |     |         |  leech.py  |
      +------+------+     |         +-----+------+
             |              |              |
             +---------+----+---------+----+
                       |              |
                 +-----+----+   +----+-----+
                 | state.py |   | utils.py |
                 +----------+   +----------+

app.py -- Kivy GUI

The main application entry point. Configures a 480x320 fullscreen kiosk window via Kivy's Config system with KMSDRM/SDL2 rendering. Contains:

  • KioskApp -- The Kivy App subclass; creates required directories on startup.
  • Router (ScreenManager) -- A stack-based screen navigator with push()/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 an APRow widget 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, calls ensure_mon() and tune_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 in pcap_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 combining ButtonBehavior + BoxLayout with 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().

actions.py -- Core Logic

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 to pcaps/<SSID>_<BSSID>_<timestamp>.pcap using Scapy's wrpcap(). Updates pcap_info.json with 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> with input='q\n' to read the network listing and parse (N handshake) counts.
  • Password lookup (lookup_password(bssid, ssid)):

    • Searches pcap_info.json for 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).
  • 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 \r and \n to catch aircrack-ng's carriage-return progress overwrites.
    • ANSI escape sequences are stripped via _ANSI_RE regex.
    • 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.
  • RNG brute force (run_bruteforce_rng()):

    • Generates passwords using itertools.product(charset, repeat=length) for each length from min_len to max_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)).
  • Character sets (CHARSETS):

    • lower: a-z (26 chars)
    • upper: A-Z (26 chars)
    • digits: 0-9 (10 chars)
    • special: !@#$%^&*()-_=+[]{}|;:,.<>?/ (28 chars)

actions_safe.py -- Safe Scanning Actions

Non-privileged operations that do not require monitor mode:

  • do_scan(callback) -- Runs scan_access_points() in a background thread. Results are stored in STATE.last_scan and passed to the callback.
  • set_selection(ap) / clear_selections() -- Manage STATE selection fields.
  • list_pcaps() -- Lists .pcap/.pcapng files in pcaps/, sorted by modification time (newest first).
  • read_pcap_meta(name) -- Reads metadata for a PCAP from pcap_info.json.
  • write_placeholder_pcap_and_meta(ap) -- Creates an empty PCAP placeholder with metadata (used for defensive/logging mode).

wifi_safe.py -- AP Scanning and Security Classification

Uses nmcli (NetworkManager CLI) for WiFi scanning without monitor mode:

  • scan_access_points(dedup='ssid_band') -- Runs nmcli dev wifi rescan then 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)
  • _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.

wifi_mon.py -- Monitor Mode and Deauthentication

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) via shutil.which() with fallback paths in /usr/sbin/, /usr/bin/, /sbin/, /bin/.

  • PHY auto-detection (_detect_usb_phy()):

    • Scans /sys/class/ieee80211/*/device symlinks 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_PHY environment variable.
  • Monitor interface management:

    • ensure_mon(default_ch, bw) -- Unblocks rfkill, sets regulatory domain, then calls retune_root().
    • retune_root(ch, bw) -- Destroys all interfaces on the PHY, creates a fresh mon1 monitor interface, sets the channel, and brings it up. Tries three strategies in order:
      1. Interface-level channel set while DOWN, then bring UP
      2. Interface-level channel set while UP (required by some Realtek/Mediatek drivers)
      3. 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 for DFS-CAC-COMPLETED or DFS-RADAR-DETECTED, then cleans up.
  • Deauthentication:

    • start_deauth(bssid, iface, client) -- Launches aireplay-ng -0 0 -a <bssid> as a persistent background process. The -0 0 flag 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_PACKET socket mode (no pcap).
    • Reloads interface list and sets the default interface to mon1.
  • Diagnostic helpers:

    • send_probe() -- Injects a deauth frame via Scapy sendp().
    • send_test_beacon() -- Injects beacon frames for testing.
    • get_mon_mac() -- Gets MAC from sysfs, Scapy raw, or generates a random LAA MAC.

leech.py -- Hotspot Bridging (Leech AP)

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()):

  1. Detect USB PHY and tear down any monitor mode interfaces.
  2. Connect wlan0 to upstream via nmcli dev wifi connect <ssid> password <pw>.
  3. Wait for DHCP IP (polls nmcli for up to 15 seconds).
  4. Create ap0 AP interface on the USB PHY (iw phy <phy> interface add ap0 type __ap).
  5. Configure ap0 with static IP 192.168.4.1/24.
  6. Policy routing: Create routing table 100 so traffic from 192.168.4.0/24 is routed through the upstream gateway on wlan0 (not eth0).
  7. Enable NAT via nftables: masquerade outgoing traffic on wlan0, accept all forwarded traffic.
  8. Start dnsmasq: DHCP server on ap0 (range 192.168.4.10 - 192.168.4.200), DNS forwarding to 8.8.8.8 / 8.8.4.4.
  9. Start hostapd: AP on ap0 with configured SSID/password, 802.11g mode, WPA2-PSK (CCMP) if password provided, open if not.

Shutdown sequence (leech_stop()):

  1. Terminate hostapd and dnsmasq processes.
  2. Remove nftables NAT table and policy routing rules.
  3. Disconnect wlan0 from upstream.
  4. Delete ap0 interface.

Status monitoring (leech_status()):

  • Reports upstream SSID/IP, leech SSID, uptime, and connected client count (via iw dev ap0 station dump).

state.py -- Shared Application State

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()

utils.py -- Utilities

  • system_metrics() -- Returns CPU%, RAM%, disk%, and SoC temperature via psutil. Temperature is read from psutil.sensors_temperatures() or the sysfs thermal zone.
  • ensure_dirs(base) -- Creates pcaps/ and dicts/ directories if they don't exist.
  • load_pcap_info(base) / save_pcap_info(info, base) -- JSON I/O for pcap_info.json.

Features

1. Access Points Scanning

  • Uses nmcli dev wifi rescan followed 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.

2. Attack Screen (Monitor Mode + Capture + Deauth)

On entering the Attack screen:

  1. ensure_mon() creates the mon1 monitor interface on the USB adapter's PHY.
  2. tune_channel() tunes to the target AP's channel (with DFS support).
  3. 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.

3. Brute Force

Two attack modes for cracking WPA handshakes:

Dictionary Attack

  • 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.

RNG (Character Set) Brute Force

  • 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 to aircrack-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.json for the corresponding PCAP entry.
  • Password is then available across the application (Attack screen, PCAPs screen, Leech AP screen) via lookup_password().

4. Captured PCAPs

  • Lists all .pcap and .pcapng files in pcaps/, 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).

5. Leech AP (Rogue Hotspot Bridge)

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 using nmcli.
  • Downstream hotspot: ap0 (USB WiFi adapter) runs hostapd broadcasting a configurable SSID and WPA2 password.
  • DHCP: dnsmasq serves IPs in 192.168.4.10-200 range with DNS forwarding to Google DNS (8.8.8.8, 8.8.4.4).
  • NAT: nftables masquerade rule on the upstream interface.
  • Policy routing: Table 100 ensures hotspot client traffic is routed through wlan0 (not eth0 or 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.

6. System Controls

  • Restart Service: systemctl restart kivy-pwny.service
  • Reboot: reboot
  • Shut Down: shutdown -h now

All executed via sudo -n (passwordless sudo).


Hardware Requirements

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)

Tested USB WiFi Chipsets

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.

Software Dependencies

Python Packages

Package Purpose
kivy GUI framework (touchscreen kiosk interface)
scapy Packet capture (AsyncSniffer), injection (sendp), PCAP I/O
psutil System metrics (CPU, RAM, disk, temperature)

System Tools

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)

Sudoers Configuration

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, dnsmasq
  • pwny -- Additional pwny-specific permissions
  • kivy_pwny -- Service-related permissions

The code uses sudo -n (non-interactive) for all privileged operations, which requires these sudoers entries to be configured.


Installation and Setup

1. System Dependencies

sudo apt-get update
sudo apt-get install -y \
  aircrack-ng \
  hostapd \
  dnsmasq \
  iw \
  nftables \
  network-manager \
  python3-pip \
  python3-venv

2. Python Dependencies

pip3 install kivy scapy psutil

3. Directory Structure

The application automatically creates required directories on startup, but you can create them manually:

mkdir -p /home/admin/pwny/pcaps
mkdir -p /home/admin/pwny/dicts

4. Wordlists

Place 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)

5. Sudoers Configuration

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.

6. Systemd Service

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.target

The launch script (/usr/local/bin/run-pwny.sh) handles:

  • Touchscreen calibration (ADS7846 coordinate inversion and range mapping)
  • Kivy config generation ($KIVY_HOME/config.ini with hidinput provider)
  • SDL/KMSDRM environment setup

Enable and start:

sudo systemctl enable kivy-pwny.service
sudo systemctl start kivy-pwny.service

7. Environment Variables

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

Technical Details

PHY Auto-Detection for USB Adapter

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):

  1. Scans all entries under /sys/class/ieee80211/.
  2. Resolves the device symlink to its real path.
  3. Checks if the real path contains /usb (indicating a USB-backed controller).
  4. 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 Channel Support

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:

  1. 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.

  2. 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, or DFS-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.
  3. Fallback to non-DFS: If CAC fails (radar detected or timeout), falls back to channel 36 (UNII-1, no DFS required).

EAPOL 4-Way Handshake Detection

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 Output Parsing

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 \r and \n (handles \r\n pairs 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 rate
  • 12345 / 67890 keys -- tested / total with both formats
  • KEY FOUND! [ password123 ] -- success detection

Thread-Safe GUI Updates

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_done callbacks 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

pcap_info.json Metadata Format

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).

Leech AP Networking

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: Static 192.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.

PCAP Filename Convention

<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.

Touchscreen Calibration

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=12

Both X and Y axes are inverted to match the physical screen orientation on the Pi.


Logs and Debugging

View service logs:

journalctl -u kivy-pwny.service -f

Manual launch for debugging (with console output):

cd /home/admin/pwny
bash run-kivy-manual.sh

The 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.

About

pwny is a touchscreen-driven WiFi penetration testing tool built with Python and Kivy

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors