ESP32でWi-FiアクセスポイントをSnifferしてみる

概要

やってみようと思ったままWi-Fiまわりをあまり触っていなかったのでSnifferしてみました。今回はまわりにあるSSIDの収集と電波強度の取得をしました。

Wi-Fi Snifferとは?

Wi-Fiは基本的には暗号化された通信がメインなのですが、暗号化されていない通信もあります。ESP32ではWi-Fiを最低限のハードウエア処理のあとに、ソフトウエアで処理をしています。そのため速度的には微妙なんですが、低レベルのパケットを直接見ることが可能です。

最近のデバイスは処理に必要ないパケットはハードウエアレベルで処理をされて捨てられるので、あまりSnifferと呼ばれる飛んでいる電波を全部処理する動作をすることはできません。

IEEE802.11とは?

無線LANの一番基礎的な規格になります。現在は「IEEE802.11b/g/n」なとど複数の規格に対応しているものが多いと思います。一番基礎的で低速なIEEE802.11ではWi-Fiアクセスポイントへの接続まわりを担当しています。

まずはIEEE802.11でWi-Fiアクセスポイントを探して接続後に、より高速な規格で実際の通信をするのかなと思っています。

参考にしたスケッチ

上記のスケッチを参考にしてみました。非常にシンプルで短いスケッチですね。中身をみてみるとesp_wifi_set_promiscuous(true)で無差別モードにはいって、すべてのパケットをコールバック関数で受信しています。

実際に動かしてみるとわかるのですが、非常に大量の情報がでてきてたぶんすべての状態を受信しきれていないと思います。特に出力がSerialなので遅いんですよね。通信速度を115200から早い速度に変更してみたらArduino IDEが固まってしまいました。

さて、もともとはESP-IDF用のコードを移植したものなので不要なincludeが残っています。そこでちょっと手を入れてみたいとおもいます。

修正版スケッチ

#include "esp_wifi.h"
#include "esp_event_loop.h"
#include "nvs_flash.h"

#define WIFI_CHANNEL_SWITCH_INTERVAL  (500)
#define WIFI_CHANNEL_MAX               (14)

#define WLAN_FC_GET_STYPE(fc)  (((fc) & 0x00f0) >> 4)

uint8_t level = 0, channel = 1;

static wifi_country_t wifi_country = {.cc = "JP", .schan = 1, .nchan = 14}; //Most recent esp32 library struct

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);

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() );

  wifi_promiscuous_filter_t filter = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT};
  ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter));
  ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
  ESP_ERROR_CHECK(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) {
  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;

  // not beacon
  if (WLAN_FC_GET_STYPE(hdr->frame_ctrl) != 0x08) {
    return;
  }

  char ssid[33];
  memset(ssid, 0, sizeof(ssid));
  memcpy(ssid, &ipkt->payload[6], ipkt->payload[5]);

  printf("PACKET FC=%04x, TYPE=%s, STYPE=%02x, 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,"
         " ADDR4=%02x:%02x:%02x:%02x:%02x:%02x,"
         " SSID=%s\n",
         hdr->frame_ctrl,
         wifi_sniffer_packet_type2str(type),
         WLAN_FC_GET_STYPE(hdr->frame_ctrl),
         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],
         /* ADDR4 */
         hdr->addr4[0], hdr->addr4[1], hdr->addr4[2],
         hdr->addr4[3], hdr->addr4[4], hdr->addr4[5],
         ssid
        );
}

// the setup function runs once when you press reset or power the board
void setup() {
  Serial.begin(115200);
  delay(10);
  wifi_sniffer_init();
}

// the loop function runs over and over again forever
void loop() {
  delay(WIFI_CHANNEL_SWITCH_INTERVAL);
  wifi_sniffer_set_channel(channel);
  channel = (channel % WIFI_CHANNEL_MAX) + 1;
}

いらない行を削ったのですが、追加した処理もあるので行数はあまり変わりませんでした。こちらをベースに動作を確認してみたいと思います。

Setup

void setup() {
  Serial.begin(115200);
  delay(10);
  wifi_sniffer_init();
}

シリアルを初期化してから、Wi-Fiまわりの初期化をしています。

Wi-Fi初期化

  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() );

  wifi_promiscuous_filter_t filter = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT};
  ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter));
  ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
  ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler));

ほぼESP-IDFの関数ですね。Arduino環境だともう少し省略できるかもしれませんが、ほぼそのままです。wifi_countryとかは中国になっていたので、日本に変更しています。

追加したところは無差別モードのフィルタになります。esp_wifi_set_promiscuous_filter()で受信するパケットを制限しています。

typedef enum {
    WIFI_PKT_MGMT,  /**< Management frame, indicates 'buf' argument is wifi_promiscuous_pkt_t */
    WIFI_PKT_CTRL,  /**< Control frame, indicates 'buf' argument is wifi_promiscuous_pkt_t */
    WIFI_PKT_DATA,  /**< Data frame, indiciates 'buf' argument is wifi_promiscuous_pkt_t */
    WIFI_PKT_MISC,  /**< Other type, such as MIMO etc. 'buf' argument is wifi_promiscuous_pkt_t but the payload is zero length. */
} wifi_promiscuous_pkt_type_t;

上記の4種類あります。マネージメント、コントロール、データ、その他です。今回はSSIDを列挙するのでマネージメントのみに絞り込んでいます。

ここのパケットタイプはFCとも呼ばれているようです。実際にはこの4種類のTYPEと、さらに細かい区分をしたSTYPEの組み合わせでパケットの種別が決まっています。

TYPE値TYPESTYPE値STYPE
0WLAN_FC_TYPE_MGMT0WLAN_FC_STYPE_ASSOC_REQ
0WLAN_FC_TYPE_MGMT1WLAN_FC_STYPE_ASSOC_RESP
0WLAN_FC_TYPE_MGMT2WLAN_FC_STYPE_REASSOC_REQ
0WLAN_FC_TYPE_MGMT3WLAN_FC_STYPE_REASSOC_RESP
0WLAN_FC_TYPE_MGMT4WLAN_FC_STYPE_PROBE_REQ
0WLAN_FC_TYPE_MGMT5WLAN_FC_STYPE_PROBE_RESP
0WLAN_FC_TYPE_MGMT8WLAN_FC_STYPE_BEACON
0WLAN_FC_TYPE_MGMT9WLAN_FC_STYPE_ATIM
0WLAN_FC_TYPE_MGMT10WLAN_FC_STYPE_DISASSOC
0WLAN_FC_TYPE_MGMT11WLAN_FC_STYPE_AUTH
0WLAN_FC_TYPE_MGMT12WLAN_FC_STYPE_DEAUTH
0WLAN_FC_TYPE_MGMT13WLAN_FC_STYPE_ACTION
1WLAN_FC_TYPE_CTRL10WLAN_FC_STYPE_PSPOLL
1WLAN_FC_TYPE_CTRL11WLAN_FC_STYPE_RTS
1WLAN_FC_TYPE_CTRL12WLAN_FC_STYPE_CTS
1WLAN_FC_TYPE_CTRL13WLAN_FC_STYPE_ACK
1WLAN_FC_TYPE_CTRL14WLAN_FC_STYPE_CFEND
1WLAN_FC_TYPE_CTRL15WLAN_FC_STYPE_CFENDACK
2WLAN_FC_TYPE_DATA0WLAN_FC_STYPE_DATA
2WLAN_FC_TYPE_DATA1WLAN_FC_STYPE_DATA_CFACK
2WLAN_FC_TYPE_DATA2WLAN_FC_STYPE_DATA_CFPOLL
2WLAN_FC_TYPE_DATA3WLAN_FC_STYPE_DATA_CFACKPOLL
2WLAN_FC_TYPE_DATA4WLAN_FC_STYPE_NULLFUNC
2WLAN_FC_TYPE_DATA5WLAN_FC_STYPE_CFACK
2WLAN_FC_TYPE_DATA6WLAN_FC_STYPE_CFPOLL
2WLAN_FC_TYPE_DATA7WLAN_FC_STYPE_CFACKPOLL
2WLAN_FC_TYPE_DATA8WLAN_FC_STYPE_QOS_DATA

ESP32だと上記の定義が入っていました。今回はWi-FiアクセスポイントのSSIDを取得することが目的なので、必要なパケットを探します。

上記が参考になりました。パケットの構造を確認していくとどうやらビーコンパケットがWi-Fiアクセスポイントが定期的に送信しているデータのようです。

受信処理

void wifi_sniffer_packet_handler(void* buff, wifi_promiscuous_pkt_type_t type) {
  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;

  // not beacon
  if (WLAN_FC_GET_STYPE(hdr->frame_ctrl) != 0x08) {
    return;
  }

  char ssid[33];
  memset(ssid, 0, sizeof(ssid));
  memcpy(ssid, &ipkt->payload[6], ipkt->payload[5]);

  printf("PACKET FC=%04x, TYPE=%s, STYPE=%02x, 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,"
         " ADDR4=%02x:%02x:%02x:%02x:%02x:%02x,"
         " SSID=%s\n",
         hdr->frame_ctrl,
         wifi_sniffer_packet_type2str(type),
         WLAN_FC_GET_STYPE(hdr->frame_ctrl),
         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],
         /* ADDR4 */
         hdr->addr4[0], hdr->addr4[1], hdr->addr4[2],
         hdr->addr4[3], hdr->addr4[4], hdr->addr4[5],
         ssid
        );
}

最初にSTYPEを調べて、ビーコンの8以外のデータは表示をスキップしています。また、TYPEはMGMTでフィルタしているので、CTRLなどの他のパケットは受信していないはずです。

SSIDはビーコンのペイロードに入っていますので取り出しています。

このへんの情報はあまりなかったのですが、上記のサイトで構造がわかりました。通常はパソコンなどでキャプチャしたデータをもとに分析したりするので、実際の構造を知らなくてもなんとかなるのですがESP32だと結構面倒です。

実際にはデータを16進数ダンプして、確認しながらSSIDの場所を探しました。ipkt->payload[5]にSSIDの文字列長が入っており、ipkt->payload[6]から可変長のSSID文字列が入っているようでした。SSIDは32文字までだった気がするので33文字分のデータを確保して、0クリアしてからコピーしています。

あとはもともとあったデータやすぐに取れるデータを画面に表示しています。

ループ処理

void loop() {
  delay(WIFI_CHANNEL_SWITCH_INTERVAL);
  wifi_sniffer_set_channel(channel);
  channel = (channel % WIFI_CHANNEL_MAX) + 1;
}

500ミリ秒待機したあとに、Snifferするチャンネルを変更しています。これは2.4GHzのWi-Fiは1から13 or 14チャンネルぐらいまであり、同時に受信できるのは1つだけです。そのためチャンネルを変更しながらSnifferをします。

通常Wi-Fiアクセスポイントは100ミリ秒間隔でビーコンを送信しますので、500ミリ秒Snifferをすると4から5回受信できるはずです。全部のチャンネルに対して受信を試します。14チャンネルあると全部をスキャンするのに7秒かかることになります。

データの見かた

データ種別サンプル値備考
FC800x80しか来ないはずです
TYPEMGMT0のMGMTしか受信しないはず
STYPE88のビーコンしか受信しないはず
CHAN7チャンネル番号。1-14までがある
RSSI-68電波強度。マイナスが大きほど強い
ADDR1ff:ff:ff:ff:ff:ffビーコンの場合ff:ff:ff:ff:ff:ff固定
ADDR2bc:9c:31:??:??:??Wi-FiアクセスポイントのMACアドレス
ADDR3bc:9c:31:??:??:??Wi-FiアクセスポイントのMACアドレス
ADDR4b6:db:ba:??:??:??ビーコンでは利用しない
SSIDSSIDSSIDの名前

ざっくり上記のデータになります。ADDR1から4はTYPEとSTYPEの組み合わせによって使い方が変わってきます。ビーコンの場合には以下の組み合わせだと思います。

ADDR1DA(Destination Address)
ADDR2SA(Source Address)
ADDR3BSSID

DAはビーコンの場合にはブロードキャストなのでFF:FF:FF:FF:FF:FF固定になります。SAは送信元であるWi-Fiアクセスポイントですね。BSSIDは同じSSIDがあった場合に区別するための識別子で基本的にはSAと同じMACアドレスが入っていることがほとんどだと思います。

フィルタしている部分を外してみるといろいろなデータが表示できると思います。ただし、SSIDの表示処理が結構適当に処理をしているので、そこは消すかビーコンの場合のみ処理するように変更しないとおかしなことになると思います。

さて、実際に10秒以上データを取得してからエクセルなどで分析してみると楽しいと思います。同じSSIDでも違うMACアドレスがある場合があります。この場合には電波強度をみて強い方に接続を試す処理になっているようです。

また、同じMACアドレスでも複数のチャンネルで受信できているものがありますし、複数のSSIDを送信しているWi-Fiアクセスポイントもあります。

便利なWi-Fi Scan

#include "WiFi.h"

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

void loop() {
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  WiFi.scanNetworks(true);

  int wifiCnt;
  while (1) {
    wifiCnt = WiFi.scanComplete();
    if (wifiCnt >= 0) {
      break;
    }
  }

  Serial.printf("===========================================\n");
  for (int i = 0; i < wifiCnt; i++) {
    Serial.printf("CH=%2d, BSSID=%s, ENC=%d, RSSI=%d, SSID=%s\n", WiFi.channel(i), WiFi.BSSIDstr(i).c_str(), WiFi.encryptionType(i), WiFi.RSSI(i), WiFi.SSID(i).c_str());
  }
  WiFi.scanDelete();

  delay(1000);
}

そういえば一般的なSSIDの列挙ってどうしているんだろうって思ったら便利なWi-Fi Scanの関数群がありました!

あらかんたん、、、みなさんこちらを使いましょう。。。

まとめ

無差別モードは楽しいです。たぶんESP-NOWの通信とかはこれで全部傍受可能だと思います。元データの構造を勉強してからのほうがWi-Fi Scanのような便利関数群がどのように動いているのかがわかりやすいと思います。

まあ、ここまでやることはないと思います。。。

コメント