diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 3deb901..0aad4a7 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -30,13 +30,14 @@ 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' @@ -44,7 +45,7 @@ jobs: 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') }} diff --git a/README.md b/README.md index efde051..e31aab3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/EspNowCapture/platformio.ini b/examples/EspNowCapture/platformio.ini new file mode 100644 index 0000000..628399e --- /dev/null +++ b/examples/EspNowCapture/platformio.ini @@ -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 diff --git a/examples/EspNowCapture/src/main.cpp b/examples/EspNowCapture/src/main.cpp new file mode 100644 index 0000000..5dedee7 --- /dev/null +++ b/examples/EspNowCapture/src/main.cpp @@ -0,0 +1,48 @@ +#include +#include + +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(); +} diff --git a/src/Politician.cpp b/src/Politician.cpp index 3e7d23a..6b22c83 100644 --- a/src/Politician.cpp +++ b/src/Politician.cpp @@ -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(); + 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. diff --git a/src/Politician.h b/src/Politician.h index 0ab466a..771682e 100644 --- a/src/Politician.h +++ b/src/Politician.h @@ -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 @@ -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. @@ -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; diff --git a/src/PoliticianTypes.h b/src/PoliticianTypes.h index afb373b..9415769 100644 --- a/src/PoliticianTypes.h +++ b/src/PoliticianTypes.h @@ -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; +#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) diff --git a/src/politician_compat.h b/src/politician_compat.h index b366e67..922bc9e 100644 --- a/src/politician_compat.h +++ b/src/politician_compat.h @@ -2,6 +2,7 @@ #ifdef ARDUINO #include + #include #else #include #include