Writing a simple ESP8266-based sniffer
Ángel Martin - Thu, 26 Jul 2018
sniffer-screenshot.png

In this series of blogposts we will cover advanced, security focused, aspects of the ESP8266 /ESP32 SoCs such as sniffing and injecting 802.11 and bluetooth packets, building proof-of-concept network implant devices, etc.

The ESP8266 is a low-cost Wi-Fi capable system-on-chip with full TCP/IP stack produced by Espressif Systems. It features a Tensilica L106 32-bit RISC processor, reaching a maximum clock speed of 160 MHz.

It integrates several peripheral interfaces via its 17 GPIO (General Purpose I/O) ports, which can be assigned to various functions, such as:

  • Hardware and Software SPI interface
  • I2C Interface
  • I2S Interface
  • Universal Asynchronous Receiver Transmitter (UART)
  • Pulse-Width Modulation (PWM)
  • IR Remote Control
  • 10 bit resolution ADC (Analog-to-Digital Converter)

Sniffer interface

The ESP8266 SDK API features a promiscuous mode which can be used to capture IEEE 802.11 packets in the air, with some limitations though. It will only decode 802.11b/g/n HT20 packets (20Mhz channel bandwidth), not supporting HT40 packets or LDPC. For those, it will only return their length and other (scarce) low-level information, but no additional decoding will be performed.

Data structures

Several data structures are used (but not exposed, i.e. they need to be explicitly declared in the user program) by the SDK to represent these two kinds of packets:

sniffer_data_structures.png

Sniffer-related API functions

The ESP8266 SDK provides the following sniffing-related functions, which can be found at /include/user_interface.h:

void wifi_promiscuous_enable(uint8 promiscuous)

Enables the promiscuous mode; to do so the chip must be both in Station mode first and disconnected from any AP.

The uint8 promiscuous parameter enables (1) and disables(0) this mode.

void wifi_set_promiscuous_rx_cb(wifi_promiscuous_cb_t cb)

Registers the callback function which will be called when a data packet is received.

The callback function will get two parameters: a pointer to the buffer memory containing the received packet and its length. The latter determines the type of the received packet:

  • Management packets. Length will be sizeof(wifi_pkt_mgmt_t). The buffer will hold a wifi_pkt_mgmt_t structure, containing:
    • wifi_pkt_rx_ctrl_t structure
    • A buffer containing the 802.11 packet
    • cnt will be 1
    • len will be the length of the buffer
  • Data packets. The buffer will hold a wifi_pkt_data_t structure, containing:
    • wifi_pkt_rx_ctrl_t structure
    • buf contains the 802.11 header
    • cnt how many packets are in buf
    • lenseq contains one or more struct LenSeq, providing the following data:
      • total packet length
      • both source and destination MAC addresses
  • Unsupported packets. Length will be sizeof(wifi_pkt_rx_ctrl_t). Either the received packet is not supported or it was badly formed/received.

void wifi_promiscuous_set_mac(const uint8_t *address)

Sets a destination MAC address filter for the sniffer, which will filter out every packet except those addressed to the specified MAC or to the broadcast (FF:FF:FF:FF:FF:FF).

Sample:

uint8_t mac_address[6] = {0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe};
wifi_promiscuous_set_mac(mac_address);

uint8 wifi_get_channel(void)

Returns the current Wi-Fi channel.

Writing a simple packet sniffer

R1245505-02.jpg

Environment setup

Full code is available on GitHub as a PlatformIO project.

It was tested on a Adafruit HUZZAH feather board, with the Arduino framework, using ESP8266 SDK version 1.3.0.

IEEE802.11

A standard 802.11 frame contains a layer 2 MAC header, followed by a variable length frame body and a 32 bit checksum (FCS):

ieee80211_frame.png

There are several different types of packets:

  • Management
  • Control
  • Data
  • Misc

Our simple sniffer will parse and print out the information contained in the frame header; additionally it will extract the SSID from beacon frames:

beacon_frame.png

Program flow

By calling wifi_set_promiscuous_rx_cb() we can specify the callback function that will be called when the network interface receives a new packet.

The overall program flow will be:

  1. Initialisation
    • Initialise serial interface
    • Enable promiscuous mode
    • Set sniffer callback
    • Set wifi channel (or implement channel hopping via hardware timers)
    • Print output table header
  2. Main loop
    • Does nothing: just waits for the callback to be triggered
  3. Callback function
    • Initialise pointers to data structures within the raw packet
    • Extract and parse the information contained in the different fields
    • Format and output

Due to the blocking nature of the callback function, it is not a good idea to perform too much process in it, because we might lose packets in the meantime. However, this simple example is focused on demonstrating the SDK calls usage rather than in efficiency.

The initialisation phase will be implemented on the setup() function:

 void setup()
 {
   //Serial setup
   Serial.begin(115200);

   //Set station mode, callback, then cycle promisc. mode
   wifi_set_opmode(STATION_MODE);
   wifi_promiscuous_enable(0);
   WiFi.disconnect();

   wifi_set_promiscuous_rx_cb(wifi_sniffer_packet_handler);
   wifi_promiscuous_enable(1);
   wifi_set_channel(9);

   //Print header
   Serial.printf(...)
 }

Our callback function will then parse each raw packet (buff) as follows:

void wifi_sniffer_packet_handler(uint8_t *buff, uint16_t len)
{
   // Type cast the received buffer into our generic SDK structure
   const wifi_promiscuous_pkt_t *pt = 
      (wifi_promiscuous_pkt_t *)buff;

   // Pointer to where the actual 802.11 packet is within the structure
   const wifi_ieee80211_packet_t *pk = 
      (wifi_ieee80211_packet_t*)pt->payload;

   // Define pointers to the 802.11 packet header and payload
   const wifi_ieee80211_mac_hdr_t *hdr = &pk->hdr;
   const uint8_t *data = pk->payload;

   // Pointer to the frame control section within the packet header
   const wifi_header_frame_control_t *FC = 
      (wifi_header_frame_control_t *)&hdr->frame_ctrl;

   (...)

   // Output info to serial
   Serial.printf(...)
}

Data structures

In order to parse the packets the following data structures were used:

MAC header

  typedef struct {
     wifi_header_frame_control_t frame_ctrl;
     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;

MAC header frame control

  typedef struct {
     unsigned protocol:2;
     unsigned type:2;
     unsigned subtype:4;
     unsigned to_ds:1;
     unsigned from_ds:1;
     unsigned more_frag:1;
     unsigned retry:1;
     unsigned pwr_mgmt:1;
     unsigned more_data:1;
     unsigned wep:1;
     unsigned strict:1;
  } wifi_header_frame_control_t;

Beacon frame

  typedef struct{
     unsigned interval:16;
     unsigned capability:16;
     unsigned tag_number:8;
     unsigned tag_length:8;
     char ssid[0];
     uint8 rates[1];
  } wifi_mgmt_beacon_t;

Packet types and subtypes

  typedef enum{
     WIFI_PKT_MGMT,
     WIFI_PKT_CTRL,
     WIFI_PKT_DATA,
     WIFI_PKT_MISC,
  } wifi_promiscuous_pkt_type_t;

  typedef enum {
     ASSOCIATION_REQ,
     ASSOCIATION_RES,
     REASSOCIATION_REQ,
     REASSOCIATION_RES,
     PROBE_REQ,
     PROBE_RES,
     NU1, /* ......................*/
     NU2, /* 0110, 0111 not used */
     BEACON,
     ATIM,
     DISASSOCIATION,
     AUTHENTICATION,
     DEAUTHENTICATION,
     ACTION,
     ACTION_NACK,
  } wifi_mgmt_subtypes_t;

References/Bibliography/Useful links