Insecure machine-to-machine communication setup

OWASP category: MASVS-CODE: Code Quality

Overview

It's not rare to see applications that implement functionality allowing users to transfer data, or interact with other devices, using radio frequency (RF) communications or cabled connections. The most common technologies used in Android for this purpose are classic Bluetooth (Bluetooth BR/EDR), Bluetooth Low Energy (BLE), Wifi P2P, NFC, and USB.

These technologies are usually implemented in applications that are expected to communicate with smart home accessories, health monitoring devices, public transportation kiosks, payment terminals, and other Android-powered devices.

As with any other channel, machine-to-machine communications are susceptible to attacks that aim to compromise the trust boundary established between two or more devices. Techniques such as device impersonation can be leveraged by malicious users to achieve a broad number of attacks against the communication channel.

Android makes specific APIs for configuring machine-to-machine communications available to developers.

These APIs should be used carefully as errors while implementing communication protocols may lead to user or device data being exposed to unauthorized third-parties. In the worst case scenario, attackers may be able to remotely take over one or more devices, consequently gaining full access to the content on the device.

Impact

The impact may vary according to the device-to-device technology implemented in the application.

The wrong usage or configuration of machine-to-machine communication channels may leave the user device exposed to untrusted communication attempts. This can lead to the device being vulnerable to additional attacks such as man-in-the-middle (MiTM), command injection, DoS, or impersonation attacks.

Risk: Eavesdropping of sensitive data over wireless channels

When implementing machine-to-machine communication mechanisms, careful consideration should be given to both the used technology and the type of data that should be transmitted. While cabled connections are in practice more secure for such tasks, as they require a physical link between the involved devices, communications protocols using radio frequencies such as classic Bluetooth, BLE, NFC, and Wifi P2P can be intercepted. An attacker may be able to impersonate one of the terminals or access points involved in the data exchange, intercepting the communication over the air, consequently gaining access to sensitive user data. Additionally, malicious applications installed on the device, if granted the communication-specific runtime permissions, may be able to retrieve data exchanged between the devices by reading system message buffers.

Mitigations

If the application does require machine-to-machine exchange of sensitive data over wireless channels, then application-layer security solutions, such as encryption, should be implemented in the application's code. This will prevent attackers from sniffing on the communication channel and retrieving the exchanged data in clear-text. For additional resources, refer to the Cryptography documentation.


Risk: Wireless malicious data injection

Wireless machine-to-machine communication channels (classic Bluetooth, BLE, NFC, Wifi P2P) can be tampered with using malicious data. Sufficiently skilled attackers can identify the communication protocol in use and tamper with the data exchange flow, for example by impersonating one of the endpoints, sending specifically crafted payloads. This kind of malicious traffic may degrade the application's functionality and, in the worst case scenario, cause unexpected application and device behavior, or result in attacks such as DoS, command injection, or device takeover.

Mitigations

Android provides developers with powerful APIs to manage machine-to-machine communications such as classic Bluetooth, BLE, NFC, and Wifi P2P. These should be combined with a carefully implemented data validation logic to sanitize any data exchanged between two devices.

This solution should be implemented at the application level and should include checks that verify if the data has the expected length, format, and contains a valid payload that can be interpreted by the application.

The following snippet shows example data validation logic. This was implemented over the Android developers example for implementing Bluetooth data transfer:

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

Risk: USB malicious data injection

USB connections between two devices can be targeted by a malicious user interested in intercepting communications. In this case, the physical link required constitutes an additional security layer as the attacker needs to gain access to the cable that connects the terminals to be able to eavesdrop on any message. Another attack vector is represented by untrusted USB devices that, either intentionally or unintentionally, are plugged into the device.

If the application filters USB devices using PID/VID for triggering specific in-app functionality, attackers may be able to tamper with the data sent over the USB channel by impersonating the legitimate device. Attacks of this kind can allow malicious users to send keystrokes to the device or execute application activities that, in the worst case, may lead to remote code execution or unwanted software download.

Mitigations

An application-level validation logic should be implemented. This logic should filter the data sent over USB checking that the length, format, and content match the application use case. For example, a heartbeat monitor shouldn't be able to send keystroke commands.

Additionally, when possible, consideration should be given to restricting the number of USB packets that the application can receive from the USB device. This prevents malicious devices from performing attacks such as the rubber ducky.

This validation can be accomplished by creating a new thread for checking the buffer content, for example, upon a 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();
        }
    }

Specific Risks

This section gathers risks that require non-standard mitigation strategies or were mitigated at certain SDK level and are here for completeness.

Risk: Bluetooth – incorrect discoverability time

As highlighted in the Android developers Bluetooth documentation, while configuring the Bluetooth interface within the application, using the startActivityForResult(Intent, int) method to enable device discoverability and setting the EXTRA_DISCOVERABLE_DURATION to zero will cause the device to be discoverable as long as the application is running either in background or foreground. As for classic Bluetooth specification, discoverable devices are constantly broadcasting specific discovery messages that allow other devices to retrieve device data or connect to it. In such a scenario, a malicious third-party can intercept such messages and connect to the Android-powered device. Once connected, an attacker can perform further attacks such as data theft, DoS, or command injection.

Mitigations

The EXTRA_DISCOVERABLE_DURATION should never be set to zero. If the EXTRA_DISCOVERABLE_DURATION parameter is not set, by default, Android makes the devices discoverable for 2 minutes. The max value that can be set for the EXTRA_DISCOVERABLE_DURATION parameter is 2 hours (7200 seconds). It is recommended to keep the discoverable duration time to the shortest time possible, according to the application use case.


Risk: NFC – cloned intent-filters

A malicious application can register intent-filters to read specific NFC tags or NFC-enabled devices. These filters can replicate the ones defined by a legitimate application, making it possible for an attacker to read the content of the exchanged NFC data. It should be noted that, when two activities specify the same intent-filters for a specific NFC tag, the Activity Chooser is presented, therefore the user will still need to choose the malicious application for the attack to be successful. Nevertheless, combining intent-filters with cloaking, this scenario is still possible. This attack is significant only for cases where the data exchanged over NFC can be considered highly sensitive.

Mitigations

When implementing NFC reading capabilities within an application, intent-filters can be used together with Android application records (AARs). Embedding the AAR record inside an NDEF message will give strong assurance that only the legitimate application, and its associated NDEF handling activity, is started. This will prevent unwanted applications or activities from reading highly sensitive tag or device data exchanged through NFC.


Risk: NFC – lack of NDEF message validation

When an Android-powered device receives data from an NFC tag or NFC-enabled device, the system automatically triggers the application or the specific activity that is configured to handle the NDEF message contained within. According to the logic implemented in the application, the data contained in the tag, or received from the device, can be served to other activities to trigger further actions, such as opening web pages.

An application lacking NDEF message content validation may allow attackers to use NFC-enabled devices or NFC tags to inject malicious payloads within the application, causing unexpected behavior that may result in malicious file download, command injection, or DoS.

Mitigations

Before dispatching the received NDEF message to any other application component, data within should be validated to be in the expected format and to contain the expected information. This avoids malicious data being passed to other applications' components unfiltered, reducing the risk of unexpected behavior or attacks using tampered NFC data.

The following snippet shows example data validation logic implemented as a method with an NDEF message as an argument and its index in the messages array. This was implemented over the Android developers example to get data from a scanned NFC NDEF tag:

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

Resources