إعداد الاتصال بين الجهازين غير الآمن

فئة OWASP: MASVS-CODE: جودة الرمز

نظرة عامة

ليس من النادر أن نرى تطبيقات تنفّذ وظائف تتيح للمستخدمين نقل البيانات أو التفاعل مع أجهزة أخرى باستخدام اتصالات لاسلكية (RF) أو اتصالات سلكية. تتضمّن التقنيات الأكثر شيوعًا المستخدَمة في Android لهذا الغرض كلاً من Bluetooth Classic (Bluetooth BR/EDR) وBluetooth Low Energy (BLE) وWifi P2P وNFC وUSB.

ويتم عادةً استخدام هذه التقنيات في التطبيقات التي يُتوقّع منها التواصل مع ملحقات المنزل المزوّد بأجهزة ذكية وأجهزة مراقبة الصحة وأكشاك وسائل النقل العام ومحطات الدفع وغيرها من الأجهزة التي تعمل بنظام التشغيل Android.

وكما هو الحال مع أي قناة أخرى، فإنّ عمليات التواصل بين الأجهزة معرّضة للهجمات التي تهدف إلى اختراق حدود الثقة التي تم إنشاؤها بين جهازين أو أكثر. يمكن للمستخدمين الضارّين الاستفادة من أساليب مثل انتحال هوية الجهاز لتنفيذ عدد كبير من الهجمات على قناة الاتصال.

يتيح نظام التشغيل Android للمطوّرين استخدام واجهات برمجة تطبيقات معيّنة لإعداد عمليات التواصل بين الأجهزة.

يجب استخدام واجهات برمجة التطبيقات هذه بعناية لأنّ حدوث أخطاء أثناء تنفيذ بروتوكولات الاتصال قد يؤدي إلى تعرُّض بيانات المستخدم أو الجهاز لجهات خارجية غير مصرَّح لها. في أسوأ الحالات، قد يتمكّن المهاجمون من السيطرة عن بُعد على جهاز واحد أو أكثر، وبالتالي الحصول على إذن الوصول الكامل إلى المحتوى على الجهاز.

التأثير

وقد يختلف التأثير حسب التكنولوجيا التي تتيح التواصل بين الأجهزة والمستخدَمة في التطبيق.

قد يؤدي الاستخدام أو الإعداد غير الصحيحَين لقنوات الاتصال بين الأجهزة إلى تعرُّض جهاز المستخدم لمحاولات اتصال غير موثوق بها. ويمكن أن يؤدي ذلك إلى تعرُّض الجهاز لهجمات إضافية، مثل هجمات الوسيط أو هجمات حقن الأوامر أو هجمات رفض الخدمة أو هجمات انتحال الهوية.

الخطر: التنصّت على البيانات الحساسة عبر القنوات اللاسلكية

عند تنفيذ آليات الاتصال بين الأجهزة، يجب مراعاة التكنولوجيا المستخدَمة ونوع البيانات التي يجب نقلها. في حين أنّ الاتصالات السلكية أكثر أمانًا من الناحية العملية لهذه المهام، لأنّها تتطلّب رابطًا ماديًا بين الأجهزة المعنية، يمكن اعتراض بروتوكولات الاتصالات التي تستخدم الترددات اللاسلكية، مثل البلوتوث الكلاسيكي وBLE وNFC وWifi P2P. قد يتمكّن المهاجم من انتحال هوية إحدى المحطات أو نقاط الوصول المشاركة في تبادل البيانات، ما يؤدي إلى اعتراض الاتصال عبر الأثير، وبالتالي الوصول إلى بيانات المستخدم الحسّاسة. بالإضافة إلى ذلك، قد تتمكّن التطبيقات الضارة المثبَّتة على الجهاز من استرداد البيانات المتبادلة بين الأجهزة من خلال قراءة مخازن مؤقتة لرسائل النظام، وذلك في حال منحها أذونات وقت التشغيل الخاصة بالتواصل.

إجراءات التخفيف

إذا كان التطبيق يتطلّب تبادلاً بين الأجهزة للبيانات الحسّاسة عبر قنوات لاسلكية، يجب تنفيذ حلول أمان على مستوى التطبيق، مثل التشفير، في الرمز البرمجي للتطبيق. سيمنع ذلك المهاجمين من التجسس على قناة الاتصال واسترداد البيانات المتبادلة بنص عادي. للحصول على مراجع إضافية، يُرجى الرجوع إلى مستندات التشفير.


المخاطرة: إدخال بيانات ضارة لاسلكيًا

يمكن التلاعب بقنوات الاتصال اللاسلكي بين الأجهزة (البلوتوث الكلاسيكي، والبلوتوث المنخفض الطاقة، واتصالات المجال القريب، وشبكة Wi-Fi من الند للند) باستخدام بيانات ضارة. يمكن للمهاجمين الذين يتمتعون بمهارات كافية تحديد بروتوكول الاتصال المستخدَم والتلاعب بتدفق تبادل البيانات، على سبيل المثال من خلال انتحال هوية إحدى نقاط النهاية وإرسال حمولات مصمَّمة خصيصًا. قد يؤدي هذا النوع من الزيارات الضارة إلى خفض أداء التطبيق، وفي أسوأ الحالات، قد يتسبب في حدوث سلوك غير متوقّع في التطبيق والجهاز، أو يؤدي إلى هجمات مثل رفض الخدمة (DoS) أو إدخال أوامر أو السيطرة على الجهاز.

إجراءات التخفيف

يوفّر نظام التشغيل Android للمطوّرين واجهات برمجة تطبيقات قوية لإدارة الاتصالات بين الأجهزة، مثل البلوتوث الكلاسيكي والبلوتوث المنخفض الطاقة (BLE) واتصالات المجال القريب (NFC) وشبكة Wi-Fi من جهاز إلى جهاز (P2P). ويجب دمجها مع منطق التحقّق من صحة البيانات الذي تم تنفيذه بعناية لتنظيف أي بيانات يتم تبادلها بين جهازين.

يجب تنفيذ هذا الحلّ على مستوى التطبيق، ويجب أن يتضمّن عمليات تحقّق للتأكّد من أنّ البيانات تتضمّن الطول والتنسيق المتوقّعَين، وأنّها تحتوي على حمولة صالحة يمكن للتطبيق تفسيرها.

تعرض المقتطفة التالية مثالاً على منطق التحقّق من صحة البيانات. تم تنفيذ ذلك باستخدام المثال الذي قدّمه مطوّرو 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

يمكن أن يستهدف مستخدم ضار توصيلات USB بين جهازين بهدف اعتراض الاتصالات. في هذه الحالة، يشكّل الرابط المادي طبقة أمان إضافية لأنّ المهاجم يحتاج إلى الوصول إلى الكابل الذي يربط بين الأجهزة الطرفية ليتمكّن من التجسس على أي رسالة. يتوفّر أسلوب هجوم آخر يتمثّل في أجهزة USB غير موثوق بها يتم توصيلها بالجهاز، سواء كان ذلك عن قصد أو عن غير قصد.

إذا كان التطبيق يفلتر أجهزة USB باستخدام PID/VID لتفعيل وظائف معيّنة داخل التطبيق، قد يتمكّن المهاجمون من التلاعب بالبيانات المُرسَلة عبر قناة USB من خلال انتحال هوية الجهاز الشرعي. يمكن أن تسمح الهجمات من هذا النوع للمستخدمين الضارين بإرسال ضغطات مفاتيح إلى الجهاز أو تنفيذ أنشطة التطبيقات التي قد تؤدي في أسوأ الحالات إلى تنفيذ الرمز عن بُعد أو تنزيل برنامج غير مرغوب فيه.

إجراءات التخفيف

يجب تنفيذ منطق التحقّق من الصحة على مستوى التطبيق. يجب أن تعمل هذه المنطقية على فلترة البيانات المرسَلة عبر 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)، وهي مدرَجة هنا من أجل الاكتمال.

المخاطرة: البلوتوث - وقت اكتشاف غير صحيح

كما هو موضّح في مستندات مطوّري تطبيقات Android حول البلوتوث، عند ضبط واجهة البلوتوث داخل التطبيق، سيؤدي استخدام طريقة startActivityForResult(Intent, int) لتفعيل إمكانية رصد الجهاز وضبط EXTRA_DISCOVERABLE_DURATION على صفر إلى إتاحة رصد الجهاز طالما أنّ التطبيق يعمل في الخلفية أو المقدّمة. في ما يتعلق بمواصفات البلوتوث الكلاسيكية، فإنّ الأجهزة القابلة للاكتشاف تبث باستمرار رسائل اكتشاف محددة تتيح للأجهزة الأخرى استرداد بيانات الجهاز أو الاتصال به. في مثل هذه الحالة، يمكن لجهة خارجية ضارة اعتراض هذه الرسائل والاتصال بجهاز Android. وبعد الربط، يمكن للمهاجم تنفيذ هجمات أخرى، مثل سرقة البيانات أو حجب الخدمة أو حقن الأوامر.

إجراءات التخفيف

يجب ألا يتم ضبط قيمة EXTRA_DISCOVERABLE_DURATION على صفر أبدًا. إذا لم يتم ضبط المَعلمة EXTRA_DISCOVERABLE_DURATION، سيجعل نظام التشغيل Android الأجهزة قابلة للاكتشاف لمدة دقيقتَين تلقائيًا. الحدّ الأقصى للقيمة التي يمكن ضبطها للمَعلمة EXTRA_DISCOVERABLE_DURATION هو ساعتان (7200 ثانية). يُنصح بأن تكون مدة إتاحة الجهاز للاكتشاف هي الأقصر الممكنة، وفقًا لحالة استخدام التطبيق.


المخاطرة: الاتصال القصير المدى (NFC) - فلاتر الأهداف المستنسَخة

يمكن لتطبيق ضار تسجيل فلاتر الأهداف لقراءة علامات NFC معيّنة أو الأجهزة المتوافقة مع NFC. ويمكن لهذه الفلاتر محاكاة الفلاتر التي يحدّدها تطبيق شرعي، ما يتيح للمهاجم قراءة محتوى بيانات NFC المتبادلة. يُرجى العِلم أنّه عندما يحدّد نشاطان فلاتر الأهداف نفسها لعلامة NFC معيّنة، سيتم عرض أداة اختيار النشاط، وبالتالي سيظل على المستخدم اختيار التطبيق الضار لكي تنجح عملية الهجوم. ومع ذلك، يظل هذا السيناريو ممكنًا عند الجمع بين فلاتر الأهداف وإخفاء الهوية. لا يكون هذا الهجوم مهمًا إلا في الحالات التي يمكن فيها اعتبار البيانات المتبادلة عبر NFC حساسة للغاية.

إجراءات التخفيف

عند تنفيذ إمكانات قراءة NFC داخل تطبيق، يمكن استخدام فلاتر الأهداف مع سجلات تطبيقات Android (AAR). سيؤدي تضمين سجلّ AAR داخل رسالة NDEF إلى تقديم ضمان قوي بأنّه سيتم بدء التطبيق الشرعي فقط ونشاط معالجة NDEF المرتبط به. سيمنع ذلك التطبيقات أو الأنشطة غير المرغوب فيها من قراءة البيانات الحساسة جدًا للعلامات أو الأجهزة التي يتم تبادلها من خلال NFC.


المخاطرة: الاتصال القصير المدى (NFC) – عدم التحقّق من صحة رسالة NDEF

عندما يتلقّى جهاز يعمل بنظام التشغيل Android بيانات من علامة NFC أو جهاز مزوّد بتقنية NFC، يبدأ النظام تلقائيًا في تشغيل التطبيق أو النشاط المحدّد الذي تم ضبطه للتعامل مع رسالة NDEF المتضمّنة. وفقًا للمنطق المضمّن في التطبيق، يمكن عرض البيانات الواردة في العلامة أو التي تم تلقّيها من الجهاز على أنشطة أخرى لتشغيل إجراءات إضافية، مثل فتح صفحات الويب.

قد يسمح تطبيق لا يتضمّن التحقّق من صحة محتوى رسالة NDEF للمهاجمين باستخدام أجهزة متوافقة مع تقنية NFC أو علامات NFC لإدخال حمولات ضارة في التطبيق، ما يؤدي إلى سلوك غير متوقّع قد ينتج عنه تنزيل ملف ضار أو إدخال أوامر أو هجوم رفض الخدمة.

إجراءات التخفيف

قبل إرسال رسالة 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;
    }

الموارد