راه‌اندازی ارتباط ماشین‌به‌ماشین ناامن

دسته OWASP: MASVS-CODE: کیفیت کد

نمای کلی

دیدن برنامه‌هایی که عملکردی را پیاده‌سازی می‌کنند که به کاربران اجازه می‌دهد بااستفاده از ارتباطات فرکانس رادیویی (RF) یا اتصالات کابلی، داده‌ها را انتقال دهند یا با دستگاه‌های دیگر تعامل داشته باشند، غیرمعمول نیست. رایج‌ترین فناوری‌های مورداستفاده در Android برای این منظور عبارت‌اند از بلوتوث کلاسیک (Bluetooth BR/EDR)، بلوتوث کم‌مصرف (BLE)،‏ Wifi P2P،‏ NFC، و USB.

این فناوری‌ها معمولاً در برنامه‌هایی پیاده‌سازی می‌شوند که انتظار می‌رود با لوازم جانبی خانه هوشمند، دستگاه‌های پایش سلامت، کیوسک‌های حمل‌ونقل عمومی، پایانه‌های پرداخت، و دیگر دستگاه‌های مجهز به Android ارتباط برقرار کنند.

مانند هر کانال دیگری، ارتباطات ماشین به ماشین دربرابر حملاتی که هدفشان به‌خطر انداختن مرز اعتماد ایجادشده بین دو یا چند دستگاه است آسیب‌پذیر هستند. کاربران مخرب می‌توانند از تکنیک‌هایی مانند جعل هویت دستگاه برای انجام حملات گسترده علیه کانال ارتباطی استفاده کنند.

Android میاناهای برنامه‌سازی کاربردی خاصی را برای پیکربندی ارتباطات ماشین به ماشین دراختیار توسعه‌دهندگان قرار می‌دهد.

از این میاناهای برنامه‌سازی کاربردی باید با دقت استفاده شود زیرا خطا در اجرای پروتکل‌های ارتباطی ممکن است منجر به افشای داده‌های کاربر یا دستگاه برای اشخاص ثالث غیرمجاز شود. در بدترین حالت، مهاجمان ممکن است بتوانند از راه دور کنترل یک یا چند دستگاه را به‌دست بگیرند و درنتیجه به محتوای موجود در دستگاه دسترسی کامل پیدا کنند.

تأثیر

تأثیر ممکن است بسته به فناوری دستگاه‌به‌دستگاه پیاده‌سازی‌شده در برنامه متفاوت باشد.

استفاده یا پیکربندی نادرست کانال‌های ارتباطی ماشین به ماشین ممکن است دستگاه کاربر را درمعرض تلاش‌های ارتباطی غیرقابل‌اعتماد قرار دهد. این امر می‌تواند باعث شود دستگاه دربرابر حملات دیگری مثل حمله مرد میانی (MiTM)، تزریق فرمان، DoS، یا حملات جعل هویت آسیب‌پذیر شود.

خطر: شنود داده‌های حساس ازطریق کانال‌های بی‌سیم

هنگام پیاده‌سازی سازوکارهای ارتباط ماشین به ماشین، باید به فناوری مورداستفاده و نوع داده‌هایی که باید منتقل شوند توجه دقیق شود. درحالی‌که اتصالات کابلی در عمل برای چنین کارهایی ایمن‌تر هستند، زیرا به پیوند فیزیکی بین دستگاه‌های درگیر نیاز دارند، پروتکل‌های ارتباطی که از فرکانس‌های رادیویی استفاده می‌کنند، مانند بلوتوث کلاسیک، BLE، NFC، و Wifi P2P، می‌توانند رهگیری شوند. مهاجم ممکن است بتواند خود را به‌جای یکی از پایانه‌ها یا نقاط دسترسی دخیل در تبادل داده جا بزند، ارتباطات را ازطریق هوا رهگیری کند، و درنتیجه به داده‌های حساس کاربر دسترسی پیدا کند. علاوه‌براین، اگر به برنامه‌های مخرب نصب‌شده در دستگاه اجازه‌های زمان اجرای مختص ارتباطات اعطا شده باشد، ممکن است بتوانند با خواندن میانگیرهای پیام سیستم، داده‌های ردوبدل‌شده بین دستگاه‌ها را بازیابی کنند.

کاهش

اگر برنامه نیاز به تبادل داده‌های حساس بین ماشین‌ها ازطریق کانال‌های بی‌سیم دارد، باید راهکارهای امنیتی لایه برنامه، مانند رمزگذاری، در کد برنامه پیاده‌سازی شود. این کار از شنود کانال ارتباطی توسط مهاجمان و بازیابی داده‌های مبادله‌شده به‌صورت نوشتار ساده جلوگیری می‌کند. برای منابع بیشتر، به سند رمزنگاری مراجعه کنید.


خطر: تزریق داده‌های مخرب بی‌سیم

کانال‌های ارتباطی بی‌سیم ماشین به ماشین (بلوتوث کلاسیک، BLE،‏ NFC، Wifi P2P) ممکن است بااستفاده از داده‌های مخرب دستکاری شوند. مهاجمان با مهارت کافی می‌توانند پروتکل ارتباطی مورداستفاده را شناسایی کنند و در جریان تبادل داده دست ببرند، برای مثال با جعل هویت یکی از نقاط پایانی، محموله‌هایی را که به‌طور خاص ساخته شده‌اند ارسال کنند. این نوع ترافیک مخرب ممکن است عملکرد برنامه را تضعیف کند و در بدترین سناریو، باعث رفتار غیرمنتظره برنامه و دستگاه شود یا منجر به حملاتی مانند DoS، تزریق فرمان، یا تصاحب دستگاه شود.

کاهش

Android میاناهای برنامه‌سازی کاربردی قدرتمندی را برای مدیریت ارتباطات ماشین به ماشین، مانند بلوتوث کلاسیک، BLE،‏ NFC، و Wifi P2P، دراختیار توسعه‌دهندگان قرار می‌دهد. این‌ها باید با منطق درستی‌سنجی داده که به‌دقت پیاده‌سازی شده است ترکیب شوند تا هرگونه داده‌ای که بین دو دستگاه ردوبدل می‌شود پاک‌سازی شود.

این راه‌حل باید در سطح برنامه اجرا شود و باید شامل بررسی‌هایی باشد که تأیید کند داده‌ها طول و قالب موردانتظار را دارند و حاوی بار داده معتبری هستند که برنامه می‌تواند آن را تفسیر کند.

تکه‌کد زیر نمونه‌ای از منطق اعتبارسنجی داده را نشان می‌دهد. این مورد براساس نمونه توسعه‌دهندگان Android برای پیاده‌سازی انتقال داده ازطریق بلوتوث پیاده‌سازی شده است:

کاتلین

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

جاوا

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 دریافت کند درنظر گرفته شود. این کار از انجام حملاتی مانند «اردک لاستیکی» توسط دستگاه‌های مخرب جلوگیری می‌کند.

این اعتبارسنجی را می‌توان با ایجاد رشته‌ای جدید برای بررسی محتوای میان‌گیر انجام داد، برای مثال، پس‌از bulkTransfer:

کاتلین

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()
    }
}

جاوا

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

خطرات خاص

این بخش خطراتی را جمع‌آوری می‌کند که به استراتژی‌های کاهش غیراستاندارد نیاز دارند یا در سطح خاصی از کیت توسعه نرم‌افزار کاهش یافته‌اند و برای کامل بودن در اینجا آورده شده‌اند.

خطر: بلوتوث – زمان نادرست قابل‌شناسایی بودن

همان‌طور که در سند توسعه‌دهندگان Android درباره بلوتوث برجسته شده است، هنگام پیکربندی میانای بلوتوث در برنامه، استفاده از روش startActivityForResult(Intent, int) برای فعال کردن قابلیت شناسایی دستگاه و تنظیم EXTRA_DISCOVERABLE_DURATION روی صفر باعث می‌شود دستگاه تا زمانی که برنامه در پس‌زمینه یا پیش‌زمینه درحال اجرا است، قابل‌شناسایی باشد. طبق مشخصات بلوتوث کلاسیک، دستگاه‌های قابل‌کشف به‌طور مداوم پیام‌های کاوش خاصی را همه‌فرستی می‌کنند که به دستگاه‌های دیگر اجازه می‌دهد داده‌های دستگاه را بازیابی کنند یا به آن متصل شوند. در چنین سناریویی، شخص ثالث مخرب می‌تواند این پیام‌ها را رهگیری کند و به دستگاه Android متصل شود. پس‌از اتصال، مهاجم می‌تواند حملات بیشتری مانند سرقت داده، DoS، یا تزریق فرمان انجام دهد.

کاهش

EXTRA_DISCOVERABLE_DURATION هرگز نباید روی صفر تنظیم شود. اگر پارامتر EXTRA_DISCOVERABLE_DURATION تنظیم نشده باشد، Android به‌طور پیش‌فرض دستگاه‌ها را به‌مدت ۲ دقیقه قابل‌شناسایی می‌کند. حداکثر مقداری که می‌توان برای پارامتر EXTRA_DISCOVERABLE_DURATION تنظیم کرد ۲ ساعت (۷۲۰۰ ثانیه) است. توصیه می‌شود که مدت زمان قابل‌شناسایی بودن را براساس مورد استفاده برنامه روی کوتاه‌ترین زمان ممکن تنظیم کنید.


خطر: NFC – فیلترهای قصد کلون‌شده

یک برنامه مخرب می‌تواند فیلترهای قصد را برای خواندن برچسب‌های NFC خاص یا دستگاه‌های دارای NFC ثبت کند. این فیلترها می‌توانند فیلترهای تعریف‌شده توسط یک برنامه قانونی را تکرار کنند و به مهاجم امکان دهند محتوای داده‌های NFC مبادله‌شده را بخواند. باید توجه داشت که وقتی دو فعالیت فیلترهای قصد یکسانی را برای برچسب NFC خاصی مشخص می‌کنند، انتخابگر فعالیت ارائه می‌شود، بنابراین کاربر همچنان باید برنامه مخرب را انتخاب کند تا حمله موفقیت‌آمیز باشد. بااین‌حال، با ترکیب فیلترهای قصد با پنهان‌سازی، این سناریو همچنان امکان‌پذیر است. این حمله فقط در مواردی مهم است که داده‌های مبادله‌شده ازطریق NFC بسیار حساس تلقی شوند.

کاهش

هنگام پیاده‌سازی قابلیت‌های خواندن NFC در یک برنامه، فیلترهای قصد می‌توانند همراه با سوابق برنامه Android (AAR) استفاده شوند. جاسازی کردن سوابق AAR در پیام NDEF تضمین می‌کند که فقط برنامه قانونی و فعالیت مدیریت NDEF مرتبط با آن شروع می‌شود. این کار از خواندن داده‌های بسیار حساس برچسب یا دستگاه که ازطریق NFC مبادله می‌شود توسط برنامه‌ها یا فعالیت‌های ناخواسته جلوگیری می‌کند.


خطر: NFC – عدم اعتبارسنجی پیام NDEF

وقتی دستگاهی با سیستم‌عامل Android داده‌ها را از برچسب NFC یا دستگاهی با قابلیت NFC دریافت می‌کند، سیستم به‌طور خودکار برنامه یا فعالیت خاصی را که برای مدیریت پیام NDEF موجود در آن پیکربندی شده است، راه‌اندازی می‌کند. طبق منطق پیاده‌سازی‌شده در برنامه، داده‌های موجود در برچسب یا داده‌های دریافتی از دستگاه می‌تواند برای فعالیت‌های دیگر ارائه شود تا کنش‌های بیشتری را راه‌اندازی کند، مثلاً باز کردن صفحه‌های وب.

برنامه‌ای که فاقد اعتبارسنجی محتوای پیام NDEF باشد ممکن است به مهاجمان اجازه دهد از دستگاه‌های دارای NFC یا برچسب‌های NFC برای تزریق بار داده مخرب در برنامه استفاده کنند و باعث رفتار غیرمنتظره‌ای شوند که ممکن است منجر به بارگیری فایل مخرب، تزریق فرمان، یا DoS شود.

کاهش

قبل‌از ارسال پیام NDEF دریافتی به هر مؤلفه برنامه دیگر، داده‌های درون آن باید اعتبارسنجی شود تا در قالب موردانتظار باشد و حاوی اطلاعات موردانتظار باشد. این کار از انتقال داده‌های مخرب به مؤلفه‌های برنامه‌های دیگر بدون فیلتر شدن جلوگیری می‌کند و خطر رفتار غیرمنتظره یا حملات بااستفاده از داده‌های NFC دست‌کاری‌شده را کاهش می‌دهد.

قطعه زیر نمونه‌ای از منطق اعتبارسنجی داده را نشان می‌دهد که به‌عنوان روشی با پیام NDEF به‌عنوان آرگومان و نمایه آن در آرایه پیام‌ها پیاده‌سازی شده است. این مورد روی نمونه توسعه‌دهندگان Android برای دریافت داده از برچسب NFC NDEF اسکن‌شده پیاده‌سازی شده است:

کاتلین

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

جاوا

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

منابع