26、蓝牙安全攻防:BlueBorne漏洞分析、蓝牙中间人攻击(MITM)、安全编码规范

蓝牙这东西,用起来方便,但安全坑是真不少。我做了这么多年Android开发,早期对蓝牙安全其实也没太当回事——直到有一次在客户现场,他们的蓝牙门禁系统被攻破了,整个办公室的门锁全被远程打开。嗯,从那以后,我再也不敢小看蓝牙安全了。

今天咱们就聊聊蓝牙安全攻防。我会从三个角度切入:先看看著名的BlueBorne漏洞到底是怎么回事,再讲讲蓝牙中间人攻击(MITM)的原理,最后给出实际开发中必须遵守的安全编码规范。说白了,就是让你知道黑客怎么搞你,以及你怎么防。

核心观点:蓝牙安全不是可选项,而是必选项。尤其是做IoT、支付、门禁类应用,安全漏洞就是你的命门。

蓝牙安全攻防知识体系 蓝牙安全攻防 BlueBorne漏洞 远程代码执行 · 无需配对 影响Android 4.0-9.0 中间人攻击(MITM) 伪造设备 · 劫持通信 Just Works配对最危险 安全编码规范 输入校验 · 加密存储 绑定验证 · 最小权限 防御 = 理解攻击原理 + 严格执行安全规范

一、BlueBorne漏洞:蓝牙的"心脏出血"

BlueBorne是2017年由Armis Labs披露的一组蓝牙漏洞,总共8个CVE。为什么它这么出名?因为它不需要用户交互,不需要配对,甚至不需要蓝牙处于可发现模式——只要蓝牙开着,攻击者就能远程执行代码。

我记得当时看到这个漏洞报告时,后背一阵发凉。你想想看,你的手机蓝牙一直开着,耳机、手表、车载系统都在用。结果黑客在几米外就能直接控制你的手机,这谁顶得住?

核心漏洞一览

CVE编号 漏洞类型 影响范围 严重程度
CVE-2017-0781 远程代码执行 Android 4.0-9.0 严重
CVE-2017-0782 远程代码执行 Android 4.0-9.0 严重
CVE-2017-0785 信息泄露 Android 4.0-9.0 高危
CVE-2017-8628 远程代码执行 Windows 严重

说白了,BlueBorne利用了蓝牙协议栈中的内存损坏漏洞。攻击者发送精心构造的蓝牙数据包,触发缓冲区溢出,然后就能在目标设备上执行任意代码。整个过程不需要用户点任何按钮,甚至不需要知道设备地址。

注意:即使你的蓝牙设置为"不可发现",BlueBorne攻击仍然有效。因为攻击者可以通过暴力扫描蓝牙MAC地址(前24位是厂商固定,后24位只有1677万种组合),或者监听蓝牙广播包来发现设备。

我在项目中遇到过类似的问题。有一次做蓝牙门禁系统,测试人员发现只要发送一个超长的设备名称字符串,App就会崩溃。虽然这不是BlueBorne级别的漏洞,但原理是一样的——没有做输入校验。

二、蓝牙中间人攻击(MITM)

蓝牙MITM攻击,说白了就是黑客在通信双方之间插了一脚。你以为是跟门锁在通信,其实中间还隔了一个攻击者的设备。他转发你的指令,同时也能窃听甚至篡改数据。

为什么会这样?蓝牙配对过程中,如果使用"Just Works"方式(就是那种不需要输入PIN码,直接点确认的配对),密钥交换是不防MITM的。攻击者可以在配对阶段同时跟两个设备建立连接,然后中继所有数据。

MITM攻击的典型场景

  1. 伪造设备攻击:攻击者广播一个跟目标设备同名的蓝牙设备。用户不小心连上了假的,所有数据都被窃取。
  2. 配对劫持:在配对过程中,攻击者同时连接两个设备,分别跟它们完成配对,然后中继通信。
  3. 数据篡改:攻击者截获蓝牙数据包,修改内容后再转发。比如把"开门"指令改成"锁定"。

我的建议:如果你的应用涉及敏感操作(支付、门禁、身份认证),一定要使用"Passkey Entry"或"Numeric Comparison"配对方式。这两种方式在配对过程中会交换验证码,用户可以看到并确认,能有效防止MITM。

我曾经帮一个客户排查蓝牙门锁的问题。他们的门锁App在配对时用的是Just Works,结果用户反映有时候门锁会莫名其妙自己打开。后来一查,是攻击者在附近用树莓派做了MITM攻击,截获了开锁指令并重放。嗯,从那以后,我所有项目都强制要求使用带MITM保护的配对方式。

三、安全编码规范

好了,前面讲了攻击原理,现在说说怎么防。这部分是我个人最看重的,因为很多安全问题其实都是编码习惯不好导致的。

3.1 输入校验:第一道防线

你想想看,蓝牙通信中你会收到哪些数据?设备名称、服务UUID、特征值、描述符、原始字节流……这些数据都可能被攻击者恶意构造。所以,永远不要信任蓝牙对端发来的任何数据

// 错误示范:直接信任对端数据
String deviceName = device.getName(); // 可能包含恶意内容
byte[] data = characteristic.getValue(); // 可能超长或包含特殊字符

// 正确做法:严格校验
public static boolean isValidDeviceName(String name) {
    if (name == null || name.length() > 248) {
        return false; // 蓝牙规范中设备名最长248字节
    }
    // 只允许可打印ASCII字符
    return name.matches("^[\\x20-\\x7E]+$");
}

public static boolean isValidDataPacket(byte[] data, int maxLen) {
    if (data == null || data.length == 0 || data.length > maxLen) {
        return false;
    }
    // 检查数据头是否符合协议
    if (data[0] != EXPECTED_HEADER) {
        return false;
    }
    return true;
}

关键原则:校验长度、校验格式、校验范围。任何不符合预期的数据直接丢弃,不要尝试修复。

3.2 加密存储:别把密钥当明文

我在代码审查中见过太多人把蓝牙配对密钥、设备密码直接存到SharedPreferences里。你想想看,如果手机被root了,或者有人拿到了备份文件,这些密钥就全暴露了。

// 错误示范:明文存储
SharedPreferences prefs = getSharedPreferences("bluetooth", MODE_PRIVATE);
prefs.edit().putString("device_key", "123456").apply();

// 正确做法:使用Android Keystore加密存储
public static void storeSecureKey(Context context, String alias, byte[] key) {
    try {
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        
        // 生成或获取密钥
        KeyGenerator keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
        keyGenerator.init(new KeyGenParameterSpec.Builder(alias,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .build());
        SecretKey secretKey = keyGenerator.generateKey();
        
        // 加密数据
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedData = cipher.doFinal(key);
        
        // 存储加密后的数据和IV
        SharedPreferences prefs = context.getSharedPreferences("secure", MODE_PRIVATE);
        prefs.edit()
            .putString(alias + "_data", Base64.encodeToString(encryptedData, Base64.NO_WRAP))
            .putString(alias + "_iv", Base64.encodeToString(cipher.getIV(), Base64.NO_WRAP))
            .apply();
    } catch (Exception e) {
        Log.e("SecureStorage", "存储密钥失败", e);
    }
}

注意:Android Keystore从Android 6.0开始支持硬件级加密。如果你的App需要兼容更低版本,可以使用Facebook的Conceal库或者自己实现基于用户密码的加密方案。

3.3 绑定验证:确认对方身份

蓝牙配对只是建立了加密通道,但并没有验证对端设备的身份。如果你的App需要跟特定的设备通信(比如你自己的蓝牙门锁),一定要做应用层的绑定验证。

// 绑定验证示例:使用设备MAC地址+预共享密钥
public static boolean verifyDevice(BluetoothDevice device, String expectedMac, byte[] sharedSecret) {
    // 1. 验证MAC地址
    if (!device.getAddress().equalsIgnoreCase(expectedMac)) {
        return false;
    }
    
    // 2. 发送挑战-响应验证
    byte[] challenge = generateRandomChallenge();
    sendChallenge(device, challenge);
    
    // 3. 等待响应并验证
    byte[] response = receiveResponse(device);
    byte[] expectedResponse = computeResponse(challenge, sharedSecret);
    
    return Arrays.equals(response, expectedResponse);
}

3.4 最小权限原则

蓝牙权限不要滥用。如果你的App只需要扫描设备,就不要申请BLUETOOTH_CONNECT权限。Android 12开始,蓝牙权限管理更加严格,一定要按需申请。

权限 用途 建议
BLUETOOTH_SCAN 扫描蓝牙设备 仅扫描时申请
BLUETOOTH_CONNECT 连接蓝牙设备 仅连接时申请
BLUETOOTH_ADVERTISE 广播蓝牙服务 仅作为服务端时申请
ACCESS_FINE_LOCATION 蓝牙扫描(旧版) Android 10以下需要

个人经验:我习惯在代码中动态检查权限,而不是在Manifest里一股脑全写上。这样用户能看到你的App为什么需要这个权限,也更愿意授权。

四、总结

蓝牙安全说白了就是三件事:知道别人怎么攻击你、在代码里堵住漏洞、用加密保护敏感数据。BlueBorne告诉我们,协议栈的漏洞是真实存在的,及时更新系统补丁很重要。MITM攻击提醒我们,配对方式的选择直接决定了通信的安全性。而安全编码规范,则是每个开发者都应该刻在脑子里的基本功。

我曾经在一个项目里因为没做输入校验,导致蓝牙设备被恶意数据包搞死机。那次教训让我明白:安全不是锦上添花,而是底线。希望今天的分享能帮你少踩一些坑。


公众号:蓝海资料掘金营,微信deep3321