diff --git a/wifi_deauth.py b/wifi_deauth.py index a35d667..848764c 100644 --- a/wifi_deauth.py +++ b/wifi_deauth.py @@ -1,6 +1,19 @@ #!/usr/bin/env python3 -"""Wireless plumbing: interface discovery, monitor mode, card-state restore, -and tracked airodump-ng / aireplay-ng subprocesses.""" +""" +Guided WiFi deauth test tool — for a network you own or are authorized to test. + +Flow: pick card -> monitor mode (saves state) -> scan networks -> pick one + -> scan its clients -> pick one -> deauth (30s countdown | infinite) + -> "Stop & Reset card" restores everything. + +Requires: Linux, root, and the aircrack-ng suite + iw + iproute2: + sudo apt install aircrack-ng iw iproute2 +Run: sudo python3 wifi_deauth.py +Selftest: python3 wifi_deauth.py --selftest + +This is a front-end for airmon-ng / airodump-ng / aireplay-ng. wifite2 and +airgeddon do the same job if you just want the result. +""" import glob import os import re @@ -8,10 +21,19 @@ import shutil import subprocess import sys import tempfile +import threading import time +try: + import tkinter as tk + from tkinter import messagebox, ttk +except ModuleNotFoundError: # GUI deps absent; --selftest still runs + tk = messagebox = ttk = None + SCAN_NET_SECS = 10 SCAN_CLIENT_SECS = 15 +DEAUTH_TIMED_SECS = 30 +MAC_RE = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$" # --- child process registry (so "Stop & Reset" can kill mid-scan/mid-deauth) --- _procs = set() @@ -48,6 +70,7 @@ def run(cmd, timeout=30): return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) +# --------------------------- interface discovery ------------------------------ def list_wifi_interfaces(): """[{iface, phy, type, addr, monitor_capable}] from `iw dev`.""" try: @@ -95,6 +118,7 @@ def _phy_has_monitor(phy): return False +# ------------------------------ card state ---------------------------------- class CardState: def __init__(self, iface): self.orig_iface = iface["iface"] @@ -149,6 +173,7 @@ def reset_card(state): run(["systemctl", "start", "NetworkManager"]) +# ------------------------------ scanning ----------------------------------- def airodump_scan(mon_iface, secs, bssid=None, channel=None): tmpdir = tempfile.mkdtemp(prefix="wifiscan_") prefix = os.path.join(tmpdir, "dump") @@ -209,6 +234,7 @@ def parse_airodump_csv(path): return aps, stations +# ------------------------------ deauth ------------------------------------- class Deauth: def __init__(self): self.proc = None @@ -233,6 +259,325 @@ class Deauth: return self.proc is not None and self.proc.poll() is None +# ------------------------------- GUI -------------------------------------- +class App(tk.Tk if tk is not None else object): + def __init__(self): + super().__init__() + self.title("WiFi Deauth Test — own network") + self.geometry("660x480") + self.ctx = {} + self.state_obj = None + self.deauth = Deauth() + self.busy = False + self.countdown_after = None + + self.body = tk.Frame(self) + self.body.pack(fill="both", expand=True, padx=12, pady=12) + bar = tk.Frame(self) + bar.pack(fill="x", side="bottom") + self.status = tk.Label(bar, text="", anchor="w", fg="#555") + self.status.pack(side="left", padx=12, pady=6) + self.reset_btn = tk.Button(bar, text="Stop & Reset card", + command=self.on_reset, state="disabled") + self.reset_btn.pack(side="right", padx=12, pady=6) + + self.protocol("WM_DELETE_WINDOW", self.on_close) + self.screen_select_card() + + # -- infra -- + def clear(self): + for w in self.body.winfo_children(): + w.destroy() + + def set_status(self, txt): + self.status.config(text=txt) + + def run_bg(self, fn, on_done, status="Working…", force=False): + if self.busy and not force: + return + self.busy = True + self.set_status(status) + + def worker(): + try: + res, err = fn(), None + except Exception as e: # noqa: BLE001 - surfaced in a dialog + res, err = None, e + self.after(0, lambda: self._bg_done(on_done, res, err)) + + threading.Thread(target=worker, daemon=True).start() + + def _bg_done(self, on_done, res, err): + self.busy = False + self.set_status("") + if err: + messagebox.showerror("Error", str(err)) + return + try: + on_done(res) + except tk.TclError: + pass # screen changed while a scan was in flight + + def _table(self, cols, widths, height=9): + tv = ttk.Treeview(self.body, columns=cols, show="headings", height=height) + for c, w in zip(cols, widths): + tv.heading(c, text=c) + tv.column(c, width=w) + tv.pack(fill="both", expand=True, pady=8) + return tv + + # -- screen 1: pick card -- + def screen_select_card(self): + self.clear() + tk.Label(self.body, text="1. Select WiFi card", + font=("", 13, "bold")).pack(anchor="w") + tv = self._table(("iface", "type", "addr", "monitor"), (120, 90, 170, 110), 8) + self._card_rows = {} + for it in list_wifi_interfaces(): + iid = tv.insert("", "end", values=( + it["iface"], it["type"], it["addr"], + "yes" if it["monitor_capable"] else "no")) + self._card_rows[iid] = it + + btns = tk.Frame(self.body) + btns.pack(fill="x") + tk.Button(btns, text="Rescan", command=self.screen_select_card).pack(side="left") + nxt = tk.Button(btns, text="Next →", state="disabled", + command=lambda: self._card_next(tv)) + nxt.pack(side="right") + + def on_sel(_): + sel = tv.selection() + ok = bool(sel) and self._card_rows[sel[0]]["monitor_capable"] + nxt.config(state="normal" if ok else "disabled") + + tv.bind("<>", on_sel) + + def _card_next(self, tv): + it = self._card_rows[tv.selection()[0]] + if not messagebox.askyesno( + "Confirm", + f"Put {it['iface']} into monitor mode?\n\n" + "This disconnects you from WiFi and stops NetworkManager.\n" + "Use 'Stop & Reset card' to undo."): + return + self.ctx["card"] = it + self.state_obj = CardState(it) + self.reset_btn.config(state="normal") # enable now: check-kill already ran + self.run_bg(lambda: enable_monitor(self.state_obj), self._after_monitor, + status=f"Enabling monitor mode on {it['iface']}…") + + def _after_monitor(self, mon_iface): + self.ctx["mon"] = mon_iface + self.screen_scan_networks() + + # -- screen 3: scan networks -- + def screen_scan_networks(self): + self.clear() + tk.Label(self.body, text="3. Scan networks", + font=("", 13, "bold")).pack(anchor="w") + tk.Label(self.body, text=f"Monitor interface: {self.ctx['mon']}", + fg="#555").pack(anchor="w") + tv = self._table(("essid", "bssid", "ch", "pwr", "enc"), + (210, 150, 45, 50, 90)) + self._net_rows = {} + + btns = tk.Frame(self.body) + btns.pack(fill="x") + scan_btn = tk.Button(btns, text=f"Scan ({SCAN_NET_SECS}s)") + scan_btn.pack(side="left") + nxt = tk.Button(btns, text="Next →", state="disabled") + nxt.pack(side="right") + + def do_scan(): + self.run_bg(lambda: airodump_scan(self.ctx["mon"], SCAN_NET_SECS), + fill, status=f"Scanning networks ({SCAN_NET_SECS}s)…") + + def fill(result): + aps, _ = result + tv.delete(*tv.get_children()) + self._net_rows.clear() + for ap in aps: + if not re.match(MAC_RE, ap["bssid"] or ""): + continue + iid = tv.insert("", "end", values=( + ap["essid"], ap["bssid"], ap["channel"], ap["power"], ap["privacy"])) + self._net_rows[iid] = ap + if not self._net_rows: + messagebox.showinfo("Scan", "No networks found. Try again / move closer.") + + def go_next(): + self.ctx["net"] = self._net_rows[tv.selection()[0]] + self.screen_scan_clients() + + scan_btn.config(command=do_scan) + tv.bind("<>", + lambda _: nxt.config(state="normal" if tv.selection() else "disabled")) + nxt.config(command=go_next) + do_scan() + + # -- screen 4: scan clients -- + def screen_scan_clients(self): + self.clear() + net = self.ctx["net"] + tk.Label(self.body, text="4. Scan clients", + font=("", 13, "bold")).pack(anchor="w") + tk.Label(self.body, + text=f"{net['essid']} · {net['bssid']} · ch {net['channel']}", + fg="#555").pack(anchor="w") + tv = self._table(("station", "pwr", "packets"), (210, 60, 80)) + self._cli_rows = {} + + ch_raw = str(net["channel"]).strip() + ch = ch_raw if ch_raw.lstrip("-").isdigit() and int(ch_raw) > 0 else None + + btns = tk.Frame(self.body) + btns.pack(fill="x") + scan_btn = tk.Button(btns, text=f"Scan ({SCAN_CLIENT_SECS}s)") + scan_btn.pack(side="left") + tk.Button(btns, text="← Networks", + command=self.screen_scan_networks).pack(side="left", padx=6) + nxt = tk.Button(btns, text="Next →", state="disabled") + nxt.pack(side="right") + + def do_scan(): + self.run_bg( + lambda: airodump_scan(self.ctx["mon"], SCAN_CLIENT_SECS, + bssid=net["bssid"], channel=ch), + fill, status=f"Scanning clients ({SCAN_CLIENT_SECS}s)…") + + def fill(result): + _, stations = result + tv.delete(*tv.get_children()) + self._cli_rows.clear() + for st in stations: + if st["bssid"].lower() != net["bssid"].lower(): + continue + if not re.match(MAC_RE, st["station"] or ""): + continue + iid = tv.insert("", "end", values=( + st["station"], st["power"], st["packets"])) + self._cli_rows[iid] = st + if not self._cli_rows: + messagebox.showinfo("Scan", "No clients seen — they may be idle. Try again.") + + def go_next(): + self.ctx["client"] = self._cli_rows[tv.selection()[0]] + self.screen_deauth() + + scan_btn.config(command=do_scan) + tv.bind("<>", + lambda _: nxt.config(state="normal" if tv.selection() else "disabled")) + nxt.config(command=go_next) + do_scan() + + # -- screen 5: deauth -- + def screen_deauth(self): + self.clear() + net, client = self.ctx["net"], self.ctx["client"] + tk.Label(self.body, text="5. Deauth", font=("", 13, "bold")).pack(anchor="w") + tk.Label(self.body, text=f"Network : {net['essid']} ({net['bssid']})").pack( + anchor="w", pady=(8, 0)) + tk.Label(self.body, text=f"Client : {client['station']}").pack(anchor="w") + + self.mode_var = tk.StringVar(value="timed") + f = tk.Frame(self.body) + f.pack(anchor="w", pady=12) + tk.Radiobutton(f, text=f"{DEAUTH_TIMED_SECS} seconds (countdown)", + variable=self.mode_var, value="timed").pack(anchor="w") + tk.Radiobutton(f, text="Infinite (until you click Stop)", + variable=self.mode_var, value="inf").pack(anchor="w") + + self.deauth_status = tk.Label(self.body, text="Idle.", font=("", 12)) + self.deauth_status.pack(anchor="w", pady=8) + + bf = tk.Frame(self.body) + bf.pack(anchor="w", pady=8) + self.start_btn = tk.Button(bf, text="Start deauth", + command=self.start_deauth, width=14) + self.start_btn.pack(side="left") + self.stop_btn = tk.Button(bf, text="Stop deauth", command=self.stop_deauth, + width=14, state="disabled") + self.stop_btn.pack(side="left", padx=8) + tk.Button(bf, text="← Clients", + command=lambda: (self.stop_deauth(), self.screen_scan_clients()) + ).pack(side="left", padx=8) + + def start_deauth(self): + net, client = self.ctx["net"], self.ctx["client"] + self.deauth.start(self.ctx["mon"], net["bssid"], client["station"]) + self.start_btn.config(state="disabled") + self.stop_btn.config(state="normal") + if self.mode_var.get() == "timed": + self._remaining = DEAUTH_TIMED_SECS + self._tick() + else: + self.deauth_status.config(text="Deauthing… (infinite) — click Stop to end.") + + def _tick(self): + if not self.deauth.running(): + self.stop_deauth() + return + if self._remaining <= 0: + self.deauth_status.config(text=f"Done ({DEAUTH_TIMED_SECS}s).") + self.stop_deauth() + return + self.deauth_status.config(text=f"Deauthing… {self._remaining}s left") + self._remaining -= 1 + self.countdown_after = self.after(1000, self._tick) + + def stop_deauth(self): + if self.countdown_after: + self.after_cancel(self.countdown_after) + self.countdown_after = None + self.deauth.stop() + if hasattr(self, "start_btn") and self.start_btn.winfo_exists(): + self.start_btn.config(state="normal") + self.stop_btn.config(state="disabled") + if "Done" not in self.deauth_status.cget("text"): + self.deauth_status.config(text="Stopped.") + + # -- reset / quit -- + def on_reset(self): + self.stop_deauth() + kill_all_children() + self.reset_btn.config(state="disabled") + self.busy = False + self.run_bg(lambda: reset_card(self.state_obj), self._after_reset, + status="Resetting card…", force=True) + + def _after_reset(self, _): + self.state_obj = None + self.ctx.clear() + messagebox.showinfo("Reset", "Card restored. NetworkManager restarted.") + self.screen_select_card() + + def on_close(self): + if self.state_obj: + if not messagebox.askyesno("Quit", "Reset card and quit?"): + return + self.stop_deauth() + try: + reset_card(self.state_obj) + except Exception: + pass + self.destroy() + + +# ------------------------------ entrypoint -------------------------------- +def preflight(): + if tk is None: + return "Missing Tkinter. Install: sudo apt install python3-tk" + if hasattr(os, "geteuid") and os.geteuid() != 0: + return f"Run as root: sudo python3 {os.path.basename(__file__)}" + missing = [b for b in ("iw", "ip", "airmon-ng", "airodump-ng", "aireplay-ng") + if not shutil.which(b)] + if missing: + return ("Missing commands: " + ", ".join(missing) + + "\nInstall: sudo apt install aircrack-ng iw iproute2") + return None + + def _selftest(): sample = ( "BSSID, First time seen, Last time seen, channel, Speed, Privacy, Cipher, " @@ -266,3 +611,8 @@ if __name__ == "__main__": if "--selftest" in sys.argv: _selftest() sys.exit(0) + err = preflight() + if err: + print(err, file=sys.stderr) + sys.exit(1) + App().mainloop()