安全でないマシンツーマシン通信の設定

OWASP カテゴリ: MASVS-CODE: コード品質

概要

無線周波数(RF)通信またはケーブル接続を使用して、ユーザーがデータを転送したり、他のデバイスとやり取りしたりできる機能を実装したアプリケーションは珍しくありません。Android でこの目的で使用される最も一般的なテクノロジーは、クラシック Bluetooth(Bluetooth BR/EDR)、Bluetooth Low Energy(BLE)、Wi-Fi P2P、NFC、USB です。

これらのテクノロジーは通常、スマートホーム アクセサリ、健康モニタリング デバイス、公共交通機関のキオスク、決済端末、その他の Android デバイスと通信することが想定されるアプリケーションに実装されています。

他のチャネルと同様に、マシン間通信も、2 つ以上のデバイス間に確立された信頼境界を侵害することを目的とした攻撃を受けやすくなっています。デバイスのなりすましなどの手法を悪意のあるユーザーが利用して、通信チャネルに対してさまざまな攻撃を行う可能性があります。

Android では、マシン間 通信を構成するための特定の API をデベロッパーが利用できます。

通信プロトコルの実装中にエラーが発生すると、ユーザーデータやデバイスデータが不正な第三者に公開される可能性があるため、これらの API は慎重に使用する必要があります。最悪の場合、攻撃者は 1 つ以上のデバイスをリモートで制御し、デバイス上のコンテンツに完全にアクセスできるようになる可能性があります。

影響

影響は、アプリケーションに実装されているデバイス間テクノロジーによって異なります。

マシン間通信チャネルの使用方法や構成が誤っていると、ユーザー デバイスが信頼できない通信試行にさらされる可能性があります。これにより、デバイスが中間者(MiTM)攻撃、コマンド インジェクション、DoS、なりすまし攻撃などの追加の攻撃に対して脆弱になる可能性があります。

リスク: 無線チャネル経由でのセンシティブ データの盗聴

マシン間通信メカニズムを実装する場合は、使用するテクノロジーと送信するデータの種類を慎重に検討する必要があります。ケーブル接続は、関連するデバイス間の物理リンクが必要なため、実際にはこのようなタスクに対してより安全ですが、クラシック Bluetooth、BLE、NFC、Wi-Fi P2P などの無線周波数を使用する通信プロトコルは傍受される可能性があります。攻撃者は、データ交換に関与する端末またはアクセス ポイントのいずれかになりすまして、無線通信を傍受し、機密性の高いユーザーデータにアクセスできる可能性があります。また、デバイスにインストールされている悪意のあるアプリケーションが、 通信固有のランタイム権限を付与されている場合、システム メッセージバッファを読み取ることでデバイス間で交換されたデータを 取得できる可能性があります。

リスクの軽減

アプリケーションでワイヤレス チャネル経由でセンシティブ データをマシン間で交換する必要がある場合は、暗号化などのアプリケーション レイヤのセキュリティ ソリューションをアプリケーションのコードに実装する必要があります。これにより、攻撃者が通信チャネルをスニッフィングして、交換されたデータを平文で取得することを防ぐことができます。その他のリソースについては、 暗号化のドキュメントをご覧ください。


リスク: 無線による悪意のあるデータの挿入

無線によるマシン間通信チャネル(クラシック Bluetooth、BLE、NFC、Wi-Fi P2P)は、悪意のあるデータを使用して改ざんされる可能性があります。十分なスキルを持つ攻撃者は、使用中の通信プロトコルを特定し、エンドポイントのいずれかになりすまして、特別に作成されたペイロードを送信するなど、データ交換フローを改ざんできます。このような悪意のあるトラフィックは、アプリケーションの機能を低下させ、最悪の場合、アプリケーションやデバイスの予期しない動作を引き起こしたり、DoS、コマンド インジェクション、デバイスの乗っ取りなどの攻撃につながる可能性があります。

リスクの軽減

Android には、強力な API を使用して クラシック Bluetooth、BLE、NFC、Wi-Fi P2P などのマシン間通信を管理するための機能が用意されています。これらを慎重に実装されたデータ検証ロジックと組み合わせて、2 つのデバイス間で交換されるデータをサニタイズする必要があります。

このソリューションはアプリケーション レベルで実装し、データの長さ、形式が想定どおりであるか、アプリケーションで解釈できる有効なペイロードが含まれているかを確認するチェックを含める必要があります。

次のスニペットは、データ検証ロジックの例を示しています。これは、Bluetooth データ 転送を実装するための 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 による悪意のあるデータの挿入

2 つのデバイス間の 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 レベルで軽減されたリスクをまとめています。また、完全を期すためにその他のリスクも挙げています。

リスク: Bluetooth - 検出可能時間が正しくない

Android デベロッパーの Bluetooth ドキュメントで説明されているように、アプリケーション内で Bluetooth インターフェースを構成する際に、startActivityForResult(Intent, int) メソッドを使用してデバイスの検出を有効にし、EXTRA_DISCOVERABLE_DURATION をゼロに設定すると、アプリケーションがバックグラウンドまたはフォアグラウンドで実行されている限り、デバイスが検出可能になります。クラシック Bluetooth 仕様では、検出可能なデバイスは、他のデバイスがデバイスデータを取得したり、接続したりできるように、特定の検出 メッセージを常にブロードキャストしています。このようなシナリオでは、悪意のある第三者がこのようなメッセージを傍受して、Android デバイスに接続する可能性があります。接続されると、攻撃者はデータ窃盗、DoS、コマンド インジェクションなどの攻撃を行う可能性があります。

リスクの軽減

EXTRA_DISCOVERABLE_DURATION をゼロに設定しないでください。EXTRA_DISCOVERABLE_DURATION パラメータが設定されていない場合、デフォルトでは、Android はデバイスを検出可能にする時間を 2 分に設定します。EXTRA_DISCOVERABLE_DURATION パラメータに設定できる最大値は 2 時間(7,200 秒)です。検出可能時間は、アプリケーションのユースケースに応じて、できるだけ短くすることをおすすめします。


リスク: NFC - クローン作成されたインテント フィルタ

悪意のあるアプリケーションは、特定の NFC タグまたは NFC 対応デバイスを読み取るためのインテント フィルタを登録できます。これらのフィルタは、正規のアプリケーションで定義されたフィルタを複製できるため、攻撃者は交換された NFC データのコンテンツを読み取ることができます。2 つのアクティビティが特定の NFC タグに 同じインテントフィルタを指定すると、選択ダイアログが 表示されるため、攻撃を成功させるには、ユーザーが悪意のある アプリケーションを選択する必要があります。ただし、インテント フィルタをクローキングと組み合わせると、このシナリオは可能です。この攻撃は、NFC 経由で交換されるデータが機密性が高いと見なされる場合にのみ重要です。

リスクの軽減

アプリケーション内で NFC 読み取り機能を実装する場合は、インテント フィルタ を Android アプリケーション レコード(AAR)とともに使用できます。AAR レコードを NDEF メッセージに埋め込むと、正規のアプリケーションとその関連する NDEF 処理アクティビティのみが起動されることが保証されます。 これにより、不要なアプリケーションやアクティビティが、NFC 経由で交換される機密性の高いタグデータやデバイスデータを読み取ることを防ぐことができます。


リスク: NFC - NDEF メッセージの検証がない

Android デバイスが NFC タグまたは NFC 対応 デバイスからデータを受信すると、システムは、含まれている NDEF メッセージを処理するように構成されたアプリケーションまたは特定のアクティビティを自動的にトリガーします。 アプリケーションに実装されているロジックに応じて、タグに含まれているデータやデバイスから受信したデータを他のアクティビティに提供して、ウェブページを開くなどのアクションをトリガーできます。

NDEF メッセージ コンテンツの検証がないアプリケーションでは、攻撃者が NFC 対応デバイスまたは NFC タグを使用してアプリケーション内に悪意のあるペイロードを挿入し、悪意のあるファイルのダウンロード、コマンド インジェクション、DoS などの予期しない動作を引き起こす可能性があります。

リスクの軽減

受信した NDEF メッセージを他のアプリケーション コンポーネントにディスパッチする前に、データが想定される形式であり、想定される情報が含まれていることを検証する必要があります。これにより、悪意のあるデータがフィルタリングされていない状態で他のアプリケーションのコンポーネントに渡されるのを防ぎ、改ざんされた NFC データを使用した予期しない動作や攻撃のリスクを軽減できます。

次のスニペットは、NDEF メッセージを引数として受け取るメソッドとして実装されたデータ検証ロジックの例と、メッセージ配列内のインデックスを示しています。 これは、スキャンした NFC NDEF タグからデータを取得するための Android デベロッパーのサンプルに基づいて実装されています。

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;
    }

リソース