Wifi sniffers, cheap and easy
Introduction
The physics behind wifi
Wifi sniffers have been around just as long as wifi itself. The concept is simple- Imagine you've got a lantern in the dark. It shines light in a 360 degree radius around it, and anyone within its vicinity will be able to see the light. Now, imagine that the light emitted by the lantern is invisible and can pass through solid objects: That's exactly what wifi signals are, although immensely oversimplified.
Indeed, wifi signals are a form of electromagnetic radiation (EMR), the same as light, X-Rays, and even heat lamps. Its relatively low frequency allows it to permeate solid objects, otherwise you wouldn't be able to connect to your wifi if there's a wall between you and the access point.
Concept
Because information is being exchanged via these wifi signals, this poses an interest for potential malicious actors or simply curious individuals. With commonly available hardware and minimal knowledge of the topic, it is possible to capture and record any and all wifi signals that are in the vicinity- And that's what we're going to explore today.
Preparation
Materials
To make our wifi sniffer, we only need a computer that can access wifi and has an adapter that is capable of entering promiscuous mode. In my last post, I introduced the "mini computer" SoC ESP32 by Espressif. These are extraordinarily cheap, small, and powerful. You can purchase one here for about $7 USD.
Software
In order to write the sniffer, we'll be using the esp-idf toolchain (refer to this post to see why). I use the VSCode IDE as my text editor, but what editor you use is personal preference and makes no difference.
Writing the code
The code for this project is very simple and just a PoC, so it is minimal and not that useful. In esp-idf projects, we begin with an entrypoint into our app_main()
function. Before we do anything, we need to do a few prerequisite setup steps in order to initialize the wifi and callback methods:
- Start the esp32 event loop
- Initialize NVS flash
- Initialize network interface
- Configure network interface
Steps 1 & 2
These steps are very simple and pretty standard for projects of this type- Here's the code, which I'll explain below. Remember, this is all contained in our entrypoint function.
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
Step 1 is fully contained in a single line of code on line 1- The rest is initializing the NVS flash. There is a conditional check here which in case of an error, erases flash memory and tries to initialize it again. The ESP_ERROR_CHECK()
function you see throughout this snippet is a builtin function that accepts a return code, and throws an error if the return code is anything except success.
Steps 3 & 4
Now that we are ready to use the esp32, we need to set up the network components. I extracted these steps into a separate function, which is just called from the main function in order to organize the code a bit better:
static void setup_wifi(void) {
// Initializ
ESP_LOGI(TAG, "Beginning wifi setup");
ESP_ERROR_CHECK(esp_netif_init());
// Interface defaults
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
// Default config
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
// Setup callback
wifi_promiscuous_cb_t callback = &wifi_callback;
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(callback));
// Set station mode and start
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
// Config filters and stuff
wifi_promiscuous_filter_t filter = {.filter_mask =
WIFI_PROMIS_FILTER_MASK_MGMT};
esp_wifi_set_promiscuous_filter(&filter);
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
ESP_LOGI(TAG, "Wifi started successfully");
wifi_mode_t ptr;
ESP_ERROR_CHECK(esp_wifi_get_mode(&ptr));
const char *c = wifi_mode_to_string(ptr);
ESP_LOGI(TAG, "Mode: %s", c);
}
In order to actually use these packets, we need to create a callback function, which gets called whenever a frame matching the filter is captured, which we do on line 15-16.
On line 23, I set a filter to only capture management frames, which is just certain kind of message- I do this for no reason other than to reduce the noise created by this program, otherwise there would be thousands of packets per second hitting the interface. This code is fairly self explanatory, but you might notice I call a function called wifi_mode_to_string
. This is because the wifi_mode_t
member is an enum, and the great C language is not capable of printing out the string representation of an enum, so we have to map it manually:
const char *wifi_mode_to_string(wifi_mode_t mode) {
switch (mode) {
case WIFI_MODE_NULL:
return "WIFI_MODE_NULL";
case WIFI_MODE_STA:
return "WIFI_MODE_STA";
case WIFI_MODE_AP:
return "WIFI_MODE_AP";
case WIFI_MODE_APSTA:
return "WIFI_MODE_APSTA";
case WIFI_MODE_NAN:
return "WIFI_MODE_NAN";
case WIFI_MODE_MAX:
return "WIFI_MODE_MAX";
default:
return "UNKNOWN_MODE";
}
}
Putting it all together
Now everything is setup, we just need to create the wifi callback function and log data that we obtain. In our callback function, since this is a PoC, we don't need to do anything other than print the frames-
void wifi_callback(void *buf, wifi_promiscuous_pkt_type_t type) {
const uint16_t data_length = 256;
ESP_LOGI(TAG, "Printing first %d bytes of received packet with type %s\n",
data_length, wifi_pkt_type_to_string(type));
unsigned char *char_buf = (unsigned char *)buf;
for (int i = 0; i < data_length; i++) {
printf("%02X ", char_buf[i]); // 1 byte = 0x00, two hex chars
if (i % 16 == 15) { // Organize it by groups of 16 bytes because im spoiled by wireshark
printf("\n");
}
}
}
Executing
Now we simply navigate to the project working directory and build the project, compile it, flash it to the ESP32, and finally establish a UART connection so we can see the output. We can do this with a simple command:
idfx build && idfx flash COM11 monitor
And here's what the output looks like-
Conclusion
As noted in the beginning, this is just a PoC, and is not useful really in any practical way. There are established and fantastic tools already built for this purpose, such as Wireshark and Air*-ng suite.
The purpose of this post is to provide a simplified explanation and overview of what wifi sniffing is, and how easy it can be accomplished. As such, I hope I was able to explain things well- I am not great at writing blog posts, but I am trying to improve, so please bear with me.