不安全的机器到机器通信设置

OWASP 类别: MASVS-CODE:代码质量

概览

应用程序实现允许用户通过射频 (RF) 通信或有线连接传输数据或与其他设备交互的功能并不罕见。为此目的,Android 中最常用的技术是经典蓝牙 (Bluetooth BR/EDR)、低功耗蓝牙 (BLE)、Wifi P2P、NFC 和 USB。

这些技术通常用于需要与智能家居配件、健康监测设备、公共交通自助服务终端、支付终端以及其他 Android 设备通信的应用中。

与任何其他通道一样,机器到机器通信容易受到旨在破坏两个或多个设备之间建立的信任边界的攻击。恶意用户可以利用设备伪造等技术对通信通道发动多种攻击。

Android 为开发者提供了配置机器到机器通信的特定 API

这些 API 应谨慎使用,因为实现通信协议时出错可能导致用户或设备数据泄露给未经授权的第三方。在最糟糕的情况下,攻击者可能能够远程控制一个或多个设备,从而完全访问设备上的内容。

影响

影响可能因应用中实现的设备到设备技术而异。

机器到机器通信通道的不当使用或配置可能会导致用户设备面临不受信任的通信尝试。这可能使设备容易受到中间人攻击 (MiTM)、命令注入、DoS 或伪造攻击等额外攻击。

风险:通过无线通道窃听敏感数据

在实现机器到机器通信机制时,应仔细考虑所使用的技术以及应传输的数据类型。虽然有线连接在实践中对于此类任务更安全,因为它们需要在涉及的设备之间建立物理连接,但使用射频的通信协议(如经典蓝牙、BLE、NFC 和 Wifi P2P)可能会被拦截。攻击者可能能够伪装成数据交换中的终端或接入点之一,通过无线方式拦截通信,从而获取敏感用户数据。此外,如果设备上安装的恶意应用获得了通信特定的运行时权限,则可能通过读取系统消息缓冲区来检索设备之间交换的数据。

缓解措施

如果应用确实需要在无线通道上进行敏感数据的机器到机器交换,则应在应用代码中实现应用层安全解决方案,例如加密。这将阻止攻击者嗅探通信通道并以明文形式检索交换的数据。如需更多资源,请参阅加密技术文档。


风险:无线恶意数据注入

无线机器到机器通信通道(经典蓝牙、BLE、NFC、Wifi P2P)可能会受到恶意数据的篡改。技术娴熟的攻击者可以识别使用的通信协议并篡改数据交换流程,例如通过伪装成一个端点,发送专门构造的负载。这种恶意流量可能会降低应用功能,在最糟糕的情况下,导致应用和设备出现意外行为,或导致 DoS、命令注入或设备接管等攻击。

缓解措施

Android 为开发者提供了强大的 API,用于管理经典蓝牙、BLE、NFC 和 Wifi P2P 等机器到机器通信。这些 API 应结合精心实现的数据验证逻辑,以清理在两个设备之间交换的任何数据。

此解决方案应在应用层面实现,并应包含验证数据是否具有预期长度、格式以及是否包含可由应用解释的有效负载的检查。

以下代码段展示了示例数据验证逻辑。这是在 Android 开发者关于实现蓝牙数据传输的示例基础上实现的

Kotlin

class MyThread(private val mmInStream: InputStream, private val handler: Handler) : Thread() {

    private val mmBuffer = ByteArray(1024)
      override fun run() {
        while (true) {
            try {
                val numBytes = mmInStream.read(mmBuffer)
                if (numBytes > 0) {
                    val data = mmBuffer.copyOf(numBytes)
                    if (isValidBinaryData(data)) {
                        val readMsg = handler.obtainMessage(
                            MessageConstants.MESSAGE_READ, numBytes, -1, data
                        )
                        readMsg.sendToTarget()
                    } else {
                        Log.w(TAG, "Invalid data received: $data")
                    }
                }
            } catch (e: IOException) {
                Log.d(TAG, "Input stream was disconnected", e)
                break
            }
        }
    }

    private fun isValidBinaryData(data: ByteArray): Boolean {
        if (// Implement data validation rules here) {
            return false
        } else {
            // Data is in the expected format
            return true
        }
    }
}

Java

public void run() {
            mmBuffer = new byte[1024];
            int numBytes; // bytes returned from read()
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    // Read from the InputStream.
                    numBytes = mmInStream.read(mmBuffer);
                    if (numBytes > 0) {
                        // Handle raw data directly
                        byte[] data = Arrays.copyOf(mmBuffer, numBytes);
                        // Validate the data before sending it to the UI activity
                        if (isValidBinaryData(data)) {
                            // Data is valid, send it to the UI activity
                            Message readMsg = handler.obtainMessage(
                                    MessageConstants.MESSAGE_READ, numBytes, -1,
                                    data);
                            readMsg.sendToTarget();
                        } else {
                            // Data is invalid
                            Log.w(TAG, "Invalid data received: " + data);
                        }
                    }
                } catch (IOException e) {
                    Log.d(TAG, "Input stream was disconnected", e);
                    break;
                }
            }
        }

        private boolean isValidBinaryData(byte[] data) {
            if (// Implement data validation rules here) {
                return false;
            } else {
                // Data is in the expected format
                return true;
           }
    }

风险:USB 恶意数据注入

两个设备之间的 USB 连接可能成为恶意用户的目标,他们试图拦截通信。在这种情况下,所需的物理连接构成了一个额外的安全层,因为攻击者需要获取连接终端的线缆才能窃听任何消息。另一个攻击向量是插入设备中的不受信任的 USB 设备,无论是有意还是无意插入。

如果应用使用 PID/VID 过滤 USB 设备以触发特定的应用内功能,攻击者可能会通过伪装成合法设备来篡改通过 USB 通道发送的数据。此类攻击可能允许恶意用户向设备发送按键或执行应用活动,在最糟糕的情况下,可能导致远程代码执行或下载不需要的软件。

缓解措施

应实现应用级别的数据验证逻辑。该逻辑应过滤通过 USB 发送的数据,检查长度、格式和内容是否与应用用例匹配。例如,心率监测器不应能够发送按键命令。

此外,在可能的情况下,应考虑限制应用可以从 USB 设备接收的 USB 数据包数量。这可以防止恶意设备执行如 rubber ducky 等攻击。

例如,在进行 bulkTransfer 时,可以通过创建一个新线程来检查缓冲区内容以完成此验证

Kotlin

fun performBulkTransfer() {
    // Stores data received from a device to the host in a buffer
    val bytesTransferred = connection.bulkTransfer(endpointIn, buffer, buffer.size, 5000)

    if (bytesTransferred > 0) {
        if (//Checks against buffer content) {
            processValidData(buffer)
        } else {
            handleInvalidData()
        }
    } else {
        handleTransferError()
    }
}

Java

public void performBulkTransfer() {
        //Stores data received from a device to the host in a buffer
        int bytesTransferred = connection.bulkTransfer(endpointIn, buffer, buffer.length, 5000);
        if (bytesTransferred > 0) {
            if (//Checks against buffer content) {
                processValidData(buffer);
            } else {
                handleInvalidData();
            }
        } else {
            handleTransferError();
        }
    }

特定风险

本节收集了需要非标准缓解策略的风险,或者在特定 SDK 级别已得到缓解的风险,此处列出是为了完整性。

风险:蓝牙 – 不正确的可发现时间

正如Android 开发者蓝牙文档中所强调的,在应用内配置蓝牙接口时,使用 startActivityForResult(Intent, int) 方法启用设备可发现性并将 EXTRA_DISCOVERABLE_DURATION 设置为零将导致设备在应用在前台或后台运行时始终可被发现。根据经典蓝牙规范,可发现设备会不断广播特定的发现消息,允许其他设备检索设备数据或连接到设备。在这种情况下,恶意的第三方可以拦截此类消息并连接到 Android 设备。连接后,攻击者可以执行进一步的攻击,例如数据盗窃、DoS 或命令注入。

缓解措施

EXTRA_DISCOVERABLE_DURATION 不应设置为零。如果未设置 EXTRA_DISCOVERABLE_DURATION 参数,Android 默认会使设备可被发现 2 分钟。可以为 EXTRA_DISCOVERABLE_DURATION 参数设置的最大值为 2 小时(7200 秒)。建议根据应用用例将可发现持续时间保持在最短。


风险:NFC – 克隆的 intent-filter

恶意应用可以注册 intent-filter 以读取特定的 NFC 标签或支持 NFC 的设备。这些 filter 可以复制合法应用定义的 filter,使攻击者能够读取交换的 NFC 数据内容。值得注意的是,当两个 Activity 为特定 NFC 标签指定相同的 intent-filter 时,会呈现 Activity Chooser,因此用户仍然需要选择恶意应用才能使攻击成功。尽管如此,结合 intent-filter 和伪装,这种场景仍然可能发生。此攻击仅在通过 NFC 交换的数据可被视为高度敏感的情况下具有重要意义。

缓解措施

在应用中实现 NFC 读取功能时,intent-filter 可以与 Android 应用记录 (AAR) 一起使用。将 AAR 记录嵌入 NDEF 消息中将提供强有力的保证,确保只有合法应用及其相关的 NDEF 处理 Activity 启动。这将防止不需要的应用或 Activity 读取通过 NFC 交换的高度敏感的标签或设备数据。


风险:NFC – 缺少 NDEF 消息验证

当 Android 设备从 NFC 标签或支持 NFC 的设备接收数据时,系统会自动触发配置用于处理其中包含的 NDEF 消息的应用或特定 Activity。根据应用中实现的逻辑,标签中包含的数据或从设备接收的数据可以提供给其他 Activity,以触发进一步的操作,例如打开网页。

缺少 NDEF 消息内容验证的应用可能允许攻击者使用支持 NFC 的设备或 NFC 标签向应用注入恶意负载,导致意外行为,可能导致恶意文件下载、命令注入或 DoS。

缓解措施

在将接收到的 NDEF 消息分派给任何其他应用组件之前,应验证其中的数据是否为预期格式并包含预期信息。这可以避免恶意数据未经过滤地传递给其他应用组件,从而降低意外行为或使用被篡改的 NFC 数据进行攻击的风险。

以下代码段展示了作为方法实现的示例数据验证逻辑,该方法以 NDEF 消息作为参数及其在消息数组中的索引。这是在 Android 开发者关于从扫描的 NFC NDEF 标签获取数据的示例基础上实现的

Kotlin

//The method takes as input an element from the received NDEF messages array
fun isValidNDEFMessage(messages: Array<NdefMessage>, index: Int): Boolean {
    // Checks if the index is out of bounds
    if (index < 0 || index >= messages.size) {
        return false
    }
    val ndefMessage = messages[index]
    // Retrieves the record from the NDEF message
    for (record in ndefMessage.records) {
        // Checks if the TNF is TNF_ABSOLUTE_URI (0x03), if the Length Type is 1
        if (record.tnf == NdefRecord.TNF_ABSOLUTE_URI && record.type.size == 1) {
            // Loads payload in a byte array
            val payload = record.payload

            // Declares the Magic Number that should be matched inside the payload
            val gifMagicNumber = byteArrayOf(0x47, 0x49, 0x46, 0x38, 0x39, 0x61) // GIF89a

            // Checks the Payload for the Magic Number
            for (i in gifMagicNumber.indices) {
                if (payload[i] != gifMagicNumber[i]) {
                    return false
                }
            }
            // Checks that the Payload length is, at least, the length of the Magic Number + The Descriptor
            if (payload.size == 13) {
                return true
            }
        }
    }
    return false
}

Java

//The method takes as input an element from the received NDEF messages array
    public boolean isValidNDEFMessage(NdefMessage[] messages, int index) {
        //Checks if the index is out of bounds
        if (index < 0 || index >= messages.length) {
            return false;
        }
        NdefMessage ndefMessage = messages[index];
        //Retrieve the record from the NDEF message
        for (NdefRecord record : ndefMessage.getRecords()) {
            //Check if the TNF is TNF_ABSOLUTE_URI (0x03), if the Length Type is 1
            if ((record.getTnf() == NdefRecord.TNF_ABSOLUTE_URI) && (record.getType().length == 1)) {
                //Loads payload in a byte array
                byte[] payload = record.getPayload();
                //Declares the Magic Number that should be matched inside the payload
                byte[] gifMagicNumber = {0x47, 0x49, 0x46, 0x38, 0x39, 0x61}; // GIF89a
                //Checks the Payload for the Magic Number
                for (int i = 0; i < gifMagicNumber.length; i++) {
                    if (payload[i] != gifMagicNumber[i]) {
                        return false;
                    }
                }
                //Checks that the Payload length is, at least, the length of the Magic Number + The Descriptor
                if (payload.length == 13) {
                    return true;
                }
            }
        }
        return false;
    }

资源