{"slug": "wallflower-reproducing-a-wifi-identity-inference-attack", "title": "Wallflower – reproducing a WiFi identity-inference attack", "summary": "Researchers at a WiFi-sensing lab reproduced the BFId identity-inference attack, which uses beamforming feedback information (BFI) broadcast in the clear by 802.11ac/ax/be devices to identify individuals by their gait with 99.5% accuracy. The reproduction, called wallflower, captures BFI and CSI traces from multiple perspectives and trains an LSTM classifier, aiming to verify the attack's generalizability beyond a single room. The work highlights a privacy vulnerability in WiFi beamforming protocols that leak person-specific channel perturbations even on encrypted networks.", "body_md": "A WiFi-sensing lab that reproduces the BFId paper — identity inference from the beamforming feedback 802.11 devices broadcast in the clear:\n\nTodt, Morsbach, Strufe.\n\n\"BFId: Identity Inference Attacks Utilizing Beamforming Feedback Information.\"CCS '25.\n\nwallflower\n\n- records synchronised\n**CSI + BFI** traces from a 4-perspective setup, - segments and parses them into ML-ready variable-length time series, and\n- trains/evaluates a baseline\n**LSTM identity classifier**.\n\nThe headline paper result it targets is **BFI identity accuracy 99.5% ± 0.38**\nfrom normal walking; the next milestone is reproducing it on our own captures and\nshowing it generalises beyond one room.\n\n802.11ac/ax/be beamforming requires a receiver to periodically send the\ntransmitter **Beamforming Feedback Information (BFI)** — compressed\n(quantised-angle) channel-state matrices — *in the clear*, even on encrypted\nnetworks. BFId shows that a passive observer who records this BFI while a person\nwalks can infer **who** the person is, because gait perturbs the channel in a\nperson-specific way. wallflower reproduces this by capturing BFI alongside\nground-truth **CSI** (channel state information) from multiple perspectives and\ntraining an identity classifier.\n\nFeature dimensions (from the paper, defined once in `wallflower/contract.py`\n\n):\n\n| Modality | Features | Composition | Nominal rate |\n|---|---|---|---|\n| BFI | 740 (+1 dt) | 10 quantised angles × 74 channels | ~10 Hz |\n| CSI | 212 (+1 dt) | (phase + magnitude) × 53 subcarriers × 2 antennas | ~285 Hz |\n\nThe `+1`\n\nis an appended time-delta column the models consume; parsers store it\nseparately as `dt`\n\nin each `.npz`\n\n.\n\nOperative setup (current pilot).A single lab-ownedASUS RT-AXE7800AP on5 GHz / channel 36 / 80 MHz(SSID`LAB_AP`\n\n, BSSID`AA:BB:CC:DD:EE:F4`\n\n) serves both the CSI and BFI roles — this is what`configs/ap_channels.yaml`\n\nand`wallflower/contract.py`\n\nactually encode. On node1 the host`wlp1s0`\n\nAX210 doesBFI capture + traffic + the live RSSI dashboard;CSI is captured bare-metal by FeitCSIon the second AX210. Everything runs on bare metal — no VMs. Real BFI is captured and decoded. The 6 GHz / dual-AP plan described below is the paper'snominalfull-deployment target, not the current rig.\n\nPaper-nominal full 4-perspective deployment (logical inventory in\n`configs/nodes.yaml`\n\nunder `topology.full`\n\n):\n\n**2× access points**, both 6 GHz / 160 MHz:** AP-CSI**on** channel 37**— carries the CSI-driving traffic.** AP-BFI**on** channel 85**— its beamforming sounding is what the passive recorder collects.\n\n**4 perspective nodes**, each with** 2× Intel AX210/AX1675 2×2**radios:** radio A = CSI**capture (monitor mode; FeitCSI).** radio B = BFI**client (associates to AP-BFI to elicit sounding).\n\n**1 CSI-traffic node**— generates`iperf3`\n\ntraffic (200 Mb/s TCP drives BFI sounding; 30 Kb/s UDP keeps CSI flowing).**1 passive BFI recorder**— a single monitor-mode capture (`tcpdump`\n\n) of all BFI sounding into one`bfi_recorder.pcapng`\n\n.**1 controller**— orchestrates sessions over SSH and (optionally) trains.\n\n```\n              AP-CSI (ch37)            AP-BFI (ch85)        6 GHz / 160 MHz\n                  |                         |\n   csi-traffic ---+                         +--- bfi-recorder (passive, 1 pcapng)\n                  |                         |\n        +---------+----------+   +----------+----------+   ... x4 perspectives\n        | perspective node N |   | radioA=CSI monitor  |\n        | 2x AX210           |   | radioB=BFI client   |\n        +--------------------+   +---------------------+\n                         \\             /\n                          \\           /\n                        controller (SSH orchestrator + trainer)\n```\n\nThe controller talks to each node over SSH (or locally in pilot) via:\n\n```\npython3 -m nodes.<agent> <action> --participant P001 --style normal --trial 001 \\\n    [--perspective N] [--out-dir DIR]\n# actions: detect | start | stop | status | health\n```\n\nEvery agent prints **one structured JSON object** to stdout\n(`{agent, action, ok, node, ts_utc, ...}`\n\n). `start`\n\nwrites a pidfile so `stop`\n\ncan terminate the capture. Output filenames come from `wallflower.contract`\n\n(`csi_raw_name(p)`\n\n, `bfi_recorder_name()`\n\n).\n\n```\ndata/raw/participant=P001/style=normal/trial=001/\n    metadata.json\n    csi_p1.raw  csi_p2.raw  csi_p3.raw  csi_p4.raw\n    bfi_recorder.pcapng\n    logs/\nwallflower/\n├── wallflower/            # shared CONTRACT (constants, paths, dataclasses) — import this\n│   └── contract.py  #   single source of truth: feature dims, channels, layout\n├── orchestrator/    # controller CLI (`wallflower`) — session/trial orchestration over SSH\n├── nodes/           # per-node agents (csi, bfi client, bfi recorder, traffic)\n├── capture/         # low-level capture helpers (tcpdump/iw wrappers, stdlib-only)\n├── parsers/         # CSI .raw + BFI .pcapng -> variable-length .npz / parquet\n├── models/          # baseline LSTM identity classifier + training/eval\n├── configs/         # lab.yaml, nodes.yaml, ap_channels.yaml\n├── scripts/         # operator convenience scripts\n├── experiments/     # experiment configs / run outputs\n└── data/            # raw / processed / models / reports  (git-ignored; .gitkeep)\n```\n\nConfig files (kept consistent with `wallflower/contract.py`\n\n):\n\n`configs/lab.yaml`\n\n— data root, 80/20 split, perspectives, styles + repeats, band/width/channels, clock-sync tolerance, sample-rate targets, traffic.`configs/nodes.yaml`\n\n— full 4-perspective inventory**and** the`pilot`\n\nprofile mapping every role onto node1.`configs/ap_channels.yaml`\n\n— operative RF plan: single ASUS AP, 5 GHz / 80 MHz, both roles on ch36 (SSID`LAB_AP`\n\n, BSSID`AA:BB:CC:DD:EE:F4`\n\n), with the paper-nominal 6 GHz / dual-channel plan noted as historical context.\n\n```\n# Core (orchestrator + nodes + capture stay stdlib-first; only pyyaml needed):\npip install -e .\n\n# ML stack (parsers + models): numpy/torch/pandas/pyarrow/scikit-learn/optuna\npip install -e \".[ml]\"\n\n# BFI pcapng parsing helper (scapy):\npip install -e \".[capture]\"\n\n# Dev tooling (ruff + pytest):\npip install -e \".[dev]\"\n```\n\nRequires Python ≥ 3.12.\n\n- Ubuntu 26.04 LTS, kernel 7.0.0, Python 3.14;\n`iw`\n\n6.17,`tcpdump`\n\n,`ssh`\n\npresent. **2× Intel AX210/AX1675 2×2** radios (iwlwifi loaded, firmware present):- PCI\n`01:00.0`\n\n→`wlp1s0`\n\n,`phy1`\n\n, MAC`AA:BB:CC:DD:EE:02`\n\n→**BFI client / traffic** role. - PCI\n`02:00.0`\n\n→`wlp2s0`\n\n,`phy2`\n\n, MAC`AA:BB:CC:DD:EE:03`\n\n→**BFI recorder** role.\n\n- PCI\n- Wired control plane:\n`eno1`\n\n. **Assumed MISSING**(degrade gracefully / document install):`tshark`\n\n,`iperf3`\n\n,`chrony`\n\n/`ntp`\n\n,`ptp4l`\n\n, PicoScenes,`git`\n\n,`gcc`\n\n/`make`\n\n/`cmake`\n\n/`dkms`\n\n.\n\n`sudo`\n\non node1 **requires a password** — non-interactive root is not available.\nAnything needing root (monitor mode, channel set via `iw`\n\n, package install, raw\nsocket capture) is **printed for the operator to run**, prefixed clearly, e.g.:\n\n```\n[OPERATOR-RUN] sudo iw dev wlp1s0 set type monitor\n[OPERATOR-RUN] sudo iw dev wlp1s0 set channel 36 80MHz   # 5 GHz\n```\n\nRead-only inspection (`lspci`\n\n, `iw dev`\n\n, `ip link`\n\n, reading `/sys`\n\n) works\nwithout root.\n\nThe pilot collapses every role onto node1 (see `configs/nodes.yaml`\n\nprofile\n`pilot`\n\n). It validates the end-to-end pipeline on a single machine before scaling\nto 4 perspectives.\n\n```\n# 0. Inspect radios without root (sanity check the pilot mapping):\npython3 -m nodes.csi_agent detect --perspective 1\n\n# 1. Create a session (writes session config under data/):\nwallflower init-session --participant P001 --profile pilot\n\n# 2. Start a trial (clock-sync check, spawns node agents, writes metadata.json):\nwallflower start-trial --participant P001 --style normal --trial 001\n\n# ...participant walks back and forth...\n\n# 3. Stop the trial (terminates captures via pidfiles, finalises metadata):\nwallflower stop-trial --participant P001 --style normal --trial 001\n\n# 4. Validate everything that was recorded for the session:\nwallflower validate-session --participant P001\n```\n\nAny privileged step that cannot run will print an `[OPERATOR-RUN]`\n\ncommand for\nyou to execute manually, then continue without crashing.\n\nInstalled as the `wallflower`\n\nconsole script (`orchestrator.cli:main`\n\n):\n\n| Command | Purpose |\n|---|---|\n`init-session` |\nCreate/validate a session and select a profile (`pilot` or `full` ). |\n`start-trial` |\nRun the clock-sync gate, spawn node agents (CSI, BFI client, BFI recorder, traffic), write the trial `metadata.json` . |\n`stop-trial` |\nStop captures via their pidfiles and finalise `metadata.json` . |\n`validate-session` |\nCheck raw layout, file presence, sample-rate / clock-sync tolerances (per `configs/lab.yaml` ) and report problems. |\n\nApache 2.0 — see [ LICENSE](/punklabs-ai/wallflower/blob/main/LICENSE).", "url": "https://wpnews.pro/news/wallflower-reproducing-a-wifi-identity-inference-attack", "canonical_source": "https://github.com/punklabs-ai/wallflower", "published_at": "2026-07-15 14:14:06+00:00", "updated_at": "2026-07-15 14:17:43.713427+00:00", "lang": "en", "topics": ["machine-learning", "ai-safety", "ai-ethics"], "entities": ["BFId", "wallflower", "ASUS RT-AXE7800", "Intel AX210", "FeitCSI", "CCS '25"], "alternates": {"html": "https://wpnews.pro/news/wallflower-reproducing-a-wifi-identity-inference-attack", "markdown": "https://wpnews.pro/news/wallflower-reproducing-a-wifi-identity-inference-attack.md", "text": "https://wpnews.pro/news/wallflower-reproducing-a-wifi-identity-inference-attack.txt", "jsonld": "https://wpnews.pro/news/wallflower-reproducing-a-wifi-identity-inference-attack.jsonld"}}