☰
BLE设备通信劫持自动化测试框架:从原理到代码实现
2026/10/1 4:53:16 网站建设 项目流程

1. 项目背景与设计思路

做着做着就发现,手里那台工业级 BLE 设备经常无故断连,有时候明明连接成功了,过几分钟却莫名其妙掉线。排查到最后,问题竟然出在通信链路上——设备压根没真正校验连接方的身份,只要拿到正确的服务和特征值 UUID,任何人都能“接管”通信。

后来我把这套验证过程做成了自动化测试框架,专门用来对 BLE 设备做通信劫持测试。说白了,就是通过模拟真实攻击路径,检验设备是否存在连接劫持、数据窃听、重放攻击等安全风险。很多做 BLE 开发测试的朋友可能都遇到过类似的痛点:要么纯靠手动操作,用 BLE 调试工具慢慢摸,效率低而且容易漏掉边界场景;要么压根没意识到自己的设备在安全设计上存在如此大的隐患。

所以我把这套测试框架的思路和完整实现细节整理出来,希望能给正在做 BLE 相关开发、测试、安全评估的朋友一些参考。它适用于嵌入式开发工程师验证自己设备的抗劫持能力、测试团队做通信安全回归测试,以及安全研究人员对 BLE 设备做初步的健壮性评估。

1.1 核心需求解析

在动手写这个框架之前,需要先理清楚核心需求。这个项目最终要解决三个非常具体的问题:

第一个,劫持验证。用户提到的“BLE 设备通信劫持”,在技术层面的本质就是验证设备会不会无条件接受一个伪造的连接请求,或者中间人是否能够轻松介入连接链路。这里涉及的关键技术点包括 GATT 服务发现、特征值读写权限校验、配对绑定机制等。

第二个,自动化替代人工。平时用手机上的 BLE 调试工具,只能看到当前连接的设备服务和特征值,想模拟特定攻击流程非常繁琐。框架需要一个能够完全程序化控制扫描、连接、读写操作的底层库,并且能把这些操作组织成可重复执行的测试脚本。

第三个,结果可沉淀、报告可输出。测试不能只跑一遍就结束,需要反复回归,还要能记录每一步的耗时、收发的数据内容以及失败的具体原因,才能作为安全评估的参考依据。

1.2 技术选型背后的取舍

技术选型这一个环节,说实话我踩了不少坑,一开始想过用 Java 做 Android 端的测试工具,毕竟 Android 对 BLE 的支持比较成熟。但后来评估了一下,决定使用 Python 作为主要开发语言。原因很简单,Python 在自动化测试生态上太完善了,pytest 框架写起来效率高,断言、夹具、参数化这些功能都是开箱即用,而且处理二进制数据非常顺手——BLE 通信中大量的数据包解析就是 bytes 操作,Python 在这方面比 Java 简洁得多。

底层通信库上,最终选用了 bleak。这个库是跨平台的,在 Windows、Linux、macOS 上都能跑,它的异步 API 设计非常适合做自动化测试。另一个选择是 pybluez,但 pybluez 在 Windows 上的安装非常折腾,Python 3.10 以上的环境兼容性也一般。相比之下,bleak 用 pip 直接安装就能搞定,省下不少时间。

中间人测试的部分,需要抓取和伪造 BLE 数据包,这就要用到一套单独的数据包处理模块来实现 LL(链路层)数据包的解析和注入。配合 nRF Sniffer 这类硬件抓包工具,可以拿到真实的空中数据包,再用解析脚本还原出完整的交互流程。

注意:这里提到的所有测试手段,都是用来对自己拥有或明确获授权测试的 BLE 设备做安全评估,请勿对他人设备进行未授权的测试操作。

2. 框架核心模块与通信原理

这个框架从整体上可以拆成四个模块,每个模块各管一摊事情,相互之间通过接口串联起来。

2.1 BLE 劫持测试的关键原理

要说清楚框架的每个模块,必须先把这个“劫持”动作在 BLE 协议层面是什么样子讲明白。BLE 通信分为广播、扫描、连接、配对绑定几个阶段,劫持的风险就藏在这几个阶段里。

最容易被忽视的风险点在连接的认证环节。BLE 设备之间建立连接后,如果要进行敏感数据的读写,通常会通过配对(Pairing)过程来创建加密链路。Pairing 分为 Legacy Pairing 和 LE Secure Connections 两种。Legacy Pairing 在密钥协商阶段使用短的 PIN 码,很容易被暴力破解,这就是一个经典的劫持切入点。

实际测试中你会发现很多 BLE 外设(尤其是低成本设备)甚至没有开启配对绑定。也就是说,任何中心设备只要扫描到这个外设并发起连接,就能直接读写它的特征值。这类设备根本没有身份校验,所谓“劫持”连攻击都不用,就是一个普通连接的事。所以框架里专门设计了一个连接测试模块,重点验证设备在这些环节上是否具备足够的安全防护能力。

测试过程还涉及“中间人”角色。中间人攻击的基本思路是设备 A 和外设之间插入一个伪造的中心设备 M,M 替代真实外设的 GATT 服务,对真实设备来说 M 就是合法外设,对真实外设来说 M 又是合法中心。如果外设没有进行双向认证,这个 M 就能同时欺骗两端,中转或篡改数据。

2.2 扫描发现与设备识别模块

框架的第一步动作是扫描设备,但直接上全量扫描效率太低,通常测试环境里还有其他 BLE 设备在广播干扰。所以扫描模块需要支持过滤规则,可以按设备名称、广播数据里的服务 UUID、MAC 地址前缀这些维度筛选目标。

初始扫描参数是用一段简洁的配置来控制的,里面包括扫描窗口和扫描间隔。这个参数的具体含义值得仔细体会:扫描窗口决定每次扫描持续多久,扫描间隔是两次扫描之间的时间间隔。窗口设置越大,发现设备越快,但功耗也更高。因为这是测试框架不是嵌入式设备,功耗不太重要,所以可以把窗设置得大一些来换取扫描速度。

扫描完成后拿到的是设备列表,每个设备都包含广播数据。广播数据里有个 AD Structure(广播数据结构)的概念,数据是一段一段拼接的,每段由长度、类型、具体数据组成。框架中写了一个解析函数,把 broadcast data 中的服务 UUID、设备名称、厂商自定义数据全部提取出来,方便后面对目标设备做指纹识别。

2.3 GATT 服务发现与特征值挖掘

连接成功后,最重要的一步是服务发现。GATT 协议把设备的能力组织成服务,服务下面包含特征值,特征值支持读、写、通知等不同的属性。对于测试来说,特征值就像一扇扇门,每一扇门背后都是设备的一个功能点。

框架会在连接成功后自动遍历设备的所有服务,把每个特征值的 UUID、属性、权限都打印出来。这里有个很容易踩的坑,就是某些特征值的 UUID 并不是标准 16 位 UUID,而是厂商自定义的 128 位 UUID。扫描 128 位 UUID 需要时间和耐心,尤其在设备服务比较多的时候。但我要提醒的是,这部分工作恰恰是劫持测试最有价值的环节,因为很多开发者在自定义服务里隐藏了一些“敏感”的功能接口,比如固件升级、参数配置、状态重置等。如果这些特征值的写权限没有做访问控制,那就是明摆着的攻击面。

自动化处理服务发现的一个技巧是设置合理的超时。GATT 服务发现过程如果中途出问题,很容易卡住整个测试流程,所以要给整个发现过程加超时控制。

2.4 劫持验证自动化流程

框架的核心测试流程使用一套简单的状态机来驱动。理解这套状态机是理解整个框架的关键。

整个流程分成四个阶段:扫描发现、连接绑定、读写操作、断连与重连。每个阶段都对应一组测试用例,用例之间互相独立,方便单独执行和结果追踪。

在连接测试阶段,首要验证的是绑定校验机制。框架会用一个全新的、没有任何配对信息的虚拟中心设备去连接外设,观察外设是否直接允许通过,还是会发起配对请求。如果外设直接允许通过,说明完全没有认证保护。接下来用测试工具尝试读取写保护的特征值,看设备是否会拒绝写入或者返回错误码。这一步能验证特征值的权限是否真正在应用层生效。

重连机制方面也有不少门道。测试框架会主动断开连接,然后立刻重新连接,验证设备是否允许上次连接留下的 session key 直接恢复会话。如果在没有重新配对的情况下就能恢复加密会话,那说明会话密钥的存储和管理存在被利用的可能。这个测试点对蓝牙耳机、智能锁这类产品尤其重要。

3. 测试框架代码实现详解

前面讲了原理和模块设计,现在进入到最核心的实操部分。代码量不小,但我会把骨架贴出来,然后详细解释每一块的作用和设计原因。

3.1 调试环境准备

先说环境搭建,这块相对简单。需要一台支持 BLE 的电脑,加上 Python 3.9 以上的环境。PyBluez 在 Windows 上确实很难装,所以我推荐用 bleak:

pip install bleak pytest pip install asyncio-mqtt # 可选,如果后续需要对接物联网平台

如果是做抓包和更底层的链路层分析,最好准备一块 Nordic nRF52840 Dongle,配合 Wireshark 的 BLE 解析插件使用。这步不是必须的,但有了它你才能真正看到空中传输的原始数据包,对排查问题是质的提升。

3.2 核心扫描与连接封装

来看看扫描模块的具体实现。用 bleak 提供接口来实现设备扫描,代码非常简洁:

import asyncio from bleak import BleakScanner async def scan_devices(scan_time=10, name_filter=None): devices = await BleakScanner.discover(scan_time, return_adv=True) results = [] for addr, adv_data in devices.items(): name = adv_data.local_name or "" if name_filter and name_filter not in name: continue results.append({ "addr": addr, "name": name, "rssi": adv_data.rssi, "service_uuids": list(adv_data.service_uuids), "manufacturer_data": adv_data.manufacturer_data }) return results

段代码里比较值得琢磨的是return_adv=True这个参数的用法。它是把广播数据一并返回,避免再走一遍系统 API 获取完整广播信息。获取到的manufacturer_data解析出来之后会得到一个字典,key 是厂商 ID,value 是厂商自定义的数据段,这个在识别特定设备的时候非常有用。

连接封装则要注意 BLE 连接的超时和重试机制。Windows 下的 bleak 连接偶尔会莫名其妙超时,重启蓝牙驱动才能恢复,所以框架里加了重试逻辑和最长连接等待时间限制。

from bleak import BleakClient async def connect_device(address, max_retries=3): for attempt in range(max_retries): try: client = BleakClient(address, timeout=20.0) await client.connect() return client except Exception as e: print(f"连接第 {attempt+1} 次失败: {e}") await asyncio.sleep(2) raise ConnectionError(f"设备 {address} 连接失败,超过重试次数")

连接的时间太长也是一个常见问题。20 秒的超时看起来很长,但在某些信号弱的环境下,BLE 建连过程确实能达到这个时间量级。

3.3 特征值读写测试实现

拿到连接后,先做一个全面的服务发现,枚举所有特征值。这一步我把 GATT 信息格式化输出,方便测试人员直观判断哪些特征值是敏感入口。

async def enumerate_gatt(client): services = await client.get_services() for service in services: print(f"服务: {service.uuid}") for char in service.characteristics: properties = ",".join(char.properties) print(f" 特征值: {char.uuid} 属性: {properties}") if "read" in char.properties: try: value = await client.read_gatt_char(char.uuid) print(f" 读取值: {value.hex()}") except Exception as e: print(f" 读取失败: {e}")

这里读到的二进制数据直接以 hex 形式打印,方便对照协议文档。很多 BLE 设备的数据格式是厂商自定义的,不是标准的小端序,所以用 hex 查看是最中性、最不容易出误判的方式。

特征值写入测试同样重要。测试时会尝试写四种类型的数据:正常指令、超长数据、空数据、随机字节。这四类数据的测试意图分别是:验证正常功能、测试 MTU 边界处理、测试空数据包处理、测试未知指令的容错。

async def write_characteristic_with_log(client, char_uuid, data: bytes): before_state = client.is_connected try: await client.write_gatt_char(char_uuid, data, response=True) print(f"写入成功: {data.hex()}") return True except Exception as e: print(f"写入失败: {data.hex()} 错误: {e}") return False

response=True表示写入时等待设备回复确认。这样写速度更慢,但能确保设备确实收到了数据。如果设备在收到超长数据时行为异常,通常是 MTU 协商没做好,很多低端 BLE 外设默认 MTU 是 23 字节,超过就被丢包或者断连。

3.4 基于 pytest 的自动化测试用例组织

所有上述功能最后都要嵌入到 pytest 框架中,这样才能形成完整的自动化测试闭环。利用 pytest 的 fixture 机制,把连接、服务发现这些重复性的前置逻辑统一管理起来。

import pytest @pytest.fixture(scope="module") async def ble_device(): # 前置:连接设备并完成服务发现 client = await connect_device("AA:BB:CC:DD:EE:FF") await enumerate_gatt(client) yield client # 后置:断开连接 await client.disconnect() @pytest.mark.asyncio async def test_read_sensitive_characteristic(ble_device): # 尝试读取设备关键配置特征值 value = await read_characteristic_ro(ble_device, "00002a00-0000-1000-8000-00805f9b34fb") assert value is not None, "敏感特征值读取失败"

scope="module"意味着同一个模块下的多个测试用例共享同一个连接,避免每个测试用例都重新扫描、连接,节省大量测试时间。这在 BLE 测试上尤其重要,因为反复的连接和断开操作既慢又容易触发设备端的连接次数限制。

上面的代码里用特征值读取来测试敏感特征值是否可读。如果设备对这些特征值做了访问控制,返回值里会带加密错误标志,断言应该要捕捉这个异常来验证保护机制。

3.5 抓包数据与自动化测试的联动

纯靠 API 层的自动化测试,只能看到“能不能读写成功”的结果。中间人攻击测试需要更底层的视角——看看空中链路的加密状态、看看关键数据包是否可以被伪造。

这套抓包联动的方案是由 nRF Sniffer 捕获空中报文,Wireshark 侧解析出逻辑链路控制和适配协议报文,然后框架里跑一个数据分析脚本算出特定数据的包序号。如果收到的配对请求数据包的 Seq 号与预期不一致,说明链路中可能存在重放,对应的测试用例标记为失败。

实现起来并不复杂,核心逻辑就是监听串口数据,把收到的抓包数据实时写入 Wireshark 的 extcap 管道,Wireshark 那边就能实时看见加密链路里的各种事件。后续对这些 pcap 文件跑的自动化分析代码是另一块重点内容,但思路就是上面说的联动方式。

4. 实战问题排查与调试记录

流程跑通是一回事,真正能稳定运行又是另一回事。我在这个项目里踩了不少坑,下面这几条是印象最深刻的。

4.1 MTU 协商失败导致数据截断

测试一个温湿度传感器时,发现读取长特征值时数据老是少一截。用 nRF Sniffer 抓包看,发现设备端只回复了部分数据。深入排查后确定是 MTU 协商的问题。这个传感器的 BLE 协议栈版本比较老,对 MTU 协商支持不完整,当中心设备发来大 MTU 请求时,它只回复了一个默认值,导致后续长数据被截断。

解决办法是在连接后主动设置一个保守的 MTU 值,不要在应用层发满载荷:

await client.write_gatt_char(char_uuid, data, response=True) # 主动设置期望的 MTU,但不能超过设备端支持的最大值 mtu = await client.exchange_mtu(128)

这个问题的通用教训是:BLE 开发测试中,遇到数据读取不完整,第一反应应该看 MTU 而不是怀疑自己的代码。很多设备默认 MTU 只有 23 字节,这意味着单次通知最多只能承载 20 字节有效数据。超出部分要么被拆分,要么被直接丢弃。

4.2 设备连接数限制导致自动化测试不稳定

有些蓝牙设备默认只允许一路连接。测试框架反复连接、断开、重连,如果上一次连接没有被设备端及时清理干净,下一次扫描可能就连接不上了。尤其是在 Windows 10 自带的蓝牙协议栈上表现明显,系统缓存不清理的话,连接状态会一直显示“已连接”,实际上链路早断了。

这个问题的规避方法有两个层面。框架层面,每次断开连接后加一个强制延迟,等设备端链路状态清理完成:

async def safe_disconnect(client): await client.disconnect() await asyncio.sleep(5) # 等待设备端清理连接状态

设备层面,如果是自己开发的设备,最好在协议设计时明确处理连接异常断开的情况,设定一个超时时间,超过后强制释放资源。

4.3 Windows 蓝牙适配器兼容性问题

bleak 在 Windows 上跑的时候,扫不到设备或者扫到了连接不上是家常便饭。这个问题大半是 Windows 蓝牙驱动的锅。质量参差不齐的 USB 蓝牙适配器很容易触发这个问题,但我发现用 CSR 4.0 芯片的老款蓝牙适配器反而非常稳定。

如果遇到扫描不稳定,可以尝试禁用再启用蓝牙适配器,或者更换一个适配器。跑自动化测试的话,强烈建议用有线连接的 USB 蓝牙适配器,不要依赖笔记本电脑的内置蓝牙,内置蓝牙常常省电模式导致设备响应延迟高。

4.4 自动化测试用例超时控制

BLE 通信是高延迟链路,必须给所有调用设置合理的超时时间。但超时时间也不能设置得太激进,否则正常链路波动就会导致测试误判失败。实践下来,扫描阶段超时设置在 10-15 秒比较合理,连接阶段在 20-30 秒,读写操作根据数据量大小设置在 5-10 秒。

在 pytest 中可以通过自定义超时装饰器来实现统一的超时管理:

import functools import asyncio def with_timeout(timeout): def decorator(func): @functools.wraps(func) async def wrapper(*args, **kwargs): return await asyncio.wait_for(func(*args, **kwargs), timeout=timeout) return wrapper return decorator

用这个装饰器包住所有测试函数,就可以用统一的超时策略来避免测试用例卡死。最终测试报告会把超时标记为失败,而不是笼统地标记为“无响应”。

5. 常见问题速查与避坑指南

把到目前为止积累的经验汇总一下,做成一个速查表,方便测试时快速排查。

问题表现可能原因排查思路
扫描不到目标设备广播间隔太长或设备进入休眠检查广播参数,降低广播间隔到 100ms 以内
连接经常失败设备连接数已满重启设备,或等待连接超时释放资源
读取数据不完整MTU 协商失败抓包确认协商结果,手动设置保守 MTU
写入超时特征值属性不支持写查看 GATT 属性列表,确认是否只有 write_no_response
通知事件不触发未使能 CCCD写入 0x0001 到特征值的 Client Characteristic Configuration Descriptor
测试过程设备死机设备对异常数据无容错代码审查设备协议栈,增加数据长度和内容校验

格外提醒一点,关于 CCCD 的使能问题。很多人初次写 BLE 测试时,会订阅通知却发现一直收不到设备主动上报的数据。这里的关键在于,光订阅特征值还不够,得往对应的描述符(0x2902)里写入 0x0001,通知功能才会真正打开。这也是 BLE 开发测试中最高频的失误点之一,没有之一。

还有一个小技巧,很多设备的敏感指令并不是明文写在协议文档里的。在做劫持安全性测试时可以把所有特征值的 UUID、可写属性都整理出来,然后用框架自动遍历一遍,尝试写入各种常见的控制指令格式,比如01,FF,00 01这类。很多设备对非法指令的处理会暴露它的内部状态。

6. 框架扩展方向与真实体会

这套框架目前处理的主要是单设备、单链路的通信劫持场景。而在真实的物联网环境里,一个测试可能同时面临多个设备、多种通信制式交错的复杂情况,所以框架后续可以考虑扩展自动化设备组管理的能力,通过测试配置模板来批量化地跑整个环境的设备安全回归测试。

另一个明显的扩展方向是集成模糊测试。自动化测试框架做的不只是验证功能是否满足,还可以通过随机化输入数据来测试设备的健壮性。把模糊数据生成器集成到框架中来,让它自动生成一批边界值、随机值、畸形数据包,写入设备特征值,观察设备是否会出现崩溃、重启或者数据损坏。这个对智能门锁、医疗设备这类对稳定性要求高的产品特别有价值。

目前这套框架在我这边已经跑了一段时间,用的最多的场景还是新固件发布前的通信安全回归测试。每次固件更新后跑一遍全量测试,看看有没有引入新的安全隐患。实际使用中我发现,安全测试最大的意义不在于找到多少漏洞,而在于让整个开发团队建立起一种“链路不安全是常态”的意识。当你习惯性地把自己写的 BLE 设备当作攻击目标来测试时,设计阶段的防御措施自然就做得更扎实了。

最后想分享的一点经验:测试框架本身的重心不在代码,而在对协议的理解。你越清楚 BLE 在链路层、属性层各有什么风险点,你的自动化用例就会越有针对性。磨刀不误砍柴工,协议啃透了,框架里每一行代码都会特别顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询