Loading up nexmon on a RPI4 with SPR
The built-in wifi radio on a Raspberry Pi 4 is kind of sad, as it does not support monitor mode. Luckily the hackers at Seemo Labs have fixed this.
In this post we'll describe how to load Seemoo's Nexmon onto a pi4 running a modern kernel, and package it into a SPR Plugin named spr-nexmon. We'll demonstrate that packet capture and injection works.
First, we will copy the template plugin
$ cp -R super/api_sample_plugin/ spr-nexmon
Development
Prebuilt binaries
We'll use some prebuilt binaries that include
- the nexmon firmware build for the broadcom wifi radio
- the 6.2 kernel build
- the nexutil binary
These were built from the 6.1/6.2 support pull-request
$ cp -R ../nexmon/binaries spr-nexmon/binaries
Docker preparations
We'll update the Dockerfile to include some useful tools and build the project.
FROM ubuntu:23.04 as builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y --no-install-recommends nano ca-certificates git curl
RUN mkdir /code
WORKDIR /code
ARG TARGETARCH
RUN curl -O https://dl.google.com/go/go1.20.linux-${TARGETARCH}.tar.gz
RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.linux-${TARGETARCH}.tar.gz
ENV PATH="/usr/local/go/bin:$PATH"
COPY code/ /code/
ARG USE_TMPFS=true
RUN \
[ "$USE_TMPFS" = "true" ] && ln -s /tmpfs /root/go; \
go build -ldflags "-s -w" -o /nexmon_plugin /code/nexmon_plugin.go
FROM ghcr.io/spr-networks/container_template:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends tcpdump kmod iw wireless-regdb && rm -rf /var/lib/apt/lists/*
COPY scripts /scripts/
COPY /nexmon_plugin /
COPY binaries/ nexmon/
ENTRYPOINT ["/scripts/startup.sh"]
We also want this container to use the host network and be privileged so it can load kernel modules. And we'll also set it to restart automatically
And heres the docker-compose.yml:
version: '3.4'
x-logging:
&default-logging
driver: journald
x-labels:
&default-labels
org.supernetworks.ci: ${CI:-false}
org.supernetworks.version: ${RELEASE_VERSION:-latest}${RELEASE_CHANNEL:-}
services:
nexmon:
container_name: supernexmon
build:
context: .
labels: *default-labels
logging: *default-logging
restart: always
network_mode: host
privileged: true
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- /lib/firmware/cypress/:/lib/firmware/cypress/
- "${SUPERDIR}./state/plugins/nexmon:/state/plugins/nexmon"
- "${SUPERDIR}./state/public/:/state/public/:ro"
Extending the SPR API
The Nexmon patch breaks the ability to change channels normally. Instead, we can do it with the 'nexutil' binary that nexmon provides.
We'll rename sample_plugin.go
to nexmon_plugin.go
and define a new function
func changeChannel(w http.ResponseWriter, r *http.Request) {
channel := r.URL.Query().Get("channel")
// Use regexp.MatchString to check if the input matches the pattern
matches, err := regexp.MatchString("^[0-9/]*$", channel)
if err != nil || !matches {
http.Error(w, "Invalid channel string", 400)
return
}
err = exec.Command("/nexmon/nexutil", "-k"+channel).Run()
if err != nil {
http.Error(w, err.Error(), 400)
return
}
}
//...
func main() {
//...
unix_plugin_router.HandleFunc("/change_channel", changeChannel).Methods("PUT")
}
Updating the startup script
When the container runs, we'll have it make sure the seemo firmware and kernel module are loaded fresh.
startup.sh:
#!/bin/bash
cd /nexmon
cp brcmfmac43455-sdio.bin /lib/firmware/cypress/cyfmac43455-sdio-standard.bin
rmmod brcmfmac_wcc
rmmod brcmfmac
insmod brcmfmac.ko
sleep 1
iw phy `iw dev wlan0 info | awk '/wiphy/ {printf "phy" $2}'` interface add mon0 type monitor
echo [+] Loaded
cd /
/nexmon_plugin
Loading
After building, with docker compose build
, we'll configure the API to load the plugin.
In the UI or by modifying configs/base/api.json
, add the nexmon plugin*
{
"Name": "nexmon",
"URI": "nexmon",
"UnixPath": "/state/plugins/nexmon/socket",
"Enabled": true,
"Plus": false,
"GitURL": "",
"ComposeFilePath": ""
}
Start the plugin with
SUPERDIR=/home/spr/super/ docker compose up -d
Testing
Running tcpdump should show captured 802.11 packets from the environment
# tcpdump -i wlan0 ...
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlan0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), snapshot length 262144 bytes
22:50:27.005540 1876482302us tsft 1.0 Mb/s 2412 MHz 11b -68dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.046106 1876522917us tsft 1.0 Mb/s 2412 MHz 11b -46dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.107930 1876584711us tsft 1.0 Mb/s 2412 MHz 11b -70dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.148500 1876625317us tsft 1.0 Mb/s 2412 MHz 11b -46dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.210323 1876687100us tsft 1.0 Mb/s 2412 MHz 11b -67dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
We can also verify that our channel switch api extension works
# curl -u admin:admin localhost/plugins/nexmon/change_channel?channel=4/20 -X PUT
# iw dev
phy#10
Interface wlan0
ifindex 44
wdev 0xa00000002
addr 00:00:00:00:00:00
type monitor
channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
Interface mon0
ifindex 43
wdev 0xa00000001
addr e4:5f:01:fd:a1:76
type managed
channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
txpower 31.00 dBm
...
* Note that the SPR UI does not allow specifying a docker compose path directly from the UI.
Instead, a user can modify or create a list in configs/base/custom_compose_paths.json
to do so.
Running barely-ap
Besides sniffing traffic, we can also do wild things with packet injection, like running a WPA2 Access Point written in scapy
Since the nexmon patch is a bit hacky, we set the wlan0 mac address ourselves and make sure the channel matches
ap = AP("turtlenet", "password1234", mode="iface", iface="wlan0", mac="e4:5f:01:cd:a1:76", channel=4)
“ET VOILÀ!”:
root@wifilab0:~/barely-ap/src# python3 ap.py
command failed: Device or resource busy (-16)
Created TUN interface scapyap at 10.10.10.1. Bind it to your services if needed.
Sending Authentication to 56:66:a3:9c:71:8b from e4:5f:01:cd:a1:76 (0x0B)...
Sending Association Response (0x01)...
sent eapol m1 56:66:a3:9c:71:8b
[+] New associated station 56:66:a3:9c:71:8b for bssid e4:5f:01:cd:a1:76
Want to try it yourself on SPR?
You can grab spr-nexmon here and barely-ap at https://github.com/spr-networks/barely-ap.