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

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

總覽

使用者經常看到應用程式執行功能,可讓使用者 轉移資料,或透過射頻 (RF) 與其他裝置互動 進行通訊或有線連線在 Android 中,最常用於這項用途的技術是經典藍牙 (Bluetooth BR/EDR)、藍牙低功耗 (BLE)、Wifi P2P、NFC 和 USB。

這些技術通常應用於預期 與智慧型住宅配件、健康監測裝置、公共服務進行通訊 交通站、感應式刷卡機和其他 Android 裝置

和其他通訊管道一樣,機器對機器通訊也容易遭受攻擊,攻擊者會試圖破壞兩個或多個裝置之間建立的信任界線。惡意使用者可利用裝置冒用等手法,對通訊管道發動大量攻擊。

Android 打造了用於設定機器對機器的特定 API 提供給開發人員的通訊內容

實作通訊時,請謹慎將這些 API 當做錯誤使用 通訊協定可能導致使用者或裝置資料在未經授權的情況下遭到揭露 使用的資料在最糟的情況下,攻擊者可能會從遠端接管一或多部裝置,進而取得裝置上所有內容的完整存取權。

影響

影響可能因採用 應用程式

機器對機器的用法或設定有誤 通訊管道可能會讓使用者裝置暴露在不受信任的位置 通訊嘗試。這可能會導致裝置容易受到 其他攻擊,如中間人 (MiTM)、命令注射、DoS 或 模擬攻擊

風險:透過無線頻道竊取機密資料

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

因應措施

如果應用程式確實需要透過無線管道進行機器對機器的機密資料交換,則應在應用程式程式碼中實作應用程式層安全性解決方案,例如加密。這可以防止 攻擊者之所以能在通訊管道上窺探 以純文字格式交換資料如需其他資源,請參閱 密碼學說明文件。


風險:無線惡意資料注入

無線機器對機器通訊管道 (經典藍牙、BLE、NFC、 Wifi 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 設為 0 只要應用程式執行 在背景或前景中運作至於傳統藍牙規格,可偵測的裝置會持續廣播特定發現 訊息,允許其他裝置擷取或連線至該裝置資料。在這種情況下,惡意第三方可以攔截這類訊息,並連線至 Android 裝置。連線後,攻擊者就能執行其他攻擊,例如資料竊取、DoS 或指令注入。

因應措施

EXTRA_DISCOVERABLE_DURATION 不得設為零。如果 未設定 EXTRA_DISCOVERABLE_DURATION 參數,Android 預設會 2 分鐘您可以設定的最大值 EXTRA_DISCOVERABLE_DURATION 參數為 2 小時 (7200 秒)。是 建議將探索時間長度保持在最短的時間 根據應用程式用途 來提供與支援選項


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

惡意應用程式可以註冊意圖篩選器,讀取特定 NFC 標記或支援 NFC 的裝置。這些篩選器可複製合法應用程式定義的篩選器,讓攻擊者能夠讀取交換的 NFC 資料內容。請注意,如果兩個活動為特定 NFC 標記指定相同的意圖篩選器,系統會顯示活動選擇器,因此使用者仍需選擇惡意應用程式才能成功發動攻擊。不過,使用 套用偽裝的意圖篩選器,但這種情況仍有可能。這種攻擊是 只有在系統可考慮透過 NFC 交換的資料的情況下,才具有重要意義 極度敏感

因應措施

在應用程式中實作 NFC 讀取功能時,可以搭配使用意圖篩選器和 Android 應用程式記錄 (AAR)。將 AAR 記錄嵌入 NDEF 訊息中,可確保只啟動合法應用程式及其相關的 NDEF 處理活動。這麼做可防止不必要的應用程式或活動讀取透過 NFC 交換的高度敏感標記或裝置資料。


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

Android 裝置收到來自 NFC 標記或支援 NFC 的資料時 裝置時,系統會自動觸發應用程式或特定的應用程式 設定處理當中 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;
    }

資源