不安全的機器對機器通訊設定

OWASP 類別:MASVS-CODE:程式碼品質

總覽

應用程式實作功能,讓使用者透過射頻 (RF) 通訊或有線連線傳輸資料或與其他裝置互動,這並不少見。Android 最常使用的技術包括傳統藍牙 (Bluetooth BR/EDR)、藍牙低功耗 (BLE)、Wi-Fi P2P、NFC 和 USB。

這些技術通常會實作在預期與智慧住宅配件、健康監控裝置、大眾運輸資訊站、刷卡機和其他 Android 裝置通訊的應用程式中。

與其他管道一樣,機器對機器的通訊也容易受到攻擊,導致兩個或多個裝置之間建立的信任界線遭到破壞。惡意使用者可能會利用裝置模擬等技術,對通訊管道發動大量攻擊。

Android 為開發人員提供特定 API,用於設定機器對機器通訊。

實作通訊協定時若發生錯誤,可能會導致使用者或裝置資料洩漏給未經授權的第三方,因此請務必謹慎使用這些 API。在最糟的情況下,攻擊者可能會從遠端接管一或多部裝置,進而完全存取裝置上的內容。

影響

影響程度可能因應用程式中實作的裝置對裝置技術而異。

如果機器對機器通訊管道的使用方式或設定有誤,使用者裝置可能會暴露在不受信任的通訊嘗試中。這可能會導致裝置容易遭受其他攻擊,例如中間人 (MiTM)、指令注入、阻斷服務 (DoS) 或模擬攻擊。

風險:透過無線通道竊聽敏感資料

導入機器對機器通訊機制時,應審慎考量所用技術和應傳輸的資料類型。雖然以有線連線執行這類工作實際上更安全,因為這類連線需要在相關裝置之間建立實體連結,但使用無線電頻率的通訊協定 (例如傳統藍牙、BLE、NFC 和 Wi-Fi P2P) 可能會遭到攔截。攻擊者可能會冒用資料交換中涉及的其中一個終端機或存取點,攔截無線通訊,進而存取敏感的使用者資料。此外,如果裝置上安裝的惡意應用程式取得通訊專用的執行階段權限,可能會讀取系統訊息緩衝區,進而擷取裝置間交換的資料。

因應措施

如果應用程式確實需要透過無線通道進行機密資料的機器對機器交換,則應在應用程式的程式碼中實作應用程式層級的安全解決方案,例如加密。這樣一來,攻擊者就無法監聽通訊管道,並以明文擷取交換的資料。如需其他資源,請參閱「密碼編譯」說明文件。


風險:無線惡意資料注入

無線機器對機器通訊管道 (傳統藍牙、藍牙低功耗、NFC、Wi-Fi P2P) 可能會遭到惡意資料竄改。如果攻擊者具備足夠的技能,就能識別使用的通訊協定並竄改資料交換流程,例如模擬其中一個端點,傳送精心設計的酬載。這類惡意流量可能會導致應用程式功能異常,最嚴重的情況是造成應用程式和裝置行為異常,或導致 DoS、指令注入或裝置遭接管等攻擊。

因應措施

Android 提供強大的 API,可供開發人員管理裝置間的通訊,例如傳統藍牙、BLE、NFC 和 Wi-Fi P2P。這些應與精心實作的資料驗證邏輯合併,以清除兩個裝置之間交換的任何資料。

這項解決方案應在應用程式層級實作,並包含檢查,確認資料是否具有預期長度、格式,以及是否包含可供應用程式解讀的有效酬載。

以下程式碼片段顯示資料驗證邏輯範例。這是透過 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 封包數量。這樣可防止惡意裝置發動橡皮鴨攻擊等攻擊。

如要完成這項驗證,請建立新執行緒來檢查緩衝區內容,例如在 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 裝置。連線後,攻擊者就能發動資料竊取、阻斷服務或指令注入等進一步攻擊。

因應措施

EXTRA_DISCOVERABLE_DURATION 絕不可設為零。如果未設定 EXTRA_DISCOVERABLE_DURATION 參數,Android 預設會讓裝置可供探索 2 分鐘。EXTRA_DISCOVERABLE_DURATION 參數可設定的最大值為 2 小時 (7200 秒)。建議根據應用程式用途,將可探索時間盡可能縮短。


風險:NFC - 複製的意圖篩選器

惡意應用程式可以註冊意圖篩選器,讀取特定 NFC 標記或支援 NFC 的裝置。這些篩選器可以複製合法應用程式定義的篩選器,讓攻擊者能夠讀取交換的 NFC 資料內容。請注意,如果兩個活動為特定 NFC 標記指定相同的意圖篩選器,系統會顯示「活動選擇器」,因此使用者仍須選擇惡意應用程式,攻擊才能成功。不過,如果將意圖篩選器與隱匿功能結合,仍有可能發生這種情況。只有在透過 NFC 交換的資料屬於高度機密時,這類攻擊才具有重大意義。

因應措施

在應用程式中實作 NFC 讀取功能時,意圖篩選器可與 Android 應用程式記錄 (AAR) 一起使用。在 NDEF 訊息中嵌入 AAR 記錄,可確保只有正當應用程式及其相關聯的 NDEF 處理活動會啟動。這樣一來,不需要的應用程式或活動就無法透過 NFC 讀取高度敏感的標記或裝置資料。


風險:NFC - 缺少 NDEF 訊息驗證

當 Android 裝置從 NFC 標記或支援 NFC 的裝置接收資料時,系統會自動觸發應用程式或特定 Activity,以處理內含的 NDEF 訊息。根據應用程式中實作的邏輯,標記中包含或從裝置收到的資料可提供給其他活動,以觸發進一步的動作,例如開啟網頁。

如果應用程式缺少 NDEF 訊息內容驗證機制,攻擊者可能會使用支援 NFC 的裝置或 NFC 標記,在應用程式中注入惡意酬載,導致應用程式出現異常行為,例如下載惡意檔案、注入指令或阻斷服務。

因應措施

將收到的 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;
    }

資源