Небезопасная настройка межмашинного взаимодействия

Категория OWASP: MASVS-CODE: качество кода

Обзор

Нередко в приложениях реализуются функции, позволяющие пользователям передавать данные или взаимодействовать с другими устройствами, используя радиочастотную связь или кабельные соединения. Наиболее распространенные технологии, используемые в Android для этой цели, – это классический Bluetooth (Bluetooth BR/EDR), Bluetooth с низким энергопотреблением (BLE), Wi-Fi P2P, NFC и USB.

Обычно эти технологии используются в приложениях, которые должны взаимодействовать с аксессуарами умного дома, устройствами для мониторинга здоровья, киосками общественного транспорта, кассовыми терминалами и другими устройствами Android.

Как и в случае с любым другим каналом, межмашинные коммуникации уязвимы для атак, направленных на компрометацию границы доверия, установленной между двумя или более устройствами. Злоумышленники могут использовать такие методы, как имитация устройства, для проведения широкого спектра атак на канал связи.

Android предоставляет разработчикам специальные API для настройки межмашинного взаимодействия.

Используйте эти API с осторожностью, поскольку ошибки при реализации протоколов связи могут привести к тому, что данные пользователя или устройства станут доступны неавторизованным третьим лицам. В худшем случае злоумышленники могут удаленно взять под контроль одно или несколько устройств и получить полный доступ к их контенту.

Влияние

Влияние может различаться в зависимости от технологии передачи данных между устройствами, реализованной в приложении.

Неправильное использование или настройка каналов связи между машинами может привести к тому, что устройство пользователя будет уязвимо для попыток связи из ненадежных источников. Это может сделать устройство уязвимым для дополнительных атак, таких как атака посредника, внедрение команд, DoS-атака или атака с выдачей себя за другое лицо.

Риск: перехват конфиденциальных данных по беспроводным каналам.

При реализации механизмов межмашинного взаимодействия следует тщательно продумать как используемую технологию, так и тип передаваемых данных. Проводные соединения на практике более безопасны для таких задач, поскольку требуют физического подключения между устройствами. Однако протоколы связи, использующие радиочастоты, такие как классический Bluetooth, BLE, NFC и Wi-Fi P2P, могут быть перехвачены. Злоумышленник может выдать себя за один из терминалов или одну из точек доступа, участвующих в обмене данными, перехватить беспроводную связь и получить доступ к конфиденциальным пользовательским данным. Кроме того, вредоносные приложения, установленные на устройстве, могут получить доступ к данным, которыми обмениваются устройства, если у них есть динамические разрешения на чтение буферов системных сообщений.

Предотвращение атак

Если приложению требуется обмен конфиденциальными данными между машинами по беспроводным каналам, то в его коде должны быть реализованы решения для обеспечения безопасности на прикладном уровне, например шифрование. Это не позволит злоумышленникам перехватывать данные, передаваемые по каналу связи, и получать их в виде обычного текста. Дополнительные ресурсы можно найти в документации по криптографии.


Риск: беспроводное внедрение вредоносных данных

Беспроводные каналы связи между устройствами (классический Bluetooth, BLE, NFC, Wi-Fi P2P) могут быть взломаны с помощью вредоносных данных. Злоумышленники с достаточными навыками могут определить используемый протокол связи и вмешаться в поток обмена данными, например выдав себя за одну из конечных точек и отправив специально созданные полезные нагрузки. Такой вредоносный трафик может ухудшить работу приложения и в худшем случае привести к неожиданному поведению приложения и устройства или к таким атакам, как DoS, внедрение команд или захват устройства.

Предотвращение атак

Android предоставляет разработчикам мощные API для управления межмашинными коммуникациями, такими как классический Bluetooth, BLE, NFC и Wi-Fi P2P. Их следует сочетать с тщательно реализованной логикой проверки данных, чтобы очищать любые данные, которыми обмениваются два устройства.

Это решение должно быть реализовано на уровне приложения и включать проверки, которые подтверждают, что данные имеют ожидаемую длину, формат и содержат допустимую полезную нагрузку, которая может быть интерпретирована приложением.

Ниже приведен пример логики проверки данных. Это было реализовано на основе примера для разработчиков Android по реализации передачи данных через 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;
           }
    }

Риск: внедрение вредоносных данных через USB

USB-подключения между двумя устройствами могут быть уязвимы для злоумышленников, которые хотят перехватить данные. В этом случае физическая связь представляет собой дополнительный уровень безопасности, поскольку злоумышленнику необходимо получить доступ к кабелю, соединяющему терминалы, чтобы иметь возможность подслушивать любое сообщение. Ещё один вектор атаки – ненадежные USB-устройства, которые намеренно или случайно подключаются к устройству.

Если приложение фильтрует USB-устройства по PID/VID, чтобы активировать определенные функции, злоумышленники могут подделать данные, отправляемые по USB-каналу, выдав себя за легитимное устройство. В результате злоумышленники могут отправлять на устройство нажатия клавиш или выполнять действия в приложениях, что в худшем случае может привести к удаленному выполнению кода или скачиванию нежелательного ПО.

Предотвращение атак

Необходимо реализовать логику проверки на уровне приложения. Эта логика должна фильтровать данные, отправляемые через USB, проверяя, что длина, формат и содержимое соответствуют сценарию использования приложения. Например, пульсометр не должен отправлять команды нажатия клавиш.

Кроме того, по возможности следует ограничить количество пакетов, которые приложение может получить от USB-устройства. Это позволяет предотвратить атаки с использованием вредоносных устройств, например Rubber Ducky.

Для этого можно создать новый поток, который будет проверять содержимое буфера, например при вызове 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 – неправильное время видимости

Как указано в документации по Bluetooth для разработчиков Android, если при настройке интерфейса Bluetooth в приложении использовать метод startActivityForResult(Intent, int), чтобы сделать устройство видимым, и задать для EXTRA_DISCOVERABLE_DURATION значение "0", устройство будет видимым, пока приложение работает в фоновом или активном режиме. Согласно классической спецификации Bluetooth, видимые устройства постоянно транслируют специальные сообщения, которые позволяют другим устройствам получать данные или подключаться. В таком случае злоумышленник может перехватить эти сообщения и подключиться к устройству Android. После подключения злоумышленник может совершать дальнейшие атаки, например кражу данных, DoS или внедрение команд.

Предотвращение атак

Значение EXTRA_DISCOVERABLE_DURATION не должно быть равно нулю. Если параметр EXTRA_DISCOVERABLE_DURATION не задан, по умолчанию устройство Android будет видимым в течение двух минут. Максимальное значение, которое можно задать для параметра EXTRA_DISCOVERABLE_DURATION, составляет 2 часа (7200 секунд). Рекомендуется устанавливать минимально возможную продолжительность видимости в зависимости от сценария использования приложения.


Риск: NFC – клонированные фильтры intent-filter

Вредоносное приложение может зарегистрировать фильтры намерений, чтобы считывать определенные NFC-метки или данные с устройств с поддержкой NFC. Эти фильтры могут копировать фильтры, определенные легитимным приложением, что позволяет злоумышленнику читать содержимое передаваемых данных NFC. Следует отметить, что если два действия указывают одни и те же фильтры интентов для определенного NFC-тега, то появляется окно выбора действия. Таким образом, чтобы атака была успешной, пользователь должен выбрать вредоносное приложение. Однако если объединить фильтры намерений с маскировкой, то такой сценарий все же возможен. Эта атака представляет опасность только в тех случаях, когда данные, передаваемые по NFC, можно считать конфиденциальными.

Предотвращение атак

При реализации возможностей чтения NFC в приложении фильтры интентов можно использовать вместе с записями приложений для Android (AAR). Встраивание записи AAR в сообщение NDEF дает надежную гарантию того, что будет запущено только легитимное приложение и связанное с ним действие по обработке NDEF. Это не позволит нежелательным приложениям или действиям считывать конфиденциальные данные тегов или устройств, передаваемые через NFC.


Угроза: NFC – отсутствие проверки сообщений NDEF

Когда устройство Android получает данные от NFC-тега или устройства с поддержкой NFC, система автоматически запускает приложение или определенное действие, настроенное для обработки содержащегося в нем сообщения NDEF. Согласно логике, реализованной в приложении, данные, содержащиеся в теге или полученные с устройства, могут быть переданы другим действиям для запуска дальнейших действий, таких как открытие веб-страниц.

Если в приложении не проверяется контент сообщений NDEF, злоумышленники могут использовать устройства или теги с поддержкой NFC, чтобы внедрять в приложение вредоносные полезные нагрузки. Это может привести к неожиданному поведению, например скачиванию вредоносных файлов, внедрению команд или DoS-атакам.

Предотвращение атак

Прежде чем отправлять полученное сообщение 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;
    }

Ресурсы