BLE设备通信劫持自动化测试框架:从原理到代码实现

发布时间:2026/10/1 4:53:27
BLE设备通信劫持自动化测试框架:从原理到代码实现 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 和外设之间插入一个伪造的中心设备 MM 替代真实外设的 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 上确实很难装所以我推荐用 bleakpip 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_time10, name_filterNone): devices await BleakScanner.discover(scan_time, return_advTrue) 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_advTrue这个参数的用法。它是把广播数据一并返回避免再走一遍系统 API 获取完整广播信息。获取到的manufacturer_data解析出来之后会得到一个字典key 是厂商 IDvalue 是厂商自定义的数据段这个在识别特定设备的时候非常有用。连接封装则要注意 BLE 连接的超时和重试机制。Windows 下的 bleak 连接偶尔会莫名其妙超时重启蓝牙驱动才能恢复所以框架里加了重试逻辑和最长连接等待时间限制。from bleak import BleakClient async def connect_device(address, max_retries3): for attempt in range(max_retries): try: client BleakClient(address, timeout20.0) await client.connect() return client except Exception as e: print(f连接第 {attempt1} 次失败: {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, responseTrue) print(f写入成功: {data.hex()}) return True except Exception as e: print(f写入失败: {data.hex()} 错误: {e}) return FalseresponseTrue表示写入时等待设备回复确认。这样写速度更慢但能确保设备确实收到了数据。如果设备在收到超长数据时行为异常通常是 MTU 协商没做好很多低端 BLE 外设默认 MTU 是 23 字节超过就被丢包或者断连。3.4 基于 pytest 的自动化测试用例组织所有上述功能最后都要嵌入到 pytest 框架中这样才能形成完整的自动化测试闭环。利用 pytest 的 fixture 机制把连接、服务发现这些重复性的前置逻辑统一管理起来。import pytest pytest.fixture(scopemodule) 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, 敏感特征值读取失败scopemodule意味着同一个模块下的多个测试用例共享同一个连接避免每个测试用例都重新扫描、连接节省大量测试时间。这在 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, responseTrue) # 主动设置期望的 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), timeouttimeout) 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 在链路层、属性层各有什么风险点你的自动化用例就会越有针对性。磨刀不误砍柴工协议啃透了框架里每一行代码都会特别顺。