1. 硬件选型与平台搭建
想要打造一个蓝牙空口抓包器,硬件选型是第一步。我选择了ZYNQ7020+AD9361这套组合,原因很简单:性价比高、灵活性好,而且社区支持完善。这套方案的总成本可以控制在2000元以内,比商业抓包设备动辄上万元的价格亲民多了。
ZYNQ7020这颗芯片很有意思,它把ARM处理器和FPGA集成在了一起。具体来说:
- 双核Cortex-A9处理器,主频最高866MHz,跑Linux系统绰绰有余
- FPGA部分有85K逻辑单元,做实时信号处理完全够用
- 自带DDR3控制器、USB、千兆网口等外设
AD9361则是射频部分的灵魂:
- 70MHz-6GHz超宽频段覆盖,蓝牙2.4GHz频段轻松拿捏
- 56MHz瞬时带宽,抓取蓝牙信号游刃有余
- 集成12位ADC/DAC,省去了额外的高速数据转换芯片
硬件连接也很简单:
- 通过FMC接口将AD9361与ZYNQ7020对接
- 用网线连接ZYNQ和电脑
- 接上天线和电源就完成了硬件搭建
实测中发现一个坑:AD9361对电源噪声特别敏感。建议使用低噪声LDO供电,我在板子上加了TPS7A4700,把电源纹波控制在5mV以内,信号质量明显改善。
2. 软件环境配置
软件栈的选择直接影响开发效率。我的配置方案是:
- Vivado 2019.1 用于FPGA逻辑设计
- PetaLinux 构建嵌入式Linux系统
- GNU Radio 3.8.5 处理基带信号
- Wireshark 3.6.0 解析协议数据
安装GNU Radio时有个小技巧:直接用PyBOMBS安装比源码编译省心很多。执行以下命令就能搞定:
sudo pip install pybombs pybombs auto-config pybombs install gnuradio驱动配置要注意两点:
- 确保安装了libiio和libad9361-iio
- 给当前用户添加plugdev组权限,否则会报设备访问错误
验证硬件是否就绪可以运行:
iio_info -u ip:10.88.110.66看到AD9361设备信息就说明驱动正常。
3. GNU Radio信号处理流程设计
整个信号处理链路我拆解为6个关键模块:
3.1 射频前端配置
PlutoSDR Source模块负责硬件控制,关键参数:
- 中心频率设为2402MHz(蓝牙37信道)
- 采样率10MHz,略高于蓝牙2MHz带宽要求
- RF带宽设为2MHz,与蓝牙信道匹配
- 接收增益-90dBm起步,根据信号强度调整
3.2 信号预处理
Simple Squelch模块像是个"噪声门",设置-60dB的阈值可以过滤掉大部分环境噪声。这里有个经验值:当看到信号强度在-50dBm到-30dBm之间时,说明蓝牙信号捕获效果最佳。
3.3 下变频处理
Frequency Xlating FIR Filter完成两个任务:
- 将中心频率搬移到基带
- 进行抗混叠滤波 我用的参数是:
- 抽取因子5,将10MHz采样率降到2MHz
- FIR滤波器用汉明窗,过渡带宽200kHz
3.4 GFSK解调
这是最核心的部分,GFSK Demod模块配置要点:
- 调制指数0.5,与蓝牙标准一致
- 符号率1Msps(蓝牙标准速率)
- 频偏校准范围±100kHz
调试时发现,如果频偏超过150kHz就会导致误码率飙升。解决方法是在前面加个自动频率校正环。
3.5 比特流处理
Unpacked to Packed模块把每bit占1字节的稀疏数据压缩成紧凑格式。这里要注意字节序问题,蓝牙协议规定LSB first,所以参数要设置为:
bits_per_chunk = 1 endianness = gr.LSB_FIRST3.6 数据传输
ZMQ PUB-SUB模式非常适合实时数据流传输。我在GNU Radio端用ZMQ PUB发送:
address = "tcp://*:55555" socket = zmq.Context().socket(zmq.PUB) socket.bind(address)4. 蓝牙协议解析实战
收到原始比特流后,真正的挑战才开始。我的解析流程分为5个阶段:
4.1 前导码检测
蓝牙广播包的前导码固定为0xAA(1M PHY)或0xAAAA(2M PHY)。代码实现时用了状态机:
#define PREAMBLE_1M 0xAA #define PREAMBLE_2M 0xAAAA if(preamble == PREAMBLE_1M) { phy_mode = PHY_1M; } else if(preamble == PREAMBLE_2M) { phy_mode = PHY_2M; } else { // 非蓝牙数据丢弃 }4.2 接入地址验证
广播包的接入地址固定为0x8E89BED6。这里有个优化技巧:先检查最后1字节是否为0xD6,可以快速过滤掉80%的无效数据。
4.3 PDU头解析
PDU头包含关键信息:
typedef struct { uint8_t type:4; // PDU类型 uint8_t rfu:1; // 保留位 uint8_t chan_sel:1; // 信道选择标志 uint8_t tx_add:1; // 发送地址类型 uint8_t rx_add:1; // 接收地址类型 uint8_t length; // 载荷长度 } ble_header_t;4.4 数据去白处理
蓝牙使用LFSR进行数据白化,去白算法实现如下:
void dewhiten(uint8_t *data, int len, uint8_t channel) { uint8_t lfsr = (channel & 0x3F) | 0x02; for(int i=0; i<len; i++) { uint8_t d = data[i]; for(int j=0; j<8; j++) { if(lfsr & 0x80) { lfsr ^= 0x11; d ^= (1 << (7-j)); } lfsr <<= 1; } data[i] = d; } }4.5 CRC校验
蓝牙使用24位CRC,多项式为0x00065B。校验通过的数据才会交给Wireshark:
uint32_t calc_crc(const uint8_t *data, int len, uint32_t init) { uint32_t crc = init; for(int i=0; i<len; i++) { crc ^= (data[i] << 16); for(int j=0; j<8; j++) { crc <<= 1; if(crc & 0x1000000) crc ^= 0x10065B; } } return crc & 0xFFFFFF; }5. Wireshark数据注入技巧
要让抓包数据在Wireshark中完美展示,需要处理三个关键点:
5.1 PCAP文件格式
蓝牙LE的PCAP格式比较特殊,我在文件头中指定了链路类型:
#define LINKTYPE_BLUETOOTH_LE_LL_WITH_PHDR 256 struct pcap_hdr { uint32_t magic_number; uint16_t version_major; uint16_t version_minor; int32_t thiszone; uint32_t sigfigs; uint32_t snaplen; uint32_t network; // 这里设为256 };5.2 实时数据管道
用FIFO实现零延迟传输:
mkfifo /tmp/bt_sniffer wireshark -k -i /tmp/bt_sniffer & ./sniffer -o /tmp/bt_sniffer5.3 信道标记
在PCAP包中添加RF信道信息:
struct ble_phdr { uint8_t channel; uint8_t signal_power; uint8_t noise_power; uint8_t access_offenses; uint32_t ref_access_addr; uint16_t flags; };6. 实战效果与优化建议
经过两周的调试,最终实现了以下效果:
- 成功捕获BLE广播包(37/38/39信道)
- 平均捕获率92%(在-70dBm信号强度下)
- 端到端延迟<50ms
实测中发现几个优化点:
- 增加CRC校验失败计数,当失败率>5%时自动调整增益
- 对高频出现的无效接入地址进行过滤,减少CPU占用
- 添加信号强度热力图,辅助天线定位
这套方案的局限也很明显:
- 目前只能抓广播包,连接数据包还在开发中
- 同时抓多信道需要多个SDR设备
- 时间戳精度只有微秒级
建议想复现的开发者先从单信道抓包开始,等核心流程跑通后再扩展功能。我在GitHub开源了全部代码,包含详细的注释和测试数据。