Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/build-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,22 @@ jobs:
- MsChapCapture
- WpsCapture
- KarmaResponder
- EspNowCapture

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.x'

- name: Install PlatformIO
run: pip install platformio intelhex

- name: Cache PlatformIO packages
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: pio-${{ runner.os }}-${{ hashFiles('**/platformio.ini') }}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,7 @@ beaconFlood(ssids, ssidCount, 6, 5000);
| `MsChapCapture` | Bare EAP-MSCHAPv2 credential capture (hashcat -m 5500) |
| `KarmaResponder` | KARMA rogue AP responder with runtime Serial toggle |
| `PassiveSensing` | RSSI-based human presence and motion detection |
| `EspNowCapture` | ESP-NOW action frame capture and payload parsing |

## Legal & Ethical Use

Expand Down
12 changes: 12 additions & 0 deletions examples/EspNowCapture/platformio.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[platformio]
default_envs = esp32

[env:esp32]
platform = espressif32 @ ~6.13.0
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
symlink://../..
build_flags =
-D CORE_DEBUG_LEVEL=5
48 changes: 48 additions & 0 deletions examples/EspNowCapture/src/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <Arduino.h>
#include <Politician.h>

using namespace politician;

Politician engine;

void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n\n--- Politician ESP-NOW Capture Example ---");

Config cfg;
// We don't necessarily need to capture beacons for ESP-NOW, but it helps populate the AP cache
cfg.capture_filter = LOG_FILTER_BEACONS | LOG_FILTER_PROBES;

// Initialize the engine
if (engine.begin(cfg) != politician::OK) {
Serial.println("Failed to start Politician engine");
while (1) delay(100);
}

// Set the callback to process ESP-NOW payloads
engine.setEspNowCallback([](const EspNowRecord &rec) {
Serial.printf("[ESP-NOW] ch=%d rssi=%d src=%02x:%02x:%02x:%02x:%02x:%02x dst=%02x:%02x:%02x:%02x:%02x:%02x len=%d payload=[",
rec.channel, rec.rssi,
rec.src[0], rec.src[1], rec.src[2], rec.src[3], rec.src[4], rec.src[5],
rec.dst[0], rec.dst[1], rec.dst[2], rec.dst[3], rec.dst[4], rec.dst[5],
rec.length);

// Print the first few bytes of the payload in hex
int printLen = rec.length > 16 ? 16 : rec.length;
for (int i = 0; i < printLen; i++) {
Serial.printf("%02x ", rec.payload[i]);
}
if (rec.length > 16) Serial.print("...");
Serial.println("]");
});

// Start hopping to discover ESP-NOW devices on any channel,
// or use engine.lockChannel(1) if you know their specific channel.
engine.startHopping();
Serial.println("Hopping started. Waiting for ESP-NOW traffic...");
}

void loop() {
engine.tick();
}
29 changes: 29 additions & 0 deletions src/Politician.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,35 @@ void Politician::_handleMgmt(const ieee80211_hdr_t *hdr, const uint8_t *payload,
return;
}

#ifndef POLITICIAN_NO_ESPNOW
if (subtype == MGMT_SUB_ACTION) {
if (_espNowCb != nullptr && len >= 15) {
if (payload[0] == 0x7F &&
payload[1] == 0x18 && payload[2] == 0xFE && payload[3] == 0x34 &&
payload[8] == 0xDD &&
payload[10] == 0x18 && payload[11] == 0xFE && payload[12] == 0x34 &&
payload[13] == 0x04 && payload[14] == 0x01) {
const uint16_t bodyOffset = 15;
const uint16_t elementBodyLength = payload[9] >= 5 ? payload[9] - 5 : 0;
const uint16_t availableBodyLength = len - bodyOffset;
const uint16_t bodyLength = elementBodyLength < availableBodyLength
? elementBodyLength
: availableBodyLength;
EspNowRecord rec;
memcpy(rec.src, hdr->addr2, 6);
memcpy(rec.dst, hdr->addr1, 6);
rec.channel = _rxChannel;
rec.rssi = rssi;
rec.ts_usec = (uint32_t)esp_timer_get_time();
Comment thread
0ldev marked this conversation as resolved.
Resolved
rec.payload = payload + bodyOffset;
rec.length = bodyLength;
_espNowCb(rec);
}
}
return;
}
#endif

// Parse both Beacons and Probe Responses.
// Sniffing Probe Responses automatically enables Active Decloaking
// of Hidden Networks when clients reconnect following a CSA/Deauth attack.
Expand Down
14 changes: 14 additions & 0 deletions src/Politician.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ typedef struct {
#define MGMT_SUB_AUTH 0xB0
#define MGMT_SUB_DISASSOC 0xA0
#define MGMT_SUB_DEAUTH 0xC0
#define MGMT_SUB_ACTION 0xD0

// ─── EAPOL ────────────────────────────────────────────────────────────────────
#define EAPOL_LLC_OFFSET 0
Expand Down Expand Up @@ -460,9 +461,19 @@ class Politician {
* The M1 message is the first EAP-WSC frame sent by the Enrollee and is unencrypted,
* revealing device attributes (name, manufacturer, model, auth/config capabilities).
* Only fired if the callback is set — has zero overhead otherwise.
* @param cb Callback function
*/
void setWpsCallback(WpsCb cb) { _wpsCb = cb; }

#ifndef POLITICIAN_NO_ESPNOW
/**
* @brief Set callback for captured ESP-NOW action frames.
* Only fired if the callback is set — has zero overhead otherwise.
* @param cb Callback function
*/
void setEspNowCallback(EspNowCb cb) { _espNowCb = cb; }
#endif

#ifndef POLITICIAN_NO_MSCHAPV2
/**
* @brief Sets the callback fired on a bare EAP-MSCHAPv2 challenge/response exchange.
Expand Down Expand Up @@ -627,6 +638,9 @@ class Politician {
RogueApCb _rogueApCb = nullptr;
_FpHookCb _fpHook = nullptr;
WpsCb _wpsCb = nullptr;
#ifndef POLITICIAN_NO_ESPNOW
EspNowCb _espNowCb = nullptr;
#endif
#ifndef POLITICIAN_NO_KARMA
KarmaCb _karmaCb = nullptr;
bool _karmaEnabled = false;
Expand Down
22 changes: 22 additions & 0 deletions src/PoliticianTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,28 @@ typedef void (*KarmaCb)(const KarmaRecord &rec);
#endif
#endif // POLITICIAN_NO_KARMA

// ─── ESP-NOW Record ───────────────────────────────────────────────────────────
#ifndef POLITICIAN_NO_ESPNOW
/**
* @brief Delivered to the EspNowCb callback when an ESP-NOW vendor-specific action frame is captured.
*/
struct EspNowRecord {
uint8_t src[6]; ///< Source MAC address (transmitter)
uint8_t dst[6]; ///< Destination MAC address (receiver or broadcast)
uint8_t channel; ///< Channel on which the frame was captured
int8_t rssi; ///< Signal strength of the frame (dBm)
uint32_t ts_usec; ///< RX timestamp in microseconds
const uint8_t* payload; ///< Pointer to internal ring-buffer memory; only valid for callback duration. Do not free, retain, or queue past callback return; memcpy if needed.
uint16_t length; ///< Length of the ESP-NOW payload
};

#ifndef POLITICIAN_NO_STD_FUNCTION
using EspNowCb = std::function<void(const EspNowRecord &rec)>;
#else
typedef void (*EspNowCb)(const EspNowRecord &rec);
#endif
#endif // POLITICIAN_NO_ESPNOW

// ─── 802.1X Enterprise Identity Record ─────────────────────────────────────────
// EAP method constants (RFC 3748 / RFC 5281)
static const uint8_t EAP_METHOD_IDENTITY = 0x01; ///< EAP Identity (always 0x01 for harvested records)
Expand Down
1 change: 1 addition & 0 deletions src/politician_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#ifdef ARDUINO
#include <Arduino.h>
#include <esp_timer.h>
#else
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
Expand Down
Loading