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

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

概要

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

これらの技術は通常、スマートホーム アクセサリ、健康管理デバイス、公共交通機関のキオスク、決済端末、その他の Android 搭載デバイスと通信することが想定されるアプリに実装されます。

他のチャネルと同様に、マシンツーマシン通信は、複数のデバイス間で確立された信頼境界を侵害することを目的とした攻撃を受けやすくなります。デバイスのなりすましなどの手法は、 悪意のあるユーザーが通信に対してさまざまな攻撃を仕掛けてくる 。

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

通信プロトコルの実装中にエラーが発生すると、ユーザーデータやデバイスデータが不正な第三者に漏洩する可能性があるため、これらの API は慎重に使用する必要があります。最悪のシナリオの場合、攻撃者は 1 台以上のデバイスを乗っ取り、コンテンツへの完全アクセス権を取得する 確認できます。

影響

影響は、アプリに実装されているデバイス間通信技術によって異なる場合があります。

マシンツーマシンの使用や構成の誤り 通信チャネルにより、ユーザーのデバイスが信頼できない 通信の試行回数などです。これにより、デバイスが 中間者(MiTM)、コマンド インジェクション、DoS、 無償ツールキットで

リスク: ワイヤレス チャネルを介した機密データの盗聴

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

リスクの軽減

アプリでワイヤレス チャネルを介した機械間機密データの交換が必要な場合は、暗号化などのアプリケーション レイヤのセキュリティ ソリューションをアプリのコードに実装する必要があります。これにより、 攻撃者が通信チャネルを傍受して 平文でデータを交換します。その他のリソースについては、 暗号化のドキュメントをご覧ください。


リスク: 悪意のあるワイヤレス データ インジェクション

ワイヤレスのマシン間通信チャネル(従来の Bluetooth、BLE、NFC、Wi-Fi P2P)は、悪意のあるデータを使用して改ざんされる可能性があります。十分なスキルを持つ攻撃者は、使用されている通信プロトコルを特定し、データ交換フローを改ざんできます。たとえば、エンドポイントのなりすましや、特別に作成されたペイロードの送信などです。この種の悪意のあるトラフィックは、 アプリケーションの機能にアクセスできず、最悪のケースでは 動作や、DoS 攻撃、コマンド実行などの攻撃に インジェクション、デバイスの乗っ取りなどが考えられます

リスクの軽減

Android には、クラシック Bluetooth、BLE、NFC、Wifi P2P などのマシン間通信を管理するための強力な API が用意されています。これらは、慎重に実装されたデータ検証ロジックと組み合わせて、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 経由で送信されたデータをフィルタし、長さ、形式、内容を確認 一致している必要がありますたとえば、心拍数モニターがキーストローク コマンドを送信できないようにする必要があります。

また 可能であれば、ルールに含まれる URL を アプリが 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 パラメータは設定されていません。デフォルトでは、 そのデバイスが 2 分間検出可能になります。EXTRA_DISCOVERABLE_DURATION パラメータに設定できる最大値は 2 時間(7,200 秒)です。検出可能な時間は、アプリケーションのユースケースに応じて可能な限り短くすることをおすすめします。


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

悪意のあるアプリは、特定の NFC タグや NFC 対応デバイスを読み取るようにインテント フィルタを登録できます。これらのフィルタは、 コンテンツを読み取れるようにし、攻撃者がそのコンテンツを 返された NFC データを渡します。2 つのアクティビティが 特定の NFC タグに対して同じインテント フィルタを設定していない場合、アクティビティ選択ツールは そのため、ユーザーは悪意のあるファイルを選択して 攻撃を成功させるための時間です。ただし、インテント フィルタとクロールを組み合わせることで、このシナリオは引き続き可能です。この攻撃は、 NFC を介して交換されるデータが考慮できる場合にのみ重要です。 検出されます。

リスクの軽減

アプリケーション内に NFC 読み取り機能を実装する場合は、インテント フィルタを Android アプリケーション レコード(AAR)とともに使用できます。ML モデルの NDEF メッセージ内の AAR レコードによって、 それに関連する 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;
    }

リソース