用 HackRF + GNU Radio 抓取并解析 5 GHz Wi‑Fi 帧:从理论到 pcap 落地

用 HackRF + GNU Radio 抓取并解析 5 GHz Wi‑Fi 帧:从理论到 pcap 落地

作者:charlie

关键词:HackRF One、GNU Radio 3.10、gr‑ieee802‑11、802.11 OFDM、radiotap pcap、嵌入式 Python 模块

摘要

本文记录如何用一块廉价的 SDR 收发器(HackRF One/Pro)配合 GNU Radio 与开源 OOT 模块 gr-ieee802-11,在不依赖商用网卡的前提下,被动嗅探 5 GHz Wi‑Fi 帧,并将解码结果落地为 Wireshark 可直接打开的 radiotap pcap 文件。文章覆盖三块内容:(1) 为什么 2.4 GHz 抓不到 Beacon 帧(802.11b DSSS/CCK 不被该模块支持),而必须转到 5 GHz;(2) 自行实现的 PCAP 写入嵌入式 Python 模块(Embedded Python Block),重点解决 GNU Radio 3.10 中 pmt.blob_data() 返回 PyCapsule 而非 bytes 的坑;(3) 对 parse_mac 的补丁,使 Beacon / Probe Response / Probe Request 三类管理帧的 SSID 都能被提取并去重打印。整套方案无需 gr-foo、无需联网拉取外部仓库即可工作。


1. 理论基础

1.1 IEEE 802.11 物理层:OFDM 与 DSSS/CCK

Wi‑Fi 历史上存在两套并行的物理层调制方式:

物理层 标准 频段 调制 gr‑ieee802‑11 是否支持
DSSS / CCK 802.11b 2.4 GHz 直接序列/补码键控 ❌ 不支持
OFDM 802.11a / g / p / n(HT) 5 GHz 及 2.4 GHz 正交频分复用 ✅ 支持

gr-ieee802-11 是一个 OFDM-only 的软件收发机。它实现了 802.11a/g/p 的 PHY 解码链,但完全不处理 802.11b 的 DSSS/CCK 信号

这个事实直接决定了我们的抓包策略:

  • 2.4 GHz 的 Beacon 帧 几乎都以 802.11b 的 1 Mbps CCK 速率发出(为了最大兼容性和最远覆盖)。因此即使用 HackRF 收到了信号,gr-ieee802-11 也无法解码 → 控制台永远看不到 SSID。
  • 5 GHz 的全部帧都是 OFDM。只要频点、带宽、增益设置合理,Beacon、Probe Request/Response、数据帧都能被正常解码。

结论先行:要稳定看到 SSID / Beacon,请锁定 5 GHz 信道(本文使用信道 44,中心频率 5220 MHz)。2.4 GHz 上只能抓到少量以 OFDM 速率发送的数据帧,几乎见不到 Beacon。

1.2 802.11 MAC 帧结构

MAC 头固定 24 字节,结构(小端)关键字段:

复制代码
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|ver|  type  |  subtype | ...           |   ...   |   ...   ... |  Frame Control (2B)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Duration / ID (2B)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Address 1 (6B)                      |
... Address 2 (6B) ... Address 3 (6B) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Sequence Control (2B)  | ...
  • type(Frame Control 的 bit 2--3):0=管理帧,1=控制帧,2=数据帧。
  • subtype(bit 4--7):管理帧中 0x08=Beacon,0x04=Probe Request,0x05=Probe Response。

管理帧的 SSID 字段位置是补丁的关键:

  • Beacon / Probe Response 在 MAC 头(24B)之后有 12 字节固定参数:Timestamp(8) + Beacon Interval(2) + Capability(2),然后才是 Tagged Parameters。SSID 元素的 Tag 编号=0,结构为 [tag=0][len][ssid...],因此 SSID 长度字节位于 buf+24+13,SSID 内容位于 buf+24+14
  • Probe Request 没有固定参数部分,Tagged Parameters 直接从 buf+24 开始。SSID 元素的 Tag 位于 buf+24,长度位于 buf+24+1,内容位于 buf+24+2。许多探测帧甚至是"通配/广播探测",SSID 长度=0,表示请求任何 SSID。

1.3 gr‑ieee802‑11 解码链路

接收端的完整数据流如下:

复制代码
osmosdr_source (HackRF, fc32)
   │  IQ 采样 (int8 经驱动转 fc32)
   ▼
sync_short  ── 短训练序列相关,检测帧起始
   ▼
sync_long   ── 长训练序列,fine CFO/采样率补偿
   ▼
stream_to_vector (64)  ── 每 64 采样成 1 个 OFDM 符号
   ▼
fft_vcc (64)  ── 64 点 FFT,频域子载波
   ▼
frame_equalizer  ── 信道估计/均衡(见 1.4)
   ▼
decode_mac  ── 解卷积、解交织、去扰、CRC 校验,输出 MPDU (blob)
   ▼
parse_mac   ── 解析 MAC 头,提取 SSID 等信息,发布 metadata

decode_mac 输出的 blob 是 去掉 FCS、跳过 SERVICE 字段后的 PSDU;因此写入 pcap 时不需要在 radiotap 中置 FCS 标志。

1.4 frame_equalizer 的四种算法

frame_equalizer 提供四种信道均衡策略,可在 GRC 中切换:

  • LS(Least Squares):一次性最小二乘信道估计,用长训练序列直接求信道响应。简单、计算量小,但对时变信道跟踪差。
  • LMS(Least Mean Squares):判决导向(decision-directed)自适应跟踪,利用已判决的子载波持续更新均衡器,适合运动或多径时变场景。
  • Linear Comb(线性组合):仅用导频做插值,固定假设 SNR=42 dB。最朴素,性能好时可用,信道差时易崩。
  • STA(Space-Time Averaging):在 LMS 基础上对相邻 OFDM 符号做平均,进一步平滑估计。

实践上先用 LS 验证链路通断,复杂环境再试 STA / LMS

1.5 HackRF 的能力边界

HackRF One/Pro 覆盖 1 MHz--6 GHz,半双工,8 位采样精度,最高 20 MS/s。用于 Wi‑Fi 嗅探:

  • 5 GHz Wi‑Fi 带宽 20 MHz,设 samp_rate = 20e6 足够;
  • 天线只需一根 2.4/5 GHz 通用 SMA 天线,与 GPS 有源天线无关,无需开 bias‑tee;
  • 增益建议 0--40 dB 可调(本文默认 16 dB),过曝会抬高低 SNR 帧的解码率;
  • 半双工无妨,被动嗅探不需要发射。

2. 实践环境搭建

依赖:

复制代码
sudo apt install -y gnuradio gr-osmosdr hackrf libhackrf-dev tshark

编译安装 gr-ieee802-11(注意:本文未使用 gr-foo,PCAP 落地由自写 epy 模块完成):

复制代码
cd gr-ieee802-11
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make -j$(nproc)
sudo make install
sudo ldconfig

验证 HackRF 识别:

复制代码
hackrf_info

流图 wifi_rx_hackrf.grc 的关键参数为:

  • 源:osmosdr_sourceargs='hackrf=0'type=fc32
  • freq = 5220000000(信道 44,5.24 GHz);
  • samp_rate = 20000000
  • gain 滑杆 0--40 dB,默认 16;
  • 接收链接入 sync_short → sync_long → stream_to_vector(64) → fft_vcc(64) → frame_equalizer → decode_mac → parse_mac

3. 关键源码

3.1 PCAP Writer:嵌入式 Python 模块(Embedded Python Block)

GNU Radio 的 decode_mac 通过消息端口 out 发出 {metadata, blob} 对,blob 即原始 802.11 MPDU。我们写一个 epy 模块把它写成标准 radiotap pcap,可被 Wireshark/tshark 直接打开。

重要坑(GNU Radio 3.10.1.1)pmt.blob_data(blob) 在 Python 端返回的是 PyCapsule 对象而非 bytes ,直接 bytes(...) 会抛 TypeError: cannot convert 'PyCapsule' object to bytes。必须借助 ctypes 取出底层指针再 string_at 拷出。

python 复制代码
import pmt
import struct
import time
import ctypes
from gnuradio import gr


def _blob_to_bytes(blob):
    """GR 3.10 中 blob_data 返回 PyCapsule,需用 ctypes 取出真实字节。"""
    data = pmt.blob_data(blob)
    if isinstance(data, (bytes, bytearray)):
        return bytes(data)
    length = pmt.blob_length(blob)
    cap = ctypes.pythonapi
    cap.PyCapsule_GetName.restype = ctypes.c_char_p
    cap.PyCapsule_GetName.argtypes = [ctypes.py_object]
    cap.PyCapsule_GetPointer.restype = ctypes.c_void_p
    cap.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
    name = cap.PyCapsule_GetName(data)
    ptr = cap.PyCapsule_GetPointer(data, name if name is not None else None)
    return ctypes.string_at(ptr, length)


class blk(gr.basic_block):
    def __init__(self):
        gr.basic_block.__init__(self, name='WiFi PCAP Writer',
                                in_sig=[], out_sig=[])
        self._file = '/home/charlie/gnuradio/wifi_capture.pcap'
        self._fh = None
        # epy 模块必须在 __init__ 内手动注册消息端口
        self.message_port_register_in(pmt.intern('in'))
        self.set_msg_handler(pmt.intern('in'), self.handle_msg)

    def _open(self):
        self._fh = open(self._file, 'wb')
        # global header: magic(LE)=0xa1b2c3d4, v2.4, tz=0, sigfigs=0,
        # snaplen=65535, network=127 (DLT_IEEE802_11_RADIO)
        self._fh.write(struct.pack('<IHHiIII',
                                   0xa1b2c3d4, 2, 4, 0, 0, 65535, 127))

    def handle_msg(self, msg):
        if not pmt.is_pair(msg):
            return
        blob = pmt.cdr(msg)
        if not pmt.is_blob(blob):
            return
        frame = _blob_to_bytes(blob)
        n = len(frame)
        if n == 0:
            return
        if self._fh is None:
            self._open()
        # radiotap: version=0, pad=0, length=8, present flags=0 (无 FCS)
        radiotap = struct.pack('<BBHI', 0, 0, 8, 0)
        ts = time.time()
        ts_sec = int(ts)
        ts_usec = int(round((ts - ts_sec) * 1e6))
        incl = len(radiotap) + n
        self._fh.write(struct.pack('<IIII', ts_sec, ts_usec, incl, incl))
        self._fh.write(radiotap)
        self._fh.write(frame)
        self._fh.flush()

    def stop(self):
        if self._fh is not None:
            self._fh.close()
            self._fh = None
        return True

注:gr.basic_block 的消息端口不会经 GRC 的 make-template 自动生成,必须显式 message_port_register_in + set_msg_handler,否则连接时报 sink key 'in' not in sink block keys。构造函数参数也无法经 GRC 模板可靠传入,故路径在 __init__ 中硬编码。

配套的 SSID 去重打印 epy 模块 (订阅 parse_macout,对每个首次出现的 SSID 打印一次):

python 复制代码
import pmt
from gnuradio import gr


class blk(gr.basic_block):
    def __init__(self):
        gr.basic_block.__init__(self, name='SSID Printer',
                                in_sig=[], out_sig=[])
        self._seen = set()
        self.message_port_register_in(pmt.intern('in'))
        self.set_msg_handler(pmt.intern('in'), self.handle_msg)

    def handle_msg(self, msg):
        if not pmt.is_pair(msg):
            return
        meta = pmt.car(msg)
        if pmt.dict_has_key(meta, pmt.intern('ssid')):
            ssid = pmt.symbol_to_string(
                pmt.dict_ref(meta, pmt.intern('ssid'), pmt.PMT_NIL))
            if ssid not in self._seen:
                self._seen.add(ssid)
                print('[SSID] ' + ssid, flush=True)

3.2 parse_mac.cc 补丁:Beacon / Probe Response / Probe Request SSID 提取

原始 gr-ieee802-11 仅在调试时打印 Beacon 的 SSID。本文补上 Probe Response(case 5)与 Probe Request(case 4)。Beacon/Probe Response 用固定偏移,Probe Request 用参数扫描(因其无固定字段,且需兼容通配探测)。

parse_managementcase 4(Probe Request)补丁:

cpp 复制代码
case 4:
    d_meta = pmt::dict_add(d_meta, pmt::mp("subtype"), pmt::mp("Probe Request"));
    dout << "Probe Request" << std::endl;
    {
        // Probe Request 没有固定参数,Tagged Parameters 从 offset 24 开始。
        // 扫描参数列表寻找 SSID 元素 (tag number = 0)。
        int off = 24;
        while (off + 2 <= length) {
            uint8_t tag  = (uint8_t)buf[off];
            uint8_t tlen = (uint8_t)buf[off + 1];
            if (off + 2 + tlen > length) { break; }
            if (tag == 0) {  // SSID
                if (tlen > 0) {
                    std::string s(buf + off + 2, tlen);
                    d_meta = pmt::dict_add(d_meta, pmt::mp("ssid"), pmt::mp(s));
                    dout << "SSID: " << s;
                } else {
                    d_meta = pmt::dict_add(d_meta, pmt::mp("ssid"),
                                           pmt::mp("<broadcast/wildcard>"));
                    dout << "SSID: <broadcast/wildcard>";
                }
                break;
            }
            off += 2 + tlen;
        }
    }
    break;

case 5(Probe Response)补丁(与 Beacon case 8 相同偏移):

cpp 复制代码
case 5:
    d_meta = pmt::dict_add(d_meta, pmt::mp("subtype"), pmt::mp("Probe Response"));
    dout << "Probe Response" << std::endl;
    if (length < 38) { return; }
    {
        uint8_t* len = (uint8_t*)(buf + 24 + 13);
        if (length < 38 + *len) { return; }
        std::string s(buf + 24 + 14, *len);
        d_meta = pmt::dict_add(d_meta, pmt::mp("ssid"), pmt::mp(s));
        dout << "SSID: " << s;
    }
    break;

修改后重新编译安装即可,流图无需改动:

复制代码
cd gr-ieee802-11/build && cmake --build . -j && \
  echo 'YOUR_SUDO_PASS' | sudo -S make install && sudo ldconfig

4. 运行与验证

运行流图(GRC 或 grcc 生成的 Python 脚本)后,HackRF 开始接收 5 GHz 帧。控制台会陆续打印:

复制代码
[SSID] 147-1-501-5G

同时 wifi_capture.pcap 实时增长。用 tshark 校验文件格式与内容:

bash 复制代码
tshark -r wifi_capture.pcap -V | head -40
tshark -r wifi_capture.pcap -Y 'wlan.ssid' -T fields -e frame.number -e wlan.ssid

示例输出(Probe Request 帧,SSID 已被 tshark 直接解析):

复制代码
Frame 155: 182 bytes on wire (...)
IEEE 802.11 Probe Request, Flags: ........
    Type/Subtype: Probe Request (0x0004)
    Tag: SSID parameter set: 147-1-501-5G

说明链路完全打通:HackRF → gr‑ieee802‑11 → pcap → tshark 得到 120+ 帧有效捕获 ,且 SSID 经 pcap 与控制台两条路径均可获得。若某 SSID 只出现在 Probe Request(尚未收到 AP 回的 Probe Response),控制台 [SSID] 现在也能打印,因为 case 4 补丁已覆盖。

提示:广播 Data 帧(如 0x0020)不是 Beacon,其 MAC 负载是 CCMP 加密密文,hex 乱码属正常,并非数据残缺。


5. 总结与踩坑清单

核心结论

  1. gr-ieee802-11 是 OFDM-only 解码器。想稳定抓 Beacon/SSID,必须上 5 GHz,2.4 GHz 的 CCK Beacon 解码不了。
  2. 不依赖 gr-foo 也能产出标准 pcap:自写 epy 模块 + radiotap header(DLT=127)即可被 Wireshark/tshark 直接消费。
  3. parse_mac 的 SSID 提取默认仅 Beacon;补上 Probe Response / Probe Request 后,控制台与 pcap 双通道都能看到 SSID。

踩坑清单(节省你半天时间)

  • grcc -d <dir> 报错"-d 未识别"→ 用 grcc -o <dir> file.grc
  • epy 模块端口报错 sink key 'in' not in sink block keys → 忘记 message_port_register_in + set_msg_handler,补上即可。
  • epy 模块构造函数参数传不进(blk(file=) 变空)→ GRC 的 make-template 不会可靠替换 epy 构造参数,路径直接硬编码进 __init__
  • TypeError: cannot convert 'PyCapsule' object to bytes → GR 3.10.1.1 的 pmt.blob_data 返回 PyCapsule,用本文 _blob_to_bytes 的 ctypes 方案解决。
  • 2.4 GHz 死活没 SSID → 不是天线问题、不是增益问题,是 CCK 不被支持,换 5 GHz。
  • sudo make install 卡在密码提示 → 用 echo 'pass' | sudo -S make install,随后 sudo ldconfig

适用与局限

  • 适用:5 GHz 被动嗅探、信道占用分析、SSID/AP 发现、教学演示。
  • 局限:HackRF 8 位量化 + 半双工,不适合高吞吐或需要 ACK 的主动交互;2.4 GHz 802.11b 流量不可见;加密数据帧内容不可读(需四步握手+密钥才能解密,超出本文范围)。

参考

  • gr-ieee802-11 项目(Bastian Bloessl)
  • IEEE Std 802.11‑2020,MAC/PHY 帧格式与 radiotap 规范
  • GNU Radio 3.10 文档:pmtgr.basic_block、Embedded Python Block
  • libpcap 文件格式(magic 0xa1b2c3d4,microsecond 分辨率)
相关推荐
daad7771 天前
从电磁波到比特:用具体数字读懂 FM 与 WiFi 的 IQ 解调
5g·wifi·fm·gnuradio
W.W.H.8 天前
将驱动编译成模块并安装到rootfs
linux·物联网·wifi·rootfs·驱动
永栀哇9 天前
RT-Thread studio STM32F407+RW007模块+物联网软件包-阿里云-onenet云平台
阿里云·wifi·onenet·rtthread-stm32·物联网软件包
永栀哇9 天前
RT-Thread studio W25Q64 SFUD-FAL分区-存储参数+虚拟文件-ulog-RTC存储日志到文件-wifi-导出文件到电脑
wifi·rtthread·rw007·存储参数+日志文件+导出文件
艺杯羹15 天前
公共 Wi-Fi 流量安全全解析:从明文监听、流量拦截到 HTTPS 降级攻防
网络·web安全·网络安全·wifi
W.W.H.16 天前
嵌入式 Linux外接USB/WIFI模块兼容5G频段实战
linux·运维·5g·wifi
欢乐熊嵌入式编程2 个月前
WIFI通信协议全解析18: ESP32 作为 AP 热点:打造自己的“微型路由器”(附完整实战代码)
物联网·wifi·esp32·蓝牙·wifi协议·ap热点
天南散修2 个月前
MT7916 BA流程
网络·驱动开发·wifi·802.11
天南散修2 个月前
MT7916驱动中802.11转换为802.3
linux·网络·驱动开发·wifi·802.11