הגדרה לא מאובטחת של תקשורת בין מכונות

קטגוריה של OWASP: MASVS-CODE: איכות הקוד

סקירה כללית

לא נדיר לראות אפליקציות שמטמיעות פונקציונליות שמאפשרת למשתמשים להעביר נתונים או ליצור אינטראקציה עם מכשירים אחרים באמצעות תקשורת בתדרי רדיו (RF) או חיבורים בכבלים. הטכנולוגיות הנפוצות ביותר שמשמשות ב-Android למטרה הזו הן Bluetooth קלאסי (Bluetooth BR/EDR),‏ Bluetooth Low Energy‏ (BLE),‏ Wi-Fi P2P,‏ NFC ו-USB.

בדרך כלל מטמיעים את הטכנולוגיות האלה באפליקציות שאמורות לתקשר עם אביזרים לבית החכם, מכשירים למעקב אחר בריאות, קיוסקים של תחבורה ציבורית, מסופי תשלום ומכשירים אחרים שמבוססים על Android.

כמו בכל ערוץ אחר, תקשורת בין מכונות חשופה למתקפות שמטרתן לפרוץ את גבולות האמון שנקבעו בין שני מכשירים או יותר. משתמשים זדוניים יכולים להשתמש בטכניקות כמו התחזות למכשיר כדי לבצע מספר רב של התקפות נגד ערוץ התקשורת.

מערכת Android מספקת למפתחים ממשקי API ספציפיים להגדרת תקשורת בין מכונות.

חשוב להשתמש בממשקי ה-API האלה בזהירות, כי שגיאות בהטמעה של פרוטוקולי תקשורת עלולות לגרום לחשיפה של נתוני משתמשים או מכשירים לצדדים שלישיים לא מורשים. בתרחיש הגרוע ביותר, התוקפים יוכלו להשתלט מרחוק על מכשיר אחד או יותר, וכתוצאה מכך לקבל גישה מלאה לתוכן במכשיר.

השפעה

ההשפעה עשויה להשתנות בהתאם לטכנולוגיה להעברת נתונים ממכשיר למכשיר שמיושמת באפליקציה.

שימוש או הגדרה לא נכונים של ערוצי תקשורת בין מכונות עלולים לחשוף את מכשיר המשתמש לניסיונות תקשורת לא מהימנים. זה עלול להפוך את המכשיר לפגיע להתקפות נוספות כמו התקפות אדם בתווך (MiTM), החדרת פקודות, DoS או התחזות.

סיכון: האזנה למידע אישי רגיש בערוצים אלחוטיים

כשמיישמים מנגנוני תקשורת בין מכונות, חשוב לשקול בקפידה את הטכנולוגיה שבה משתמשים ואת סוג הנתונים שצריך להעביר. בפועל, חיבורים באמצעות כבלים מאובטחים יותר למשימות כאלה, כי הם דורשים קישור פיזי בין המכשירים המעורבים. עם זאת, אפשר ליירט פרוטוקולי תקשורת שמשתמשים בתדרי רדיו, כמו Bluetooth קלאסי, BLE,‏ NFC ו-Wifi P2P. יכול להיות שתוקף יוכל להתחזות לאחד מהמסופים או לנקודות הגישה שמעורבים בחילופי הנתונים, ליירט את התקשורת בחיבור אלחוטי וכתוצאה מכך לקבל גישה לנתוני משתמש רגישים. בנוסף, אפליקציות זדוניות שמותקנות במכשיר, אם ניתנו להן הרשאות בתחילת ההפעלה שספציפיות לתקשורת, עשויות להיות מסוגלות לאחזר נתונים שהועברו בין המכשירים על ידי קריאת מאגרי הודעות המערכת.

אמצעי צמצום סיכונים

אם האפליקציה דורשת החלפת מידע אישי רגיש בין מכונות דרך ערוצים אלחוטיים, צריך להטמיע בקוד של האפליקציה פתרונות אבטחה בשכבת האפליקציה, כמו הצפנה. הפעולה הזו תמנע מתוקפים לרחרח את ערוץ התקשורת ולאחזר את הנתונים שהועברו בטקסט גלוי. למידע נוסף, אפשר לעיין במאמרי העזרה בנושא קריפטוגרפיה.


סיכון: החדרת נתונים זדוניים לרשת אלחוטית

אפשר לשנות את הערוצים של תקשורת אלחוטית בין מכונות (Bluetooth קלאסי, ‏ BLE, ‏ NFC,‏ Wi-Fi P2P) באמצעות נתונים זדוניים. תוקפים מיומנים מספיק יכולים לזהות את פרוטוקול התקשורת שנמצא בשימוש ולשנות את זרימת חילופי הנתונים, למשל על ידי התחזות לאחת מנקודות הקצה ושליחת מטען ייעודי (payload) שנוצר במיוחד. תנועה זדונית כזו עלולה לפגוע בפונקציונליות של האפליקציה, ובמקרה הגרוע ביותר לגרום להתנהגות בלתי צפויה של האפליקציה והמכשיר, או להוביל למתקפות כמו DoS, הזרקת פקודות או השתלטות על המכשיר.

אמצעי צמצום סיכונים

‫Android מספקת למפתחים ממשקי API עוצמתיים לניהול תקשורת בין מכונות, כמו Bluetooth קלאסי, ‏BLE, ‏NFC ו-Wi-Fi P2P. ההגדרות האלה צריכות להיות משולבות עם לוגיקה של אימות נתונים שיושמה בקפידה, כדי לבצע סניטציה של כל הנתונים שמועברים בין שני מכשירים.

הפתרון הזה צריך להיות מוטמע ברמת האפליקציה, והוא צריך לכלול בדיקות שמוודאות שהנתונים הם באורך ובפורמט הצפויים, ושהם מכילים מטען ייעודי (payload) תקין שאפשר לפרש אותו באפליקציה.

בקטע הקוד הבא מוצגת לוגיקה לדוגמה של אימות נתונים. ההטמעה הזו בוצעה על בסיס הדוגמה של מפתחי 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 שהאפליקציה יכולה לקבל ממכשיר ה-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 – זמן גילוי שגוי

כפי שמודגש במסמכי התיעוד למפתחי Android בנושא Bluetooth, כשמגדירים את ממשק ה-Bluetooth באפליקציה, שימוש בשיטה startActivityForResult(Intent, int) כדי להפעיל את האפשרות לגלות את המכשיר והגדרת הערך EXTRA_DISCOVERABLE_DURATION לאפס יגרמו לכך שיהיה אפשר לגלות את המכשיר כל עוד האפליקציה פועלת ברקע או בחזית. בנוגע למפרט Bluetooth קלאסי, מכשירים שניתן לגלות אותם משדרים כל הזמן הודעות גילוי ספציפיות שמאפשרות למכשירים אחרים לאחזר נתוני מכשיר או להתחבר אליו. בתרחיש כזה, צד שלישי זדוני יכול ליירט את ההודעות האלה ולהתחבר למכשיר עם Android. אחרי החיבור, תוקף יכול לבצע מתקפות נוספות כמו גניבת נתונים, מניעת שירות או החדרת פקודות.

אמצעי צמצום סיכונים

הערך של EXTRA_DISCOVERABLE_DURATION אף פעם לא יכול להיות אפס. אם הפרמטר EXTRA_DISCOVERABLE_DURATION לא מוגדר, מכשירי Android יהיו גלויים למשך 2 דקות כברירת מחדל. הערך המקסימלי שאפשר להגדיר לפרמטר EXTRA_DISCOVERABLE_DURATION הוא שעתיים (7,200 שניות). מומלץ להגדיר את משך הזמן שבו המכשיר גלוי לזמן הקצר ביותר האפשרי, בהתאם לתרחיש השימוש באפליקציה.


סיכון: NFC – מסנני כוונות משובטים

אפליקציה זדונית יכולה לרשום מסנני Intent כדי לקרוא תגי NFC ספציפיים או מכשירים עם NFC. המסננים האלה יכולים לשכפל את המסננים שהוגדרו על ידי אפליקציה לגיטימית, וכך לאפשר לתוקף לקרוא את התוכן של נתוני ה-NFC שהועברו. חשוב לציין שאם שתי פעילויות מציינות את אותם מסנני כוונות לתג NFC ספציפי, מוצג כלי לבחירת פעילות, ולכן המשתמש עדיין צריך לבחור את האפליקציה הזדונית כדי שהמתקפה תצליח. עם זאת, שילוב של מסנני כוונות עם הסוואה עדיין מאפשר את התרחיש הזה. המתקפה הזו משמעותית רק במקרים שבהם הנתונים שמועברים באמצעות NFC נחשבים לנתונים רגישים מאוד.

אמצעי צמצום סיכונים

כשמטמיעים יכולות קריאת NFC באפליקציה, אפשר להשתמש ב-intent-filters יחד עם רשומות של אפליקציות ל-Android (AAR). הטמעה של רשומת AAR בתוך הודעת NDEF תבטיח שרק האפליקציה הלגיטימית והפעילות המשויכת שלה לטיפול ב-NDEF יופעלו. כך אפשר למנוע מאפליקציות או מפעילויות לא רצויות לקרוא נתונים רגישים מאוד של תגים או הנתונים במכשיר שמועברים באמצעות NFC.


סיכון: NFC – חוסר אימות של הודעות NDEF

כשמכשיר עם Android מקבל נתונים מתג NFC או ממכשיר עם NFC, המערכת מפעילה באופן אוטומטי את האפליקציה או את הפעילות הספציפית שהוגדרה לטיפול בהודעת NDEF שכלולה בנתונים. בהתאם ללוגיקה שהוטמעה באפליקציה, אפשר להציג את הנתונים שכלולים בתג או שמתקבלים מהמכשיר בפעילויות אחרות כדי להפעיל פעולות נוספות, כמו פתיחת דפי אינטרנט.

אפליקציה שאין בה אימות של תוכן הודעת NDEF עלולה לאפשר לתוקפים להשתמש במכשירים עם תמיכה ב-NFC או בתגי NFC כדי להחדיר מטענים ייעודיים (payloads) זדוניים לאפליקציה. כך הם עלולים לגרום להתנהגות בלתי צפויה שתוביל להורדה של קובץ זדוני, להחדרת פקודות או ל-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;
    }

משאבים