Passive Client Taxonomy
authorDenton Gentry <dgentry@google.com>
Mon, 15 Aug 2016 04:42:48 +0000 (21:42 -0700)
committerJouni Malinen <j@w1.fi>
Wed, 21 Sep 2016 21:45:24 +0000 (00:45 +0300)
Implement the signature mechanism described in the paper
"Passive Taxonomy of Wifi Clients using MLME Frame Contents"
published by Denton Gentry and Avery Pennarun.

http://research.google.com/pubs/pub45429.html
https://arxiv.org/abs/1608.01725

This involves:
1. Add a CONFIG_TAXONOMY compile option. Enabling taxonomy incurs
   a memory overhead of up to several kilobytes per associated
   station.
2. If enabled, store the Probe Request and (Re)Associate Request frame in
   struct sta_info.
3. Implement code to extract the ID of each Information Element,
   plus selected fields and bitmasks from certain IEs, into a
   descriptive text string. This is done in a new source file,
   src/ap/taxonomy.c.
4. Implement a "signature qq:rr:ss:tt:uu:vv" command
   in hostapd_cli to retrieve the signature.

Signatures take the form of a text string. For example, a signature
for the Nexus 5X is:
  wifi4|probe:0,1,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,
  vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a0201000040|assoc:0,1,48,45,
  221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,
  vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:0000000000000040

Signed-off-by: dgentry@google.com (Denton Gentry)
Signed-off-by: denny@geekhold.com (Denton Gentry)
Signed-off-by: rofrankel@google.com (Richard Frankel)
Signed-off-by: richard@frankel.tv (Richard Frankel)
12 files changed:
hostapd/Makefile
hostapd/ctrl_iface.c
hostapd/defconfig
hostapd/hostapd_cli.c
src/ap/beacon.c
src/ap/ctrl_iface_ap.c
src/ap/ctrl_iface_ap.h
src/ap/ieee802_11.c
src/ap/sta_info.c
src/ap/sta_info.h
src/ap/taxonomy.c [new file with mode: 0644]
src/ap/taxonomy.h [new file with mode: 0644]

index ba094ba..46dffe5 100644 (file)
@@ -100,6 +100,11 @@ NEED_SHA1=y
 OBJS += ../src/drivers/drivers.o
 CFLAGS += -DHOSTAPD
 
+ifdef CONFIG_TAXONOMY
+CFLAGS += -DCONFIG_TAXONOMY
+OBJS += ../src/ap/taxonomy.o
+endif
+
 ifdef CONFIG_MODULE_TESTS
 CFLAGS += -DCONFIG_MODULE_TESTS
 OBJS += hapd_module_tests.o
index 5f3d6bd..d7db4a7 100644 (file)
@@ -2367,6 +2367,11 @@ static int hostapd_ctrl_iface_receive_process(struct hostapd_data *hapd,
        } else if (os_strncmp(buf, "DISASSOCIATE ", 13) == 0) {
                if (hostapd_ctrl_iface_disassociate(hapd, buf + 13))
                        reply_len = -1;
+#ifdef CONFIG_TAXONOMY
+       } else if (os_strncmp(buf, "SIGNATURE ", 10) == 0) {
+               reply_len = hostapd_ctrl_iface_signature(hapd, buf + 10,
+                                                        reply, reply_size);
+#endif /* CONFIG_TAXONOMY */
        } else if (os_strncmp(buf, "POLL_STA ", 9) == 0) {
                if (hostapd_ctrl_iface_poll_sta(hapd, buf + 9))
                        reply_len = -1;
index f7b60e0..4659dd1 100644 (file)
@@ -337,3 +337,9 @@ CONFIG_IPV6=y
 # These extentions facilitate efficient use of multiple frequency bands
 # available to the AP and the devices that may associate with it.
 #CONFIG_MBO=y
+
+# Client Taxonomy
+# Has the AP retain the Probe Request and (Re)Association Request frames from
+# a client, from which a signature can be produced which can identify the model
+# of client device like "Nexus 6P" or "iPhone 5s".
+#CONFIG_TAXONOMY=y
index 04819d1..5e62542 100644 (file)
@@ -366,6 +366,22 @@ static char ** hostapd_complete_disassociate(const char *str, int pos)
 }
 
 
+#ifdef CONFIG_TAXONOMY
+static int hostapd_cli_cmd_signature(struct wpa_ctrl *ctrl, int argc,
+                                    char *argv[])
+{
+       char buf[64];
+
+       if (argc != 1) {
+               printf("Invalid 'signature' command - exactly one argument, STA address, is required.\n");
+               return -1;
+       }
+       os_snprintf(buf, sizeof(buf), "SIGNATURE %s", argv[0]);
+       return wpa_ctrl_command(ctrl, buf);
+}
+#endif /* CONFIG_TAXONOMY */
+
+
 #ifdef CONFIG_IEEE80211W
 static int hostapd_cli_cmd_sa_query(struct wpa_ctrl *ctrl, int argc,
                                    char *argv[])
@@ -1271,6 +1287,10 @@ static const struct hostapd_cli_cmd hostapd_cli_commands[] = {
        { "disassociate", hostapd_cli_cmd_disassociate,
          hostapd_complete_disassociate,
          "<addr> = disassociate a station" },
+#ifdef CONFIG_TAXONOMY
+       { "signature", hostapd_cli_cmd_signature, NULL,
+         "<addr> = get taxonomy signature for a station" },
+#endif /* CONFIG_TAXONOMY */
 #ifdef CONFIG_IEEE80211W
        { "sa_query", hostapd_cli_cmd_sa_query, NULL,
          "<addr> = send SA Query to a station" },
index 0a006f9..38182ae 100644 (file)
@@ -29,6 +29,7 @@
 #include "beacon.h"
 #include "hs20.h"
 #include "dfs.h"
+#include "taxonomy.h"
 
 
 #ifdef NEED_AP_MLME
@@ -784,6 +785,14 @@ void handle_probe_req(struct hostapd_data *hapd,
        }
 #endif /* CONFIG_P2P */
 
+#ifdef CONFIG_TAXONOMY
+       {
+               struct sta_info *sta = ap_get_sta(hapd, mgmt->sa);
+               if (sta)
+                       taxonomy_sta_info_probe_req(hapd, sta, ie, ie_len);
+       }
+#endif /* CONFIG_TAXONOMY */
+
        res = ssid_match(hapd, elems.ssid, elems.ssid_len,
                         elems.ssid_list, elems.ssid_list_len);
        if (res == NO_SSID_MATCH) {
index 23c8c60..3680fda 100644 (file)
@@ -23,6 +23,7 @@
 #include "ctrl_iface_ap.h"
 #include "ap_drv_ops.h"
 #include "mbo_ap.h"
+#include "taxonomy.h"
 
 
 static int hostapd_get_sta_tx_rx(struct hostapd_data *hapd,
@@ -429,6 +430,28 @@ int hostapd_ctrl_iface_disassociate(struct hostapd_data *hapd,
 }
 
 
+#ifdef CONFIG_TAXONOMY
+int hostapd_ctrl_iface_signature(struct hostapd_data *hapd,
+                                const char *txtaddr,
+                                char *buf, size_t buflen)
+{
+       u8 addr[ETH_ALEN];
+       struct sta_info *sta;
+
+       wpa_dbg(hapd->msg_ctx, MSG_DEBUG, "CTRL_IFACE SIGNATURE %s", txtaddr);
+
+       if (hwaddr_aton(txtaddr, addr))
+               return -1;
+
+       sta = ap_get_sta(hapd, addr);
+       if (!sta)
+               return -1;
+
+       return retrieve_sta_taxonomy(hapd, sta, buf, buflen);
+}
+#endif /* CONFIG_TAXONOMY */
+
+
 int hostapd_ctrl_iface_poll_sta(struct hostapd_data *hapd,
                                const char *txtaddr)
 {
index 6095d7d..4f99680 100644 (file)
@@ -19,6 +19,9 @@ int hostapd_ctrl_iface_deauthenticate(struct hostapd_data *hapd,
                                      const char *txtaddr);
 int hostapd_ctrl_iface_disassociate(struct hostapd_data *hapd,
                                    const char *txtaddr);
+int hostapd_ctrl_iface_signature(struct hostapd_data *hapd,
+                                const char *txtaddr,
+                                char *buf, size_t buflen);
 int hostapd_ctrl_iface_poll_sta(struct hostapd_data *hapd,
                                const char *txtaddr);
 int hostapd_ctrl_iface_status(struct hostapd_data *hapd, char *buf,
index 2ecd78f..f1c396b 100644 (file)
@@ -44,6 +44,7 @@
 #include "dfs.h"
 #include "mbo_ap.h"
 #include "rrm.h"
+#include "taxonomy.h"
 
 
 u8 * hostapd_eid_supp_rates(struct hostapd_data *hapd, u8 *eid)
@@ -2266,6 +2267,10 @@ static void handle_assoc(struct hostapd_data *hapd,
         * remove the STA immediately. */
        sta->timeout_next = STA_NULLFUNC;
 
+#ifdef CONFIG_TAXONOMY
+       taxonomy_sta_info_assoc_req(hapd, sta, pos, left);
+#endif /* CONFIG_TAXONOMY */
+
  fail:
        /*
         * In case of a successful response, add the station to the driver.
index c36842b..d9aa8e3 100644 (file)
@@ -222,6 +222,13 @@ void ap_free_sta(struct hostapd_data *hapd, struct sta_info *sta)
                hapd->iface->num_sta_ht_20mhz--;
        }
 
+#ifdef CONFIG_TAXONOMY
+       wpabuf_free(sta->probe_ie_taxonomy);
+       sta->probe_ie_taxonomy = NULL;
+       wpabuf_free(sta->assoc_ie_taxonomy);
+       sta->assoc_ie_taxonomy = NULL;
+#endif /* CONFIG_TAXONOMY */
+
 #ifdef CONFIG_IEEE80211N
        ht40_intolerant_remove(hapd->iface, sta);
 #endif /* CONFIG_IEEE80211N */
index cf3fbb1..d0e43be 100644 (file)
@@ -214,6 +214,11 @@ struct sta_info {
                              * received, starting from the Length field */
 
        u8 rrm_enabled_capa[5];
+
+#ifdef CONFIG_TAXONOMY
+       struct wpabuf *probe_ie_taxonomy;
+       struct wpabuf *assoc_ie_taxonomy;
+#endif /* CONFIG_TAXONOMY */
 };
 
 
diff --git a/src/ap/taxonomy.c b/src/ap/taxonomy.c
new file mode 100644 (file)
index 0000000..e533a10
--- /dev/null
@@ -0,0 +1,282 @@
+/*
+ * hostapd / Client taxonomy
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ *
+ * Parse a series of IEs, as in Probe Request or (Re)Association Request frames,
+ * and render them to a descriptive string. The tag number of standard options
+ * is written to the string, while the vendor ID and subtag are written for
+ * vendor options.
+ *
+ * Example strings:
+ * 0,1,50,45,221(00904c,51)
+ * 0,1,33,36,48,45,221(00904c,51),221(0050f2,2)
+ */
+
+#include "utils/includes.h"
+
+#include "utils/common.h"
+#include "common/wpa_ctrl.h"
+#include "hostapd.h"
+#include "sta_info.h"
+
+
+/* Copy a string with no funny schtuff allowed; only alphanumerics. */
+static void no_mischief_strncpy(char *dst, const char *src, size_t n)
+{
+       size_t i;
+
+       for (i = 0; i < n; i++) {
+               unsigned char s = src[i];
+               int is_lower = s >= 'a' && s <= 'z';
+               int is_upper = s >= 'A' && s <= 'Z';
+               int is_digit = s >= '0' && s <= '9';
+
+               if (is_lower || is_upper || is_digit) {
+                       /* TODO: if any manufacturer uses Unicode within the
+                        * WPS header, it will get mangled here. */
+                       dst[i] = s;
+               } else {
+                       /* Note that even spaces will be transformed to
+                        * underscores, so 'Nexus 7' will turn into 'Nexus_7'.
+                        * This is deliberate, to make the string easier to
+                        * parse. */
+                       dst[i] = '_';
+               }
+       }
+}
+
+
+static int get_wps_name(char *name, size_t name_len,
+                       const u8 *data, size_t data_len)
+{
+       /* Inside the WPS IE are a series of attributes, using two byte IDs
+        * and two byte lengths. We're looking for the model name, if
+        * present. */
+       while (data_len >= 4) {
+               u16 id, elen;
+
+               id = WPA_GET_BE16(data);
+               elen = WPA_GET_BE16(data + 2);
+               data += 4;
+               data_len -= 4;
+
+               if (elen > data_len)
+                       return 0;
+
+               if (id == 0x1023) {
+                       /* Model name, like 'Nexus 7' */
+                       size_t n = (elen < name_len) ? elen : name_len;
+                       no_mischief_strncpy(name, (const char *) data, n);
+                       return n;
+               }
+
+               data += elen;
+               data_len -= elen;
+       }
+
+       return 0;
+}
+
+
+static void ie_to_string(char *fstr, size_t fstr_len, const struct wpabuf *ies)
+{
+       char *fpos = fstr;
+       char *fend = fstr + fstr_len;
+       char htcap[7 + 4 + 1]; /* ",htcap:" + %04hx + trailing NUL */
+       char htagg[7 + 2 + 1]; /* ",htagg:" + %02hx + trailing NUL */
+       char htmcs[7 + 8 + 1]; /* ",htmcs:" + %08x + trailing NUL */
+       char vhtcap[8 + 8 + 1]; /* ",vhtcap:" + %08x + trailing NUL */
+       char vhtrxmcs[10 + 8 + 1]; /* ",vhtrxmcs:" + %08x + trailing NUL */
+       char vhttxmcs[10 + 8 + 1]; /* ",vhttxmcs:" + %08x + trailing NUL */
+#define MAX_EXTCAP     254
+       char extcap[8 + 2 * MAX_EXTCAP + 1]; /* ",extcap:" + hex + trailing NUL
+                                             */
+       char txpow[7 + 4 + 1]; /* ",txpow:" + %04hx + trailing NUL */
+#define WPS_NAME_LEN           32
+       char wps[WPS_NAME_LEN + 5 + 1]; /* room to prepend ",wps:" + trailing
+                                        * NUL */
+       int num = 0;
+       const u8 *ie;
+       size_t ie_len;
+       int ret;
+
+       os_memset(htcap, 0, sizeof(htcap));
+       os_memset(htagg, 0, sizeof(htagg));
+       os_memset(htmcs, 0, sizeof(htmcs));
+       os_memset(vhtcap, 0, sizeof(vhtcap));
+       os_memset(vhtrxmcs, 0, sizeof(vhtrxmcs));
+       os_memset(vhttxmcs, 0, sizeof(vhttxmcs));
+       os_memset(extcap, 0, sizeof(extcap));
+       os_memset(txpow, 0, sizeof(txpow));
+       os_memset(wps, 0, sizeof(wps));
+       *fpos = '\0';
+
+       if (!ies)
+               return;
+       ie = wpabuf_head(ies);
+       ie_len = wpabuf_len(ies);
+
+       while (ie_len >= 2) {
+               u8 id, elen;
+               char *sep = (num++ == 0) ? "" : ",";
+
+               id = *ie++;
+               elen = *ie++;
+               ie_len -= 2;
+
+               if (elen > ie_len)
+                       break;
+
+               if (id == WLAN_EID_VENDOR_SPECIFIC && elen >= 4) {
+                       /* Vendor specific */
+                       if (WPA_GET_BE32(ie) == WPS_IE_VENDOR_TYPE) {
+                               /* WPS */
+                               char model_name[WPS_NAME_LEN + 1];
+                               const u8 *data = &ie[4];
+                               size_t data_len = elen - 4;
+
+                               os_memset(model_name, 0, sizeof(model_name));
+                               if (get_wps_name(model_name, WPS_NAME_LEN, data,
+                                                data_len)) {
+                                       os_snprintf(wps, sizeof(wps),
+                                                   ",wps:%s", model_name);
+                               }
+                       }
+
+                       ret = os_snprintf(fpos, fend - fpos,
+                                         "%s%d(%02x%02x%02x,%d)",
+                                         sep, id, ie[0], ie[1], ie[2], ie[3]);
+               } else {
+                       if (id == WLAN_EID_HT_CAP && elen >= 2) {
+                               /* HT Capabilities (802.11n) */
+                               os_snprintf(htcap, sizeof(htcap),
+                                           ",htcap:%04hx",
+                                           WPA_GET_LE16(ie));
+                       }
+                       if (id == WLAN_EID_HT_CAP && elen >= 3) {
+                               /* HT Capabilities (802.11n), A-MPDU information
+                                */
+                               os_snprintf(htagg, sizeof(htagg),
+                                           ",htagg:%02hx", (u16) ie[2]);
+                       }
+                       if (id == WLAN_EID_HT_CAP && elen >= 7) {
+                               /* HT Capabilities (802.11n), MCS information */
+                               os_snprintf(htmcs, sizeof(htmcs),
+                                           ",htmcs:%08hx",
+                                           (u16) WPA_GET_LE32(ie + 3));
+                       }
+                       if (id == WLAN_EID_VHT_CAP && elen >= 4) {
+                               /* VHT Capabilities (802.11ac) */
+                               os_snprintf(vhtcap, sizeof(vhtcap),
+                                           ",vhtcap:%08x",
+                                           WPA_GET_LE32(ie));
+                       }
+                       if (id == WLAN_EID_VHT_CAP && elen >= 8) {
+                               /* VHT Capabilities (802.11ac), RX MCS
+                                * information */
+                               os_snprintf(vhtrxmcs, sizeof(vhtrxmcs),
+                                           ",vhtrxmcs:%08x",
+                                           WPA_GET_LE32(ie + 4));
+                       }
+                       if (id == WLAN_EID_VHT_CAP && elen >= 12) {
+                               /* VHT Capabilities (802.11ac), TX MCS
+                                * information */
+                               os_snprintf(vhttxmcs, sizeof(vhttxmcs),
+                                           ",vhttxmcs:%08x",
+                                           WPA_GET_LE32(ie + 8));
+                       }
+                       if (id == WLAN_EID_EXT_CAPAB) {
+                               /* Extended Capabilities */
+                               int i;
+                               int len = (elen < MAX_EXTCAP) ? elen :
+                                       MAX_EXTCAP;
+                               char *p = extcap;
+
+                               p += os_snprintf(extcap, sizeof(extcap),
+                                                ",extcap:");
+                               for (i = 0; i < len; i++) {
+                                       int lim;
+
+                                       lim = sizeof(extcap) -
+                                               os_strlen(extcap);
+                                       if (lim <= 0)
+                                               break;
+                                       p += os_snprintf(p, lim, "%02x",
+                                                        *(ie + i));
+                               }
+                       }
+                       if (id == WLAN_EID_PWR_CAPABILITY && elen == 2) {
+                               /* TX Power */
+                               os_snprintf(txpow, sizeof(txpow),
+                                           ",txpow:%04hx",
+                                           WPA_GET_LE16(ie));
+                       }
+
+                       ret = os_snprintf(fpos, fend - fpos, "%s%d", sep, id);
+               }
+               if (os_snprintf_error(fend - fpos, ret))
+                       goto fail;
+               fpos += ret;
+
+               ie += elen;
+               ie_len -= elen;
+       }
+
+       ret = os_snprintf(fpos, fend - fpos, "%s%s%s%s%s%s%s%s%s",
+                         htcap, htagg, htmcs, vhtcap, vhtrxmcs, vhttxmcs,
+                         txpow, extcap, wps);
+       if (os_snprintf_error(fend - fpos, ret)) {
+       fail:
+               fstr[0] = '\0';
+       }
+}
+
+
+int retrieve_sta_taxonomy(const struct hostapd_data *hapd,
+                         struct sta_info *sta, char *buf, size_t buflen)
+{
+       int ret;
+       char *pos, *end;
+
+       if (!sta->probe_ie_taxonomy || !sta->assoc_ie_taxonomy)
+               return 0;
+
+       ret = os_snprintf(buf, buflen, "wifi4|probe:");
+       if (os_snprintf_error(buflen, ret))
+               return 0;
+       pos = buf + ret;
+       end = buf + buflen;
+
+       ie_to_string(pos, end - pos, sta->probe_ie_taxonomy);
+       pos = os_strchr(pos, '\0');
+       if (pos >= end)
+               return 0;
+       ret = os_snprintf(pos, end - pos, "|assoc:");
+       if (os_snprintf_error(end - pos, ret))
+               return 0;
+       pos += ret;
+       ie_to_string(pos, end - pos, sta->assoc_ie_taxonomy);
+       pos = os_strchr(pos, '\0');
+       return pos - buf;
+}
+
+
+void taxonomy_sta_info_probe_req(const struct hostapd_data *hapd,
+                                struct sta_info *sta,
+                                const u8 *ie, size_t ie_len)
+{
+       wpabuf_free(sta->probe_ie_taxonomy);
+       sta->probe_ie_taxonomy = wpabuf_alloc_copy(ie, ie_len);
+}
+
+
+void taxonomy_sta_info_assoc_req(const struct hostapd_data *hapd,
+                                struct sta_info *sta,
+                                const u8 *ie, size_t ie_len)
+{
+       wpabuf_free(sta->assoc_ie_taxonomy);
+       sta->assoc_ie_taxonomy = wpabuf_alloc_copy(ie, ie_len);
+}
diff --git a/src/ap/taxonomy.h b/src/ap/taxonomy.h
new file mode 100644 (file)
index 0000000..9c85ee2
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * hostapd / Station client taxonomy
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef TAXONOMY_H
+#define TAXONOMY_H
+
+void taxonomy_sta_info_probe_req(const struct hostapd_data *hapd,
+                                struct sta_info *sta,
+                                const u8 *ie, size_t ie_len);
+void taxonomy_sta_info_assoc_req(const struct hostapd_data *hapd,
+                                struct sta_info *sta,
+                                const u8 *ie, size_t ie_len);
+int retrieve_sta_taxonomy(const struct hostapd_data *hapd,
+                         struct sta_info *sta, char *buf, size_t buflen);
+
+#endif /* TAXONOMY_H */