ESP32 – WiFi sniffer

This experimental project shows how to build a simple and inexpensive WiFi packet analyzer (also known as a WiFi sniffer). The heart of this project is WiFi module of ESP32 which is able to work in a promiscusous mode. It means that this clever chip allows IEEE802.11 network packets capturing for further analyzing. Because WiFi module doesn’t implement automated channel switching, additional code has been added in the main loop that switch channels in 500ms intervals.  Presented sniffer requires a callback function that will process all received promiscusous packets. Example callback function displays few basic information like packet type (control packet, management packet, etc.), RSSI or MAC addresses. The full code using ESP-IDF is on GitHub, click here.

Arduino IDE version is here – https://github.com/ESP-EOS/ESP32-WiFi-Sniffer

UPDATE (2017-12-14) – sniffer code has been modified to support current version of ESP-IDF (Pre-release 3.0-rc1)

Promiscusous Packet Structure

Data Types

/** 
 * Ref: https://github.com/espressif/esp-idf/blob/master/components/esp32/include/esp_wifi_types.h 
 */
typedef struct {
    signed rssi:8;            /**< signal intensity of packet */
    unsigned rate:5;          /**< data rate */
    unsigned :1;              /**< reserve */
    unsigned sig_mode:2;      /**< 0:is not 11n packet; 1:is 11n packet */
    unsigned :16;             /**< reserve */
    unsigned mcs:7;           /**< if is 11n packet, shows the modulation(range from 0 to 76) */
    unsigned cwb:1;           /**< if is 11n packet, shows if is HT40 packet or not */
    unsigned :16;             /**< reserve */
    unsigned smoothing:1;     /**< reserve */
    unsigned not_sounding:1;  /**< reserve */
    unsigned :1;              /**< reserve */
    unsigned aggregation:1;   /**< Aggregation */
    unsigned stbc:2;          /**< STBC */
    unsigned fec_coding:1;    /**< if is 11n packet, shows if is LDPC packet or not */
    unsigned sgi:1;           /**< SGI */
    unsigned noise_floor:8;   /**< noise floor */
    unsigned ampdu_cnt:8;     /**< ampdu cnt */
    unsigned channel:4;       /**< which channel this packet in */
    unsigned :12;             /**< reserve */
    unsigned timestamp:32;    /**< timestamp */
    unsigned :32;             /**< reserve */
    unsigned :32;             /**< reserve */
    unsigned sig_len:12;      /**< It is really lenth of packet */
    unsigned :12;             /**< reserve */
    unsigned rx_state:8;      /**< rx state */
} wifi_pkt_rx_ctrl_t;

typedef struct {
    wifi_pkt_rx_ctrl_t rx_ctrl;
    uint8_t payload[0];       /**< ieee80211 packet buff, The length of payload is described by sig_len */
} wifi_promiscuous_pkt_t;


/** 
 * Ref: https://github.com/lpodkalicki/blog/blob/master/esp32/016_wifi_sniffer/main/main.c 
 */
typedef struct {
    unsigned frame_ctrl:16;
    unsigned duration_id:16;
    uint8_t addr1[6]; /* receiver address */
    uint8_t addr2[6]; /* sender address */
    uint8_t addr3[6]; /* filtering address */
    unsigned sequence_ctrl:16;
    uint8_t addr4[6]; /* optional */
} wifi_ieee80211_mac_hdr_t;

typedef struct {
    wifi_ieee80211_mac_hdr_t hdr;
    uint8_t payload[0]; /* network data ended with 4 bytes csum (CRC32) */
} wifi_ieee80211_packet_t;

Parts List

  • ESP-WROOM-32 module
  • USB<->TTL converter (3.3.V)
  • Efficient power supply (3.3V)
  • R1 – resistor 560Ω, see LED Resistor Calculator
  • LED1 – basic LED

Circuit Diagram

Firmware

This code is written in C and can be compiled using xtensa-esp32-elf-gcc.  Don’t know how to start  ? Please read about how to compile and upload program into ESP32.

/**
 * Copyright (c) 2017, Łukasz Marcin Podkalicki <lpodkalicki@gmail.com>
 * ESP32/016
 * WiFi Sniffer.
 */

#include "freertos/FreeRTOS.h"
#include "esp_wifi.h"
#include "esp_wifi_types.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_event_loop.h"
#include "nvs_flash.h"
#include "driver/gpio.h"

#define	LED_GPIO_PIN			GPIO_NUM_4
#define	WIFI_CHANNEL_MAX		(13)
#define	WIFI_CHANNEL_SWITCH_INTERVAL	(500)

static wifi_country_t wifi_country = {.cc="CN", .schan=1, .nchan=13, .policy=WIFI_COUNTRY_POLICY_AUTO};

typedef struct {
	unsigned frame_ctrl:16;
	unsigned duration_id:16;
	uint8_t addr1[6]; /* receiver address */
	uint8_t addr2[6]; /* sender address */
	uint8_t addr3[6]; /* filtering address */
	unsigned sequence_ctrl:16;
	uint8_t addr4[6]; /* optional */
} wifi_ieee80211_mac_hdr_t;

typedef struct {
	wifi_ieee80211_mac_hdr_t hdr;
	uint8_t payload[0]; /* network data ended with 4 bytes csum (CRC32) */
} wifi_ieee80211_packet_t;

static esp_err_t event_handler(void *ctx, system_event_t *event);
static void wifi_sniffer_init(void);
static void wifi_sniffer_set_channel(uint8_t channel);
static const char *wifi_sniffer_packet_type2str(wifi_promiscuous_pkt_type_t type);
static void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type);

void
app_main(void)
{
	uint8_t level = 0, channel = 1;

	/* setup */
	wifi_sniffer_init();
	gpio_set_direction(LED_GPIO_PIN, GPIO_MODE_OUTPUT);

	/* loop */
	while (true) {
		gpio_set_level(LED_GPIO_PIN, level ^= 1);
		vTaskDelay(WIFI_CHANNEL_SWITCH_INTERVAL / portTICK_PERIOD_MS);
		wifi_sniffer_set_channel(channel);
		channel = (channel % WIFI_CHANNEL_MAX) + 1;
    	}
}

esp_err_t
event_handler(void *ctx, system_event_t *event)
{
	
	return ESP_OK;
}

void
wifi_sniffer_init(void)
{

	nvs_flash_init();
    	tcpip_adapter_init();
    	ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) );
    	wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
	ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
	ESP_ERROR_CHECK( esp_wifi_set_country(&wifi_country) ); /* set country for channel range [1, 13] */
	ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
    	ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_NULL) );
    	ESP_ERROR_CHECK( esp_wifi_start() );
	esp_wifi_set_promiscuous(true);
	esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler);
}

void
wifi_sniffer_set_channel(uint8_t channel)
{
	
	esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
}

const char *
wifi_sniffer_packet_type2str(wifi_promiscuous_pkt_type_t type)
{
	switch(type) {
	case WIFI_PKT_MGMT: return "MGMT";
	case WIFI_PKT_DATA: return "DATA";
	default:	
	case WIFI_PKT_MISC: return "MISC";
	}
}

void
wifi_sniffer_packet_handler(void* buff, wifi_promiscuous_pkt_type_t type)
{

	if (type != WIFI_PKT_MGMT)
		return;

	const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff;
	const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload;
	const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr;

	printf("PACKET TYPE=%s, CHAN=%02d, RSSI=%02d,"
		" ADDR1=%02x:%02x:%02x:%02x:%02x:%02x,"
		" ADDR2=%02x:%02x:%02x:%02x:%02x:%02x,"
		" ADDR3=%02x:%02x:%02x:%02x:%02x:%02x\n",
		wifi_sniffer_packet_type2str(type),
		ppkt->rx_ctrl.channel,
		ppkt->rx_ctrl.rssi,
		/* ADDR1 */
		hdr->addr1[0],hdr->addr1[1],hdr->addr1[2],
		hdr->addr1[3],hdr->addr1[4],hdr->addr1[5],
		/* ADDR2 */
		hdr->addr2[0],hdr->addr2[1],hdr->addr2[2],
		hdr->addr2[3],hdr->addr2[4],hdr->addr2[5],
		/* ADDR3 */
		hdr->addr3[0],hdr->addr3[1],hdr->addr3[2],
		hdr->addr3[3],hdr->addr3[4],hdr->addr3[5]
	);
}

39 thoughts on “ESP32 – WiFi sniffer

  1. Anyone know how to also get the SSID from the payload? I can see it in loads of ESP8266 based codes but nothing for the ESP32. Thanks.

  2. Awesome thanks Łukasz. I have been trying to find a way to conserve power on an ESP that is being set up in a remote location. If the researchers turn up on site to download the logs the system can then spin up the WiFi for them otherwise the system returns to its sleep mode as per its normal cycle.
    It looks like this will be perfect if i set it up on the second core, while the data is being collected from the sensors and stored to the flash on the first core the second can passively scan if there is a client nearby and if there is fire up the WiFi/Web interface.

    There is not likely to be any other devices in close proximity given the site is way down in Antarctica on the side of Mt Erebus so I may not even need to compare known mac addresses.

    • Hi Andrew, I find it as a kind of wow-project 🙂 Will you share it? A photo at least?
      /L

  3. Hi Lukasz, I know this is a couple years after the post but was hoping you still check comments. I’m currently looking into parts that can be used as a wifi/bluetooth packet sniffer. I know someone who used an ESP8266 for sniffing WiFi packets, but he said it didn’t work with newer cellular devices because it only supports modulation schemes up to MCS7. I assume the ESP32 is compatible with modern devices, but would like your input. I thought about using a CC2640+CC3100, but a fully integrated BLE+WiFi SOC would be ideal, especially since there is no existing host driver for interfacing the CC2640+CC3100. Any ideas?

    • Hi Alec, I always recommend ESP32 for small packets sniffer but it also has some limitations like: sniffing one channel at a time or support only 2.4GHz band. I didn’t use CC2640+CC3100 chips but looks quite good. Probably on “clear” ARM it will be more challenging to create such packet sniffier than on ESP32. Anyway, if you’re planning to design something that will be a product / production ready then I would recommend to use dedicated/existing AP hardware (with for example OpenWRT) that supports 2.4/5 GHz and write some code (User space or Kernel space – using i.e. Netfilter hooks) to capture packets.
      /L

  4. Hi, can you please answer the following question:

    I have two cell phones with wifi enabled and they don’t connected to any network. I know their MAC addresses so I expect to see them in the output results, but I don’t see them. Instead I see a lot of other MAC addresses that belong to Tp-Link, D-link and some other network vendors. I guess that all these addresses belong to routers or some other access points. Why could this happen? Why I don’t see mu devices but I see routers? Thanks.

    • Hi Andrey, you can see AP MACs because they broadcast constantly WiFi beacon frames. Client devices which are not connected to any AP also can broadcast beacon requests while performing active scanning. It happens frequently (ones per X minutes or on demand) but it depends on client-side configuration. Try to dig some info about WiFi beacons.
      /L

  5. Hi Łukasz, I am playing with Your sniffer. It’s really great ! Please correct my humble thinking – could I possible use it to sniff and extract data packets of my PV (photovoltaic inverter)? It has a wifi card (client of known password, IP,# port# ) and sends PV data every 5 mins to the server-portal. I want to make a nice small-box sniffer based on ESP8266 or ESP32 with ability of:
    – sniff and extract data packet of PV
    – decode PV data packet (I have some idea how)
    – sent mqtt packets to my home mini BMS (I have some idea how)
    Please kindly give advice and directions for a beginner.
    Regards, Paweł

    • Hi Paweł, generally – it should work but note that you can capture/decode packets only if your PV inverter produces not encrypted data. It would be easier to start with some packet analyzer (i.e. WireShark). Try to capture some packets and analyze the content before you dive into ESP32. It will give you information about payload structure. This information will be useful to create proper packet filter and payload parser. Good luck!
      /LP

  6. Hi Lukasz,
    I have a few questions:

    I am trying to figure out what the “payload” returns? like how do you know what the types and values are for “wifi_ieee80211_mac_hdr_t”?

    What do you mean with?
    /* receiver address */
    /* sender address */
    /* filtering address */

  7. Hi, seems like all that issues with “undefinds” points to the same fact – switching to newest version of ESP-IDF. I’ll update the example code to support current version of ESP-IDF.

  8. Hi, I’m having a similar issue to the above – though I am using an ESP32thing breakout and arduino IDE with espressif (I believe this is slightly different from what you are doing). I get the errors:

    from C:\Program Files (x86)\Arduino\hardware\espressif\esp32/tools/sdk/include/esp32/esp_system.h:20:0,

    from C:\Program Files (x86)\Arduino\hardware\espressif\esp32/tools/sdk/include/freertos/freertos/portable.h:126,

    from C:\Program Files (x86)\Arduino\hardware\espressif\esp32/tools/sdk/include/freertos/freertos/FreeRTOS.h:105,

    from C:\Program Files (x86)\Arduino\hardware\espressif\esp32\cores\esp32/Arduino.h:32,

    from sketch\sketch_dec08a.ino.cpp:1:

    C:\Users\Ancient Abysswalker\Documents\Arduino\Projects\Project Arianna\ESP Sniff Testing\sketch_dec08a\sketch_dec08a.ino: In function ‘void wifi_sniffer_init()’:

    sketch_dec08a:76: error: ‘WIFI_COUNTRY_EU’ was not declared in this scope

    ESP_ERROR_CHECK( esp_wifi_set_country(WIFI_COUNTRY_EU) );

    ^

    C:\Program Files (x86)\Arduino\hardware\espressif\esp32/tools/sdk/include/esp32/esp_err.h:72:25: note: in definition of macro ‘ESP_ERROR_CHECK’

    esp_err_t rc = (x); \

    ^

    C:\Users\Ancient Abysswalker\Documents\Arduino\Projects\Project Arianna\ESP Sniff Testing\sketch_dec08a\sketch_dec08a.ino: In function ‘const char* wifi_sniffer_packet_type2str(wifi_promiscuous_pkt_type_t)’:

    sketch_dec08a:95: error: ‘WIFI_PKT_CTRL’ was not declared in this scope

    case WIFI_PKT_CTRL: return “CTRL”;

    ^

    exit status 1
    ‘WIFI_COUNTRY_EU’ was not declared in this scope

    …The format is slightly different, but still based off C, with a main loop and some initialization code. I should be able to port it, but I believe there is a missing library? Perhaps “freertos”?

    • I changed the code so that the line is now

      esp_err_t esp_wifi_set_country(const wifi_country_t *USA);

      That makes sense. case wifi_pkt_rx_ctrl: return “CTRL”; is still invalid though, and I can’t seem to determine what they’ve changed it to in the new documentation document…

      • Just realized how stupid that was to do that… It appears that wifi_country_t and the packet types are undefined and as such I cannot call things like WIFI_COUNTRY_US or WIFI_PKT_CTRL? Odd

  9. Hi Lukasz,
    I am running the same problem like Defozo, but you do not explain how to solve it? is there an update to the .h files?
    Thx!

  10. I’m standing in the rain. When compiling with the Arduino IDE, the same problems are reported. Also with the answer from Lukasz, I do not know what have to be changed. Maybe Lukasz or Defozo is able to tell me, what has to be done to get it work. Thanks a lot in advance.

  11. In file included from F:/esp32/esp-idf/components/esp32/include/esp_system.h:20:0,
    from F:/esp32/esp-idf/components/freertos/include/freertos/portable.h:126,
    from F:/esp32/esp-idf/components/freertos/include/freertos/FreeRTOS.h:105,
    from F:/esp32/blog-master/esp32/016_wifi_sniffer/main/main.c:7:
    F:/esp32/blog-master/esp32/016_wifi_sniffer/main/main.c: In function ‘wifi_sniffer_init’:
    F:/esp32/blog-master/esp32/016_wifi_sniffer/main/main.c:76:40: error: ‘WIFI_COUNTRY_EU’ undeclared (first use in this function)
    ESP_ERROR_CHECK( esp_wifi_set_country(WIFI_COUNTRY_EU) );
    ^
    F:/esp32/esp-idf/components/esp32/include/esp_err.h:72:25: note: in definition of macro ‘ESP_ERROR_CHECK’
    esp_err_t rc = (x); \
    ^
    F:/esp32/blog-master/esp32/016_wifi_sniffer/main/main.c:76:40: note: each undeclared identifier is reported only once for each function it appears in
    ESP_ERROR_CHECK( esp_wifi_set_country(WIFI_COUNTRY_EU) );
    ^
    F:/esp32/esp-idf/components/esp32/include/esp_err.h:72:25: note: in definition of macro ‘ESP_ERROR_CHECK’
    esp_err_t rc = (x); \
    ^
    F:/esp32/blog-master/esp32/016_wifi_sniffer/main/main.c: In function ‘wifi_sniffer_packet_type2str’:
    F:/esp32/blog-master/esp32/016_wifi_sniffer/main/main.c:95:7: error: ‘WIFI_PKT_CTRL’ undeclared (first use in this function)
    case WIFI_PKT_CTRL: return “CTRL”;
    ^
    make[1]: *** [/f/esp32/esp-idf/make/component_wrapper.mk:243: main.o] Error 1
    make: *** [F:/esp32/esp-idf/make/project.mk:435: component-main-build] Error 2

    How to fix this?

    • Hi, looks like the compiler says that you have a few undefined’s. I think you’re using newest version of ESP-IDF where arguments passed to these functions has been changed a little, i.e.:


      /* ref: https://github.com/espressif/esp-idf/blob/3a271a4ae7df8a9049fbbb801feafca5043c31eb/components/esp32/include/esp_wifi_types.h */
      typedef struct {
      char cc[3]; /* country code string */
      uint8_t schan; /* start channel */
      uint8_t nchan; /* total channel number */
      wifi_country_policy_t policy; /* country policy */
      } wifi_country_t;

      /* ref: https://github.com/espressif/esp-idf/blob/79f206be47c3f608615c1de8c491107e6c9194bb/components/esp32/include/esp_wifi.h */
      esp_err_t esp_wifi_set_country(const wifi_country_t *country);

  12. I’m going to look at this code this weekend.

    Łukasz, I know the Arduino IDE is a C++ compiler – do you know what I’d need to do to get started converting your program to Arduino IDE? I’m guessing I might need to add some .h files, and alter some function calls?

    I’d be happy if I can drop it straight in!

    • Sarah, I’m quite sure you can do that using Arduino IDE and some Arduino libraries able to manage WiFi settings. Arduino IDE uses low-level functions from ESP-IDF project which I’m using directly in my project. What you need is set WiFi mode to WIFI_MODE_NULL (no AP nor STATION), then turn WiFi into promiscusous mode (sniffing mode) and finally register custom handler able to process raw packets. Packet structure should be the same. Good luck!

  13. Hi Łukasz
    Thanks for this example ..is working like charme 🙂
    as i develop this stuff with eclipse/SLOEBER i needed some minor adaption concerning setup() and loop

    uint8_t level = 0, channel = 1;

    // the setup function runs once when you press reset or power the board
    void setup() {
    // initialize digital pin 13 as an output.
    Serial.begin(115200);
    delay(10);
    wifi_sniffer_init();
    gpio_set_direction(LED_GPIO_PIN, GPIO_MODE_OUTPUT);
    }
    // the loop function runs over and over again forever
    void loop() {
    Serial.print(“huhuhuhu”);
    delay(1000); // wait for a second
    gpio_set_level(LED_GPIO_PIN, level ^= 1);
    vTaskDelay(WIFI_CHANNEL_SWITCH_INTERVAL / portTICK_PERIOD_MS);
    wifi_sniffer_set_channel(channel);
    channel = (channel % WIFI_CHANNEL_MAX) + 1;
    }
    ..instead of your void Main loop
    and i needed the #include for this serial out..

    GREAT base now..:-)
    in my last ESP8266 project i combined the promiscuous mode with station-Mode. the module is sniffing the channels..stop..and transfer this data via REST-API to a webServer..( and MS SQL DB ).
    this all in a loop!
    In my 8266 scatch i additionally sniff the SSID..wondering if ESP32 also will be able to get the SSID!?

    now i will digg deeper into your code and bring it hopefully to fly like ESP8266 (promiscuous + Station ).
    Target is then to additionally SNIFF the BLE ( also in promiscuous??) ..this all together will ( hopefully ) give a quite “complete” signature of moving objects 😉

    greets
    kristina

    • Hi, thanks for sharing the code! I’m sure you can find all visible acesspoints by capturing their prob req/resp packets containing all that info, i.e. SSID/BSSID.
      Ps.: I love this line 🙂 Serial.print(“huhuhuhu”);
      Cheers! L

  14. It works like a charm, but a change needs to be done in order to compile correctly.

    The line
    case WIFI_PKT_CTRL: return “CTRL”;
    shoud be commented or removed, because the type WIFI_PKT_CTRL is not included in esp_wifi_types.h and this kind of packets don’t have any interesting meaning.

    Thanks Łukasz 🙂

    • You’re welcome! 🙂 Thanks for reply on that. Good hint. I need to verify it again as esp-idf is constantly maintained what is so amazing.

  15. I tried your code with latest esp-idf (mid August) but it captures only management packets.

    No way to sniff data packets at all!
    Do you know the reason?

    • It captures management packets due to if-condition inside the function. It imply other packets are just dropped. If you remove that condition then it should capture other packets as well.

  16. Have you actually tried to build this and test it? Doesn’t promiscuous mode require the device to be already connected to a network?

    • Why should promiscuous mode need to be connected to a network? Have you read what promicuous mode is and what is it for?

  17. does this capture the handshake packets as well? i was under the impression the 8266 dropped them and that was a conscious decision on the makers part. I am currently building a sophisticated pen testing rig (hid capture and clone at a distance, 4s lipo, raspi with soft power switch..etc)and would love to throw one of these on if it can d0 hand shakes

  18. Where do you set the ssid and the password of the network? I mean, how does the esp32 know, ‘where’ he has to capture the network traffic?

    • You don’t need to set SSID/password after its been turned into promiscuous mode. This mode allows you to capture all packets from the current WiFi channel of ESP32 module.

Leave a Reply to SarahC Cancel reply