用 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_source,args='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_mac 的 out,对每个首次出现的 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_management 中 case 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. 总结与踩坑清单
核心结论
gr-ieee802-11是 OFDM-only 解码器。想稳定抓 Beacon/SSID,必须上 5 GHz,2.4 GHz 的 CCK Beacon 解码不了。- 不依赖
gr-foo也能产出标准 pcap:自写 epy 模块 + radiotap header(DLT=127)即可被 Wireshark/tshark 直接消费。 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 文档:
pmt、gr.basic_block、Embedded Python Block - libpcap 文件格式(magic
0xa1b2c3d4,microsecond 分辨率)