16、BLE重放攻击:数据包捕获与重放、时间戳防御机制、序列号防御机制、挑战-响应认证
各位同学,今天我们来聊一个在蓝牙安全里非常经典,也特别容易被忽视的问题——重放攻击。
说白了,重放攻击就是攻击者把之前合法设备发过的数据包,原封不动地再发一遍。你想想看,如果接收方没有做任何防重放处理,它就会以为这是一条新的合法指令,然后乖乖执行。我在项目中就遇到过这样的案例:一个智能门锁,攻击者抓到了开锁指令的数据包,然后反复重放,门就开了。嗯,这很可怕。
那么,BLE里怎么防重放?主要有三种手段:时间戳、序列号、挑战-响应认证。我们一个一个来看。
16.1 数据包捕获与重放:攻击者是怎么做到的?
先说说攻击者怎么捕获数据包。BLE通信是无线广播的,任何在信号范围内的设备都能收到。攻击者只需要一个支持BLE嗅探的硬件,比如nRF52840 DK或者Ubertooth One,配合Wireshark或者btlejack这类工具,就能把空中的数据包抓下来。
我个人的习惯是,在做安全测试时,先用btlejack扫描一下周围设备的BLE广播和连接数据。你会发现,很多设备的数据包结构非常简单,甚至明文传输。比如下面这个例子:
# 使用btlejack捕获BLE数据包
btlejack -f -c 37,38,39 -o capture.pcap
# 然后用Wireshark打开分析
wireshark capture.pcap
攻击者抓到数据包后,重放就更简单了。直接用btlejack的replay功能,或者自己写个脚本用hcitool发出去。比如:
# 重放一个捕获到的连接请求包
btlejack -r captured_packet.bin -c 38
你想想看,如果这个包是开锁指令、支付确认、或者医疗设备的给药指令,后果有多严重?
16.2 时间戳防御机制:给数据包打上时间烙印
时间戳防御,说白了就是在每个数据包里加上当前的时间。接收方收到后,检查这个时间戳是否在允许的误差范围内。如果时间差太大,就认为是重放攻击,直接丢弃。
举个例子,假设设备A给设备B发指令,数据包里带上当前Unix时间戳:
{
"command": "unlock",
"timestamp": 1712345678
}
设备B收到后,检查自己的当前时间与这个时间戳的差值。如果差值小于5秒,就认为是合法指令;否则丢弃。
但这里有个坑——时间同步。两个设备的时间必须基本一致,否则会误判。我在项目中遇到过,一个智能手表和手机的时间差了10秒,结果所有指令都被当成重放丢弃了。后来我们加了一个时间同步协议,每次连接时先校准时间。
16.3 序列号防御机制:递增的计数器
序列号防御,就是通信双方维护一个递增的计数器。每次发送数据包,序列号加1。接收方只接受序列号比上一次大的数据包。如果收到重复的序列号,直接丢弃。
这个机制在BLE的LL层(链路层)就已经实现了。每个连接事件都有一个递增的connEventCounter。但问题是,这个计数器只在连接期间有效。如果连接断开重连,计数器会重置。攻击者可以在重连后重放之前的数据包。
所以,应用层也需要自己维护序列号。比如:
// 发送方
uint32_t seq_num = 0;
void send_secure_command(uint8_t *cmd, uint32_t len) {
uint8_t packet[len + 4];
packet[0] = (seq_num >> 24) & 0xFF;
packet[1] = (seq_num >> 16) & 0xFF;
packet[2] = (seq_num >> 8) & 0xFF;
packet[3] = seq_num & 0xFF;
memcpy(packet + 4, cmd, len);
// 发送packet
seq_num++;
}
// 接收方
uint32_t last_seq = 0xFFFFFFFF; // 初始化为最大值
bool is_valid_packet(uint8_t *packet) {
uint32_t seq = (packet[0] << 24) | (packet[1] << 16) | (packet[2] << 8) | packet[3];
if (seq <= last_seq) {
return false; // 重放或乱序
}
last_seq = seq;
return true;
}
嗯,这里要注意:序列号不能回绕得太快。如果序列号是32位的,每秒发1000个包,也要136年才回绕一次,基本够用了。但如果是8位的,256个包就回绕了,攻击者很容易利用回绕来重放。
16.4 挑战-响应认证:双向验证的终极方案
时间戳和序列号都有各自的局限性。时间戳依赖时钟同步,序列号依赖持久化存储。有没有更通用的方案?有——挑战-响应认证。
这个机制的原理很简单:
- 接收方(比如门锁)生成一个随机数,称为“挑战”(Challenge),发给发送方(比如手机)。
- 发送方用共享密钥对这个挑战进行加密或签名,生成“响应”(Response),发回给接收方。
- 接收方用同样的密钥验证响应是否正确。如果正确,说明发送方是合法的,并且这个响应只针对本次挑战,无法重放。
用流程图来表示就是:
你看,这个流程里,每次的挑战都是随机生成的,所以即使攻击者抓到了某次通信的响应,下次挑战变了,这个响应就无效了。这就是挑战-响应能防重放的根本原因。
在BLE里,挑战-响应认证通常用在配对过程中。比如BLE 4.2的LE Secure Connections就用了类似机制。但应用层也可以自己实现,比如:
// 门锁端:生成挑战并验证
void handle_challenge_request(uint8_t *challenge, uint8_t *response) {
// 生成16字节随机挑战
uint8_t challenge[16];
generate_random(challenge, 16);
// 发送挑战给手机
send_to_phone(challenge, 16);
// 等待手机返回响应
uint8_t response[16];
recv_from_phone(response, 16);
// 用共享密钥计算期望的响应
uint8_t expected[16];
hmac_sha256(shared_key, challenge, 16, expected, 16);
// 比较
if (memcmp(response, expected, 16) == 0) {
// 认证通过
unlock_door();
} else {
// 认证失败
reject();
}
}
16.5 三种防御机制的对比
最后,我用一个表格来总结这三种机制的优缺点:
| 防御机制 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 时间戳 | 数据包携带时间,接收方检查时间差 | 实现简单,无需额外交互 | 需要时钟同步,误差窗口内可被重放 | 实时性要求不高的指令(门锁、灯控) |
| 序列号 | 递增计数器,接收方只接受更大的序列号 | 无需时钟同步,实现简单 | 需要持久化存储,重启后可能重置 | 连接稳定的数据流(传感器、音频) |
| 挑战-响应 | 接收方发随机挑战,发送方用密钥计算响应 | 最安全,不依赖时钟和持久化 | 需要额外交互,增加延迟 | 配对、认证、高安全场景 |
我个人建议,在实际项目中,不要只用一种机制。比如,可以用挑战-响应来做初始认证,认证通过后用序列号来保护后续的数据包。这样既保证了安全性,又兼顾了效率。
嗯,关于BLE重放攻击的防御,今天就讲到这里。记住一句话:任何没有防重放机制的BLE通信,都是不安全的。下次你在设计BLE应用时,一定要问自己一句:我的数据包能被重放吗?