Niezabezpieczona konfiguracja komunikacji między maszynami

Kategoria OWASP: MASVS-CODE: Jakość kodu

Przegląd

Często spotyka się aplikacje, które umożliwiają użytkownikom przesyłanie danych lub wchodzenie w interakcje z innymi urządzeniami za pomocą komunikacji radiowej lub połączeń kablowych. Najczęściej używane w tym celu technologie w Androidzie to klasyczny Bluetooth (Bluetooth BR/EDR), Bluetooth Low Energy (BLE), Wi-Fi P2P, NFC i USB.

Te technologie są zwykle wdrażane w aplikacjach, które mają komunikować się z akcesoriami do inteligentnego domu, urządzeniami do monitorowania zdrowia, kioskami transportu publicznego, terminalami płatniczymi i innymi urządzeniami z Androidem.

Podobnie jak w przypadku każdego innego kanału komunikacji, komunikacja między urządzeniami jest podatna na ataki, których celem jest naruszenie granicy zaufania między 2 lub większą liczbą urządzeń. Techniki takie jak podszywanie się pod urządzenie mogą być wykorzystywane przez złośliwych użytkowników do przeprowadzania wielu ataków na kanał komunikacji.

Android udostępnia deweloperom specjalne interfejsy API do konfigurowania komunikacji między urządzeniami.

Z tych interfejsów API należy korzystać ostrożnie, ponieważ błędy podczas implementowania protokołów komunikacyjnych mogą spowodować ujawnienie danych użytkownika lub danych na urządzeniu nieuprawnionym osobom trzecim. W najgorszym przypadku atakujący mogą zdalnie przejąć kontrolę nad jednym lub większą liczbą urządzeń, a w konsekwencji uzyskać pełny dostęp do treści na nich zapisanych.

Wpływ

Wpływ może się różnić w zależności od technologii komunikacji między urządzeniami zaimplementowanej w aplikacji.

Nieprawidłowe użycie lub konfiguracja kanałów komunikacji między urządzeniami może narazić urządzenie użytkownika na próby komunikacji z niezaufanymi źródłami. Może to sprawić, że urządzenie będzie podatne na dodatkowe ataki, takie jak ataki typu „man-in-the-middle” (MITM), wstrzykiwanie poleceń, ataki DoS lub ataki polegające na podszywaniu się pod inne osoby.

Ryzyko: podsłuchiwanie danych wrażliwych w kanałach bezprzewodowych

Podczas wdrażania mechanizmów komunikacji między maszynami należy dokładnie rozważyć zarówno stosowaną technologię, jak i rodzaj danych, które mają być przesyłane. Połączenia kablowe są w praktyce bezpieczniejsze w przypadku takich zadań, ponieważ wymagają fizycznego połączenia między urządzeniami. Jednak protokoły komunikacyjne wykorzystujące częstotliwości radiowe, takie jak Bluetooth Classic, BLE, NFC i Wi-Fi P2P, mogą zostać przechwycone. Osoba atakująca może podszywać się pod jeden z terminali lub punktów dostępu biorących udział w wymianie danych, przechwytywać komunikację bezprzewodowo i w ten sposób uzyskiwać dostęp do wrażliwych danych użytkownika. Dodatkowo złośliwe aplikacje zainstalowane na urządzeniu, jeśli mają uprawnienia środowiska wykonawczego dotyczące komunikacji, mogą pobierać dane wymieniane między urządzeniami, odczytując bufory wiadomości systemowych.

Środki ograniczające ryzyko

Jeśli aplikacja wymaga wymiany danych wrażliwych między urządzeniami za pomocą kanałów bezprzewodowych, w jej kodzie należy zaimplementować rozwiązania zabezpieczające w warstwie aplikacji, takie jak szyfrowanie. Uniemożliwi to atakującym podsłuchiwanie kanału komunikacji i pobieranie wymienianych danych w postaci zwykłego tekstu. Dodatkowe materiały znajdziesz w dokumentacji Cryptography.


Ryzyko: wstrzykiwanie szkodliwych danych w sieci bezprzewodowej

Bezprzewodowe kanały komunikacji między urządzeniami (Bluetooth Classic, BLE, NFC, Wi-Fi P2P) mogą być manipulowane za pomocą złośliwych danych. Wystarczająco wykwalifikowane osoby przeprowadzające atak mogą zidentyfikować używany protokół komunikacyjny i manipulować przepływem wymiany danych, np. przyjmując tożsamość jednego z punktów końcowych i wysyłając specjalnie przygotowane ładunki. Tego rodzaju złośliwy ruch może pogorszyć działanie aplikacji, a w najgorszym przypadku spowodować nieoczekiwane zachowanie aplikacji i urządzenia lub doprowadzić do ataków typu DoS, wstrzykiwania poleceń lub przejęcia kontroli nad urządzeniem.

Środki ograniczające ryzyko

Android udostępnia programistom zaawansowane interfejsy API do zarządzania komunikacją między urządzeniami, takimi jak klasyczny Bluetooth, BLE, NFC i Wi-Fi P2P. Należy je połączyć ze starannie wdrożoną logiką weryfikacji danych, aby oczyszczać wszelkie dane wymieniane między dwoma urządzeniami.

To rozwiązanie powinno być wdrożone na poziomie aplikacji i powinno obejmować sprawdzanie, czy dane mają oczekiwaną długość i format oraz czy zawierają prawidłowy ładunek, który może być interpretowany przez aplikację.

Poniższy fragment kodu zawiera przykładową logikę weryfikacji danych. Zostało to zaimplementowane na podstawie przykładu dla deweloperów Androida dotyczącego implementacji przesyłania danych przez 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;
           }
    }

Ryzyko: wstrzyknięcie szkodliwych danych przez USB

Połączenia USB między dwoma urządzeniami mogą być celem złośliwego użytkownika, który chce przechwycić komunikację. W tym przypadku wymagane połączenie fizyczne stanowi dodatkową warstwę zabezpieczeń, ponieważ aby podsłuchiwać wiadomości, atakujący musi uzyskać dostęp do kabla łączącego terminale. Kolejnym wektorem ataku są niezaufane urządzenia USB, które są podłączane do urządzenia celowo lub przypadkowo.

Jeśli aplikacja filtruje urządzenia USB za pomocą identyfikatora produktu (PID) lub identyfikatora dostawcy (VID), aby wywoływać określone funkcje w aplikacji, atakujący mogą manipulować danymi przesyłanymi przez kanał USB, podszywając się pod legalne urządzenie. Ataki tego typu mogą umożliwić złośliwym użytkownikom wysyłanie naciśnięć klawiszy na urządzenie lub wykonywanie działań aplikacji, które w najgorszym przypadku mogą prowadzić do zdalnego wykonania kodu lub pobrania niechcianego oprogramowania.

Środki ograniczające ryzyko

Należy wdrożyć logikę weryfikacji na poziomie aplikacji. Ta logika powinna filtrować dane przesyłane przez USB, sprawdzając, czy długość, format i treść pasują do przypadku użycia aplikacji. Na przykład monitor pracy serca nie powinien mieć możliwości wysyłania poleceń naciśnięcia klawiszy.

Dodatkowo, w miarę możliwości, należy rozważyć ograniczenie liczby pakietów USB, które aplikacja może odbierać z urządzenia USB. Zapobiega to atakom złośliwych urządzeń, takim jak atak typu „rubber ducky”.

Możesz to zrobić, tworząc nowy wątek do sprawdzania zawartości bufora, na przykład po wystąpieniu zdarzenia 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();
        }
    }

Konkretne zagrożenia

W tej sekcji znajdziesz zagrożenia, które wymagają niestandardowych strategii ograniczania ryzyka lub zostały ograniczone na określonym poziomie pakietu SDK i są tu wymienione dla pełności informacji.

Ryzyko: Bluetooth – nieprawidłowy czas wykrywalności

Zgodnie z informacjami w dokumentacji Bluetootha dla deweloperów Androida podczas konfigurowania interfejsu Bluetooth w aplikacji użycie metody startActivityForResult(Intent, int) w celu włączenia wykrywalności urządzenia i ustawienie wartości EXTRA_DISCOVERABLE_DURATION na zero spowoduje, że urządzenie będzie wykrywalne, dopóki aplikacja działa w tle lub na pierwszym planie. W przypadku klasycznej specyfikacji Bluetooth wykrywalne urządzenia stale emitują określone komunikaty wykrywania, które umożliwiają innym urządzeniom pobieranie danych na urządzeniu lub łączenie się z nim. W takim przypadku złośliwa osoba trzecia może przechwycić takie wiadomości i połączyć się z urządzeniem z Androidem. Po nawiązaniu połączenia osoba przeprowadzająca atak może wykonać kolejne ataki, takie jak kradzież danych, atak DoS lub wstrzykiwanie poleceń.

Środki ograniczające ryzyko

Wartość EXTRA_DISCOVERABLE_DURATION nigdy nie powinna być ustawiona na zero. Jeśli parametr EXTRA_DISCOVERABLE_DURATION nie jest ustawiony, Android domyślnie sprawia, że urządzenia są wykrywalne przez 2 minuty. Maksymalna wartość, jaką można ustawić dla parametru EXTRA_DISCOVERABLE_DURATION, to 2 godziny (7200 sekund). Zalecamy, aby czas wykrywania był jak najkrótszy, zgodnie z przypadkiem użycia aplikacji.


Ryzyko: NFC – sklonowane filtry intencji

Szkodliwa aplikacja może zarejestrować filtry intencji, aby odczytywać określone tagi NFC lub urządzenia z obsługą NFC. Te filtry mogą replikować filtry zdefiniowane przez legalną aplikację, co umożliwia atakującemu odczytanie zawartości wymienianych danych NFC. Należy pamiętać, że jeśli 2 aktywności określają te same filtry intencji dla konkretnego tagu NFC, wyświetlany jest selektor aktywności, więc użytkownik nadal będzie musiał wybrać złośliwą aplikację, aby atak się powiódł. Niemniej jednak połączenie filtrów intencji z maskowaniem nadal umożliwia realizację tego scenariusza. Ten atak jest istotny tylko w przypadku, gdy dane wymieniane za pomocą NFC można uznać za bardzo wrażliwe.

Środki ograniczające ryzyko

Podczas wdrażania w aplikacji funkcji odczytu NFC można używać filtrów intencji w połączeniu z rekordami aplikacji na Androida (AAR). Umieszczenie rekordu AAR w wiadomości NDEF daje pewność, że zostanie uruchomiona tylko właściwa aplikacja i powiązane z nią działanie obsługi NDEF. Zapobiegnie to odczytywaniu przez niechciane aplikacje lub działania bardzo wrażliwych danych tagu lub danych na urządzeniu wymienianych za pomocą NFC.


Ryzyko: NFC – brak weryfikacji wiadomości NDEF

Gdy urządzenie z Androidem odbiera dane z tagu NFC lub urządzenia z NFC, system automatycznie uruchamia aplikację lub konkretną aktywność skonfigurowaną do obsługi zawartej w niej wiadomości NDEF. Zgodnie z logiką zaimplementowaną w aplikacji dane zawarte w tagu lub otrzymane z urządzenia mogą być przekazywane do innych działań w celu wywołania dalszych działań, takich jak otwieranie stron internetowych.

Aplikacja, która nie sprawdza zawartości wiadomości NDEF, może umożliwiać atakującym wstrzykiwanie złośliwych ładunków do aplikacji za pomocą urządzeń lub tagów NFC. Może to powodować nieoczekiwane zachowania, takie jak pobieranie złośliwych plików, wstrzykiwanie poleceń lub atak DoS.

Środki ograniczające ryzyko

Przed wysłaniem odebranej wiadomości NDEF do dowolnego innego komponentu aplikacji należy sprawdzić, czy dane w niej zawarte mają oczekiwany format i zawierają oczekiwane informacje. Zapobiega to przekazywaniu złośliwych danych do komponentów innych aplikacji bez filtrowania, co zmniejsza ryzyko nieoczekiwanych zachowań lub ataków z użyciem zmodyfikowanych danych NFC.

Poniższy fragment kodu pokazuje przykładową logikę weryfikacji danych zaimplementowaną jako metoda z komunikatem NDEF jako argumentem i jego indeksem w tablicy komunikatów. Zostało to zaimplementowane na podstawie przykładu dla deweloperów Androida, aby uzyskać dane ze zeskanowanego tagu 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;
    }

Zasoby