DEV Community

Cover image for Title: Building an ESP32 Multi-Tool - Part 3: Packet Sniffing, Deauth Detection, and Web Portal
Donato Maglie
Donato Maglie

Posted on

Title: Building an ESP32 Multi-Tool - Part 3: Packet Sniffing, Deauth Detection, and Web Portal

In the previous articles of this series, I covered how I turned an ESP32-S3 into a BadUSB and built the FreeRTOS and LVGL architecture to run everything. Now it's time to actually use the radio. The ESP32 is famous for its Wi-Fi capabilities, and leaving it as just an offline hardware tool felt like a waste.

For network analysis, having a portable device that can monitor 802.11 traffic is a game changer. The ESP-IDF APIs are pretty flexible. Instead of just connecting to a router like a normal IoT device, we can set up a SoftAP or put the silicon into promiscuous mode to sniff raw frames right out of the air.

This post breaks down how I built the network modules for my multi-tool. I'll walk through the local Web Portal I use to upload payloads wirelessly to the LittleFS filesystem, the network scanner, and the packet sniffer that saves .pcap files directly to memory. I also added a "Deauth Radar" to catch Wi-Fi deauthentication attacks in real-time.

Handling all these network events without crashing the limited RAM of the microcontroller required some specific architectural choices, which I'll share below.

1. The Bridge: SoftAP and Web Portal

In part one, I showed the BadUSB executing DuckyScript payloads. The problem was that updating those scripts meant plugging the device into a PC over serial every single time. I needed a wireless way to transfer files.

I configured the ESP32 to run in WIFI_MODE_AP to broadcast its own network. I used the esp_http_server component from ESP-IDF to serve a retro-themed HTML interface. The server has a few endpoints, like /upload for normal files and /upload_payload which routes DuckyScript files straight into a specific LittleFS folder.

Handling uploads on an embedded device is tricky. If you try to buffer a 2MB file in RAM on an ESP32, you're going to get an Out-Of-Memory crash immediately.

To fix this, the upload handler works as a stream. First, it checks esp_littlefs_info to make sure there's actually enough flash storage left. If there is, it reads the HTTP request in small chunks using httpd_req_recv and flushes each chunk directly to the flash memory with fwrite.

Here is the streaming logic I wrote in tool_rete.c:

// Safety check: verify available storage on LittleFS
size_t total = 0, used = 0;
esp_littlefs_info("storage", &total, &used);
size_t free_space = total - used;

if (req->content_len > free_space) {
    httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Insufficient disk space!");
    return ESP_FAIL;
}

FILE *fd = fopen(filepath, "w");
char buffer[512];
int remaining = req->content_len;

// Stream processing: read from socket and write directly to flash
while (remaining > 0) {
    int to_read = (remaining < sizeof(buffer)) ? remaining : sizeof(buffer);
    int read_bytes = httpd_req_recv(req, buffer, to_read);

    if (read_bytes <= 0) {
        if (read_bytes == HTTPD_SOCK_ERR_TIMEOUT) continue; // Non-blocking retry
        fclose(fd);
        remove(filepath); // Clean up partial file on error
        return ESP_FAIL;
    }

    fwrite(buffer, 1, read_bytes, fd);
    remaining -= read_bytes;
}

fclose(fd);
httpd_resp_sendstr(req, "File successfully saved to LittleFS!\n");
return ESP_OK;
Enter fullscreen mode Exit fullscreen mode

This keeps the memory footprint completely flat during the transfer, so the rest of the OS keeps running smoothly.

2. Network Discovery: The Wi-Fi Scanner

Before playing around with network diagnostics, the tool needs to know what APs are around. I built a simple network discovery module that turns the ESP32 into a Wi-Fi scanner.

It uses esp_wifi_scan_start() and esp_wifi_scan_get_ap_records(). When you trigger the scan from the UI, it drops into Station mode (WIFI_MODE_STA) and scans the 2.4GHz channels.

One issue I ran into was that doing a blocking Wi-Fi scan on the main thread freezes the LVGL interface entirely. ESP-IDF supports async scans, but I found it cleaner to just run a synchronous scan inside a separate, low-priority FreeRTOS task. This way, the UI stays responsive, and when the scan is done, I lock the LVGL mutex and update the screen.

After the scan finishes, I grab the array of wifi_ap_record_t structures. I pull the BSSID, SSID, channel, and RSSI, and use that data to build a scrollable list of buttons on the screen.

// Configure the scan parameters
wifi_scan_config_t scan_config = {
    .ssid = NULL,
    .bssid = NULL,
    .channel = 0,
    .show_hidden = true
};

// Start a blocking scan in the background task
esp_wifi_scan_start(&scan_config, true);

// Retrieve the number of discovered Access Points
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count);

// Allocate memory and fetch the records
wifi_ap_record_t *ap_records = malloc(sizeof(wifi_ap_record_t) * ap_count);
esp_wifi_scan_get_ap_records(&ap_count, ap_records);

// Iterate through records to extract intelligence
for (int i = 0; i < ap_count; i++) {
    printf("SSID: %s, BSSID: %02x:%02x:%02x:%02x:%02x:%02x, CH: %d, RSSI: %d\n",
           ap_records[i].ssid,
           ap_records[i].bssid[0], ap_records[i].bssid[1], ap_records[i].bssid[2],
           ap_records[i].bssid[3], ap_records[i].bssid[4], ap_records[i].bssid[5],
           ap_records[i].primary,
           ap_records[i].rssi);

    // The extracted data is then used to populate the LVGL scrollable list
}

free(ap_records);
Enter fullscreen mode Exit fullscreen mode

This gives me the exact channels and MAC addresses I need to configure the sniffer and radar later.

3. Entering the Matrix: Promiscuous Mode and the PCAP Sniffer

Scanning is fine, but I wanted actual packet sniffing. Normally the Wi-Fi chip ignores frames that aren't meant for it. If you enable promiscuous mode, the hardware forwards every 802.11 frame it hears on the current channel to your code.

Setting it up is easy enough:

esp_wifi_set_promiscuous(true);
esp_wifi_set_promiscuous_rx_cb(&wifi_promiscuous_cb);
Enter fullscreen mode Exit fullscreen mode

The real problem is the I/O bottleneck. The wifi_promiscuous_cb callback is an interrupt that fires thousands of times a second on a busy network. Writing to LittleFS is slow and blocking. If you call fwrite inside that Wi-Fi callback, you will instantly trigger a Watchdog Timeout and crash the board.

I solved this by setting up a basic Producer-Consumer pipeline with a FreeRTOS Queue.

The Wi-Fi callback acts as the producer. It grabs the payload, adds a timestamp using esp_timer_get_time(), and pushes the struct into a queue using xQueueSendFromISR. If the queue is full because traffic is too heavy, it just drops the packet to keep the radio alive.

Then I have a separate, lower-priority task (sniffer_write_task) acting as the consumer. It waits on xQueueReceive. When a packet pops out, it handles the slow fwrite to the flash memory without blocking the Wi-Fi interrupts.

To make this data useful, I had to save it as a valid .pcap file so I could open it in Wireshark later. When the sniffer starts, it writes a standard PCAP global header with the magic number 0xa1b2c3d4. Then, for each packet, it writes a packet header containing the length and microsecond timestamp, followed by the raw 802.11 frame.

// The Consumer Task
static void sniffer_write_task(void *arg) {
    sniffer_packet_t pkt;
    while (1) {
        // Wait for a packet from the Wi-Fi callback queue
        if (xQueueReceive(sniffer_queue, &pkt, portMAX_DELAY) == pdTRUE) {
            if (pcap_file && sniffer_running) {
                pcap_packet_header_t hdr;
                hdr.ts_sec = pkt.ts_sec;
                hdr.ts_usec = pkt.ts_usec;
                hdr.incl_len = pkt.length;
                hdr.orig_len = pkt.length;

                // Write the PCAP packet header
                fwrite(&hdr, sizeof(pcap_packet_header_t), 1, pcap_file);
                // Write the raw 802.11 frame payload
                fwrite(pkt.payload, 1, pkt.length, pcap_file);
                fflush(pcap_file);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This decoupling lets the ESP32 survive traffic spikes and gives me clean files I can actually analyze on my PC.

4. The Deauth Radar: Real-Time Attack Detection

Saving packets for later is cool, but I also wanted a real-time warning if a deauthentication attack was happening around me.

For this "Deauth Radar", I kept the ESP32 in promiscuous mode but applied a hardware filter: WIFI_PROMIS_FILTER_MASK_MGMT. This tells the Wi-Fi silicon to ignore data frames and only pass management frames to my callback, which saves a massive amount of CPU cycles.

Inside the callback, I look at the first byte of the MAC header. If it's 0xC0 (Deauth) or 0xA0 (Disassoc), I pull the source, destination, and BSSID MAC addresses to figure out who is attacking who.

Since attacks can happen anywhere, I made a small FreeRTOS task that hops the Wi-Fi channel every 250 milliseconds to sweep the whole spectrum.

I also had to be careful with RAM. Tracking every deauth packet would eat the heap immediately. Instead, I keep a small array of structs in RAM tracking up to 10 APs and 20 targets each.

I also had to account for false positives, because regular deauth frames happen naturally when devices roam or disconnect. My logic only flags a device as "Under Attack" if it gets hit with a fast burst of more than 20 deauth frames.

// Inside the promiscuous callback
if (len >= 24) { 
    uint8_t fc = pkt->payload[0];
    // Check for Deauth (0xC0) or Disassoc (0xA0)
    if (fc == 0xC0 || fc == 0xA0) {

        uint8_t *addr1 = pkt->payload + 4;  // Destination
        uint8_t *addr2 = pkt->payload + 10; // Source
        uint8_t *addr3 = pkt->payload + 16; // BSSID

        // Determine the victim based on the packet direction
        uint8_t *victim = (memcmp(addr2, addr3, 6) == 0) ? addr1 : addr2;

        // Log the target in RAM to track the packet count
        radar_add_to_log(addr3, victim, current_radar_channel);
    }
}
Enter fullscreen mode Exit fullscreen mode

Once that threshold is hit, it sets a flag. The LVGL UI timer picks up the flag, throws a warning popup on the screen, and writes the attack details to a text file for later.

Wrapping up

By using the ESP-IDF Wi-Fi APIs and managing memory with FreeRTOS queues and streams, I managed to turn a standard ESP32 into a pretty decent network analysis tool. It now has a web portal for payloads, a Wi-Fi scanner, a PCAP sniffer, and a real-time deauth radar.

Next time, I'll switch gears from security to the UI and show how I built a Game Boy-style interface and put some playable retro games on this thing.

Top comments (0)