Configuration de la communication non sécurisée entre machines

Catégorie OWASP : MASVS-CODE : qualité du code

Présentation

Il n'est pas rare de voir des applications qui implémentent des fonctionnalités permettant aux utilisateurs de transférer des données ou d'interagir avec d'autres appareils à l'aide de communications par radiofréquence (RF) ou de connexions câblées. Les technologies les plus courantes utilisées dans Android à cet effet sont le Bluetooth classique (Bluetooth BR/EDR), le Bluetooth à faible consommation d'énergie (BLE), le Wi-Fi P2P, le NFC et l'USB.

Ces technologies sont généralement implémentées dans des applications censées communiquer avec des accessoires pour la maison connectée, des appareils de surveillance de la santé, des bornes de transport en commun, des terminaux de paiement et d'autres appareils fonctionnant sous Android.

Comme tout autre canal, les communications de machine à machine sont susceptibles d'être la cible d'attaques visant à compromettre la limite de confiance établie entre deux appareils ou plus. Les utilisateurs malveillants peuvent utiliser des techniques telles que l'usurpation d'identité d'appareil pour lancer un grand nombre d'attaques contre le canal de communication.

Android met à la disposition des développeurs des API spécifiques pour configurer les communications de machine à machine.

Ces API doivent être utilisées avec précaution, car des erreurs lors de l'implémentation des protocoles de communication peuvent entraîner l'exposition des données utilisateur ou des données de l'appareil à des tiers non autorisés. Dans le pire des cas, les pirates informatiques peuvent prendre le contrôle à distance d'un ou de plusieurs appareils, ce qui leur donne un accès complet au contenu de l'appareil.

Impact

L'impact peut varier en fonction de la technologie d'appareil à appareil implémentée dans l'application.

Une mauvaise utilisation ou configuration des canaux de communication de machine à machine peut exposer l'appareil de l'utilisateur à des tentatives de communication non fiables. Cela peut rendre l'appareil vulnérable à d'autres attaques, telles que les attaques de l'homme du milieu (MiTM), par injection de commandes, par déni de service (DoS) ou par usurpation d'identité.

Risque : écoute clandestine des données sensibles sur les canaux sans fil

Lors de l'implémentation de mécanismes de communication de machine à machine, il convient d'examiner attentivement la technologie utilisée et le type de données à transmettre. Bien que les connexions filaires soient en pratique plus sécurisées pour de telles tâches, car elles nécessitent un lien physique entre les appareils concernés, les protocoles de communication utilisant des fréquences radio telles que le Bluetooth classique, le BLE, le NFC et le Wi-Fi P2P peuvent être interceptés. Un pirate informatique peut être en mesure d'usurper l'identité d'un des terminaux ou points d'accès impliqués dans l'échange de données, d'intercepter la communication par voie hertzienne et, par conséquent, d'accéder aux données utilisateur sensibles. De plus, les applications malveillantes installées sur l'appareil peuvent, si elles ont obtenu les autorisations d'exécution spécifiques à la communication, être en mesure de récupérer les données échangées entre les appareils en lisant les tampons de messages système.

Stratégies d'atténuation

Si l'application nécessite un échange de données sensibles de machine à machine sur des canaux sans fil, des solutions de sécurité au niveau de la couche application, telles que le chiffrement, doivent être implémentées dans le code de l'application. Cela empêchera les pirates informatiques d'espionner le canal de communication et de récupérer les données échangées en texte clair. Pour obtenir d'autres ressources, consultez la documentation sur la cryptographie.


Risque : injection de données malveillantes sans fil

Les canaux de communication sans fil entre machines (Bluetooth classique, BLE, NFC, Wi-Fi P2P) peuvent être falsifiés à l'aide de données malveillantes. Des pirates informatiques suffisamment compétents peuvent identifier le protocole de communication utilisé et falsifier le flux d'échange de données, par exemple en se faisant passer pour l'un des points de terminaison et en envoyant des charges utiles spécialement conçues. Ce type de trafic malveillant peut dégrader la fonctionnalité de l'application et, dans le pire des cas, entraîner un comportement inattendu de l'application et de l'appareil, ou entraîner des attaques telles que le déni de service, l'injection de commandes ou la prise de contrôle de l'appareil.

Stratégies d'atténuation

Android fournit aux développeurs de puissantes API pour gérer les communications de machine à machine telles que le Bluetooth classique, le BLE, le NFC et le Wi-Fi P2P. Elles doivent être combinées à une logique de validation des données soigneusement implémentée pour assainir toutes les données échangées entre deux appareils.

Cette solution doit être implémentée au niveau de l'application et doit inclure des vérifications pour s'assurer que les données ont la longueur et le format attendus, et qu'elles contiennent une charge utile valide pouvant être interprétée par l'application.

L'extrait suivant montre un exemple de logique de validation des données. Cela a été implémenté sur l'exemple des développeurs Android pour l'implémentation du transfert de données Bluetooth :

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

Risque : injection de données malveillantes via USB

Les connexions USB entre deux appareils peuvent être ciblées par un utilisateur malveillant souhaitant intercepter les communications. Dans ce cas, le lien physique requis constitue une couche de sécurité supplémentaire, car le pirate informatique doit accéder au câble qui relie les terminaux pour pouvoir écouter les messages. Les périphériques USB non fiables branchés sur l'appareil, intentionnellement ou non, représentent un autre vecteur d'attaque.

Si l'application filtre les périphériques USB à l'aide de PID/VID pour déclencher des fonctionnalités spécifiques dans l'application, les pirates informatiques peuvent être en mesure de falsifier les données envoyées sur le canal USB en se faisant passer pour l'appareil légitime. Les attaques de ce type peuvent permettre à des utilisateurs malveillants d'envoyer des frappes au clavier à l'appareil ou d'exécuter des activités d'application qui, dans le pire des cas, peuvent entraîner l'exécution de code à distance ou le téléchargement de logiciels indésirables.

Stratégies d'atténuation

Une logique de validation au niveau de l'application doit être implémentée. Cette logique doit filtrer les données envoyées via USB en vérifiant que la longueur, le format et le contenu correspondent au cas d'utilisation de l'application. Par exemple, un cardiofréquencemètre ne doit pas pouvoir envoyer de commandes de frappe.

De plus, dans la mesure du possible, il convient d'envisager de limiter le nombre de paquets USB que l'application peut recevoir de l'appareil USB. Cela empêche les appareils malveillants d'effectuer des attaques telles que l'attaque Rubber Ducky.

Cette validation peut être effectuée en créant un thread pour vérifier le contenu du tampon, par exemple lors d'un 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();
        }
    }

Risques spécifiques

Cette section présente les risques qui nécessitent des stratégies d'atténuation non standards ou qui ont été atténués à certains niveaux du SDK et qui sont ici complets.

Risque : Bluetooth – durée de visibilité incorrecte

Comme indiqué dans la documentation Bluetooth pour les développeurs Android, lors de la configuration de l'interface Bluetooth dans l'application, l'utilisation de la méthode startActivityForResult(Intent, int) pour activer la détectabilité de l'appareil et la définition de EXTRA_DISCOVERABLE_DURATION sur zéro entraînera la détectabilité de l'appareil tant que l'application est en cours d'exécution en arrière-plan ou au premier plan. En ce qui concerne la spécification Bluetooth classique, les appareils détectables diffusent en permanence des messages de découverte spécifiques qui permettent à d'autres appareils de récupérer les données de l'appareil ou de s'y connecter. Dans un tel scénario, un tiers malveillant peut intercepter ces messages et se connecter à l'appareil Android. Une fois connecté, un pirate informatique peut effectuer d'autres attaques, comme le vol de données, le déni de service ou l'injection de commandes.

Stratégies d'atténuation

La valeur de EXTRA_DISCOVERABLE_DURATION ne doit jamais être définie sur zéro. Si le paramètre EXTRA_DISCOVERABLE_DURATION n'est pas défini, Android rend les appareils détectables pendant deux minutes par défaut. La valeur maximale pouvant être définie pour le paramètre EXTRA_DISCOVERABLE_DURATION est de deux heures (7 200 secondes). Nous vous recommandons de définir la durée de visibilité sur la période la plus courte possible, en fonction du cas d'utilisation de l'application.


Risque : NFC – filtres d'intent clonés

Une application malveillante peut enregistrer des filtres d'intent pour lire des tags NFC ou des appareils compatibles avec la technologie NFC spécifiques. Ces filtres peuvent reproduire ceux définis par une application légitime, ce qui permet à un pirate informatique de lire le contenu des données NFC échangées. Il convient de noter que, lorsque deux activités spécifient les mêmes filtres d'intent pour un tag NFC spécifique, le sélecteur d'activité est présenté. L'utilisateur devra donc toujours choisir l'application malveillante pour que l'attaque réussisse. Toutefois, ce scénario reste possible si vous combinez des filtres d'intent avec du masquage. Cette attaque n'est importante que dans les cas où les données échangées via NFC peuvent être considérées comme très sensibles.

Stratégies d'atténuation

Lorsque vous implémentez des fonctionnalités de lecture NFC dans une application, vous pouvez utiliser des filtres d'intent avec des enregistrements d'application Android (AAR). L'intégration de l'enregistrement AAR dans un message NDEF permet de s'assurer que seule l'application légitime et son activité de gestion NDEF associée sont lancées. Cela empêchera les applications ou activités indésirables de lire les données de tag ou d'appareil très sensibles échangées via NFC.


Risque : NFC – manque de validation des messages NDEF

Lorsqu'un appareil Android reçoit des données d'un tag NFC ou d'un appareil compatible NFC, le système déclenche automatiquement l'application ou l'activité spécifique configurée pour gérer le message NDEF contenu. Selon la logique implémentée dans l'application, les données contenues dans le tag ou reçues de l'appareil peuvent être transmises à d'autres activités pour déclencher d'autres actions, comme l'ouverture de pages Web.

Une application qui ne valide pas le contenu des messages NDEF peut permettre à des pirates informatiques d'utiliser des appareils ou des tags NFC pour injecter des charges utiles malveillantes dans l'application, ce qui peut entraîner un comportement inattendu pouvant se traduire par le téléchargement de fichiers malveillants, l'injection de commandes ou un déni de service.

Stratégies d'atténuation

Avant d'envoyer le message NDEF reçu à un autre composant d'application, les données qu'il contient doivent être validées pour s'assurer qu'elles sont au format attendu et qu'elles contiennent les informations attendues. Cela évite que des données malveillantes ne soient transmises sans filtre aux composants d'autres applications, ce qui réduit le risque de comportement inattendu ou d'attaques utilisant des données NFC falsifiées.

L'extrait suivant montre un exemple de logique de validation des données implémentée en tant que méthode avec un message NDEF comme argument et son index dans le tableau des messages. Cela a été implémenté sur l'exemple des développeurs Android pour obtenir des données à partir d'une balise NDEF NFC scannée :

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

Ressources