การตั้งค่าการสื่อสารระหว่างเครื่องที่ไม่ปลอดภัย

หมวดหมู่ OWASP: MASVS-CODE: คุณภาพของโค้ด

ภาพรวม

เรามักจะเห็นแอปพลิเคชันที่ใช้ฟังก์ชันที่อนุญาตให้ผู้ใช้โอนข้อมูลหรือโต้ตอบกับอุปกรณ์อื่นๆ โดยใช้การสื่อสารความถี่วิทยุ (RF) หรือการเชื่อมต่อแบบมีสาย เทคโนโลยีที่ใช้กันมากที่สุดใน Android เพื่อจุดประสงค์นี้คือบลูทูธแบบคลาสสิก (Bluetooth BR/EDR), บลูทูธพลังงานต่ำ (BLE), Wifi P2P, NFC และ USB

โดยปกติแล้ว เทคโนโลยีเหล่านี้จะนำไปใช้ในแอปพลิเคชันที่คาดว่าจะสื่อสารกับอุปกรณ์เสริมสมาร์ทโฮม อุปกรณ์ตรวจสอบสุขภาพ คีออสระบบขนส่งสาธารณะ เครื่องชำระเงิน และอุปกรณ์อื่นๆ ที่ใช้ Android

เช่นเดียวกับช่องทางอื่นๆ การสื่อสารระหว่างเครื่องก็อาจถูกโจมตีได้ ซึ่งมีเป้าหมายเพื่อทำลายขอบเขตความน่าเชื่อถือที่สร้างขึ้นระหว่างอุปกรณ์ 2 เครื่องขึ้นไป ผู้ใช้ที่เป็นอันตรายอาจใช้ประโยชน์จากเทคนิคต่างๆ เช่น การแอบอ้างเป็นอุปกรณ์ เพื่อทำการโจมตีจำนวนมากกับช่องทางการสื่อสาร

Android มี API เฉพาะสำหรับการกำหนดค่าการสื่อสารแบบเครื่องต่อเครื่อง ที่พร้อมให้บริการแก่นักพัฒนาแอป

คุณควรใช้ API เหล่านี้อย่างระมัดระวัง เนื่องจากข้อผิดพลาดขณะใช้โปรโตคอลการสื่อสาร อาจทำให้ข้อมูลผู้ใช้หรือข้อมูลอุปกรณ์ถูกเปิดเผยต่อบุคคลที่สามที่ไม่ได้รับอนุญาต ในกรณีที่แย่ที่สุด ผู้โจมตีอาจเข้าควบคุมอุปกรณ์อย่างน้อย 1 เครื่องจากระยะไกล และเข้าถึงเนื้อหาในอุปกรณ์ได้อย่างเต็มที่

ผลกระทบ

ผลกระทบอาจแตกต่างกันไปตามเทคโนโลยีแบบอุปกรณ์ต่ออุปกรณ์ที่ใช้ใน แอปพลิเคชัน

การใช้งานหรือการกำหนดค่าช่องทางการสื่อสารแบบเครื่องต่อเครื่องที่ไม่ถูกต้องอาจทำให้อุปกรณ์ของผู้ใช้เสี่ยงต่อความพยายามในการสื่อสารที่ไม่น่าเชื่อถือ ซึ่งอาจทำให้อุปกรณ์มีช่องโหว่ที่จะถูกโจมตีเพิ่มเติม เช่น การโจมตีแบบแทรกกลางการสื่อสาร (Man-In-The-Middle หรือ MITM), การแทรกคำสั่ง, การโจมตีแบบปฏิเสธการให้บริการ (DoS) หรือการโจมตีแบบแอบอ้างเป็นบุคคลอื่น

ความเสี่ยง: การดักฟังข้อมูลที่ละเอียดอ่อนผ่านช่องทางไร้สาย

เมื่อติดตั้งกลไกการสื่อสารแบบเครื่องต่อเครื่อง ควรพิจารณาอย่างรอบคอบทั้งเทคโนโลยีที่ใช้และประเภทข้อมูลที่ควรส่ง แม้ว่าการเชื่อมต่อแบบใช้สายจะปลอดภัยกว่าในทางปฏิบัติสำหรับงานดังกล่าว เนื่องจากต้องมีการเชื่อมต่อทางกายภาพระหว่างอุปกรณ์ที่เกี่ยวข้อง แต่โปรโตคอลการสื่อสารที่ใช้ความถี่วิทยุ เช่น บลูทูธคลาสสิก, BLE, NFC และ Wi-Fi P2P อาจถูกดักฟังได้ ผู้โจมตีอาจแอบอ้างเป็นหนึ่งในเทอร์มินัลหรือจุดเข้าถึงที่เกี่ยวข้องกับการแลกเปลี่ยนข้อมูล ดักฟังการสื่อสารผ่านอากาศ และเข้าถึงข้อมูลผู้ใช้ที่ละเอียดอ่อนได้ในที่สุด นอกจากนี้ แอปพลิเคชันที่เป็นอันตรายซึ่งติดตั้งในอุปกรณ์ หากได้รับสิทธิ์รันไทม์เฉพาะการสื่อสาร อาจดึงข้อมูลที่แลกเปลี่ยนระหว่างอุปกรณ์ได้โดยการอ่านบัฟเฟอร์ข้อความของระบบ

การลดปัญหา

หากแอปพลิเคชันจำเป็นต้องมีการแลกเปลี่ยนข้อมูลที่ละเอียดอ่อนแบบเครื่องต่อเครื่อง ผ่านช่องทางไร้สาย คุณควรใช้โซลูชันด้านความปลอดภัยที่เลเยอร์แอปพลิเคชัน เช่น การเข้ารหัส ในโค้ดของแอปพลิเคชัน ซึ่งจะป้องกันไม่ให้ ผู้โจมตีดักฟังช่องทางการสื่อสารและดึงข้อมูลที่ แลกเปลี่ยนเป็นข้อความธรรมดา ดูแหล่งข้อมูลเพิ่มเติมได้ในเอกสารประกอบการเข้ารหัส


ความเสี่ยง: การแทรกข้อมูลที่เป็นอันตรายแบบไร้สาย

ช่องทางการสื่อสารแบบไร้สายจากเครื่องสู่เครื่อง (บลูทูธคลาสสิก, BLE, NFC, Wifi P2P) อาจถูกดัดแปลงโดยใช้ข้อมูลที่เป็นอันตราย ผู้โจมตีที่มีทักษะเพียงพอจะระบุโปรโตคอลการสื่อสารที่ใช้และดัดแปลง ขั้นตอนการแลกเปลี่ยนข้อมูลได้ เช่น โดยการแอบอ้างเป็นปลายทางใดปลายทางหนึ่ง ส่ง เพย์โหลดที่สร้างขึ้นมาโดยเฉพาะ การเข้าชมที่เป็นอันตรายประเภทนี้อาจลดประสิทธิภาพการทำงานของแอปพลิเคชัน และในกรณีที่เลวร้ายที่สุดอาจทำให้เกิดลักษณะการทำงานที่ไม่คาดคิดของแอปพลิเคชันและอุปกรณ์ หรือทำให้เกิดการโจมตี เช่น DoS, การแทรกคำสั่ง หรือการยึดครองอุปกรณ์

การลดปัญหา

Android มี API ที่มีประสิทธิภาพสำหรับนักพัฒนาแอปเพื่อจัดการการสื่อสารระหว่างเครื่อง เช่น บลูทูธคลาสสิก, BLE, NFC และ Wi-Fi P2P โดยควรใช้ร่วมกับตรรกะการตรวจสอบข้อมูลที่ใช้ด้วยความระมัดระวัง เพื่อล้างข้อมูลที่แลกเปลี่ยนระหว่างอุปกรณ์ 2 เครื่อง

โซลูชันนี้ควรนำไปใช้ในระดับแอปพลิเคชันและควรมีการตรวจสอบเพื่อยืนยันว่าข้อมูลมีความยาวและรูปแบบตามที่คาดไว้ รวมถึงมีเพย์โหลดที่ถูกต้องซึ่งแอปพลิเคชันสามารถตีความได้

ข้อมูลโค้ดต่อไปนี้แสดงตรรกะการตรวจสอบข้อมูลตัวอย่าง การดำเนินการนี้ใช้ตัวอย่างของนักพัฒนาแอป 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 ระหว่างอุปกรณ์ 2 เครื่องอาจตกเป็นเป้าหมายของผู้ใช้ที่เป็นอันตราย ซึ่งสนใจดักฟังการสื่อสาร ในกรณีนี้ ลิงก์ทางกายภาพ ที่จำเป็นถือเป็นเลเยอร์ความปลอดภัยเพิ่มเติม เนื่องจากผู้โจมตีต้องได้รับ สิทธิ์เข้าถึงสายเคเบิลที่เชื่อมต่อเทอร์มินัลจึงจะสามารถดักฟังข้อความ ได้ เวกเตอร์การโจมตีอีกอย่างคืออุปกรณ์ 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 เป็น 0 จะทําให้อุปกรณ์ค้นพบได้ตราบใดที่แอปพลิเคชันทํางานอยู่เบื้องหลังหรือเบื้องหน้า สำหรับข้อกำหนดบลูทูธแบบคลาสสิก อุปกรณ์ที่ค้นพบได้จะออกอากาศข้อความการค้นหาที่เฉพาะเจาะจงอย่างต่อเนื่อง ซึ่งช่วยให้อุปกรณ์อื่นๆ ดึงข้อมูลอุปกรณ์หรือเชื่อมต่อกับอุปกรณ์ได้ ใน สถานการณ์ดังกล่าว บุคคลที่สามที่เป็นอันตรายอาจดักรับข้อความดังกล่าวและเชื่อมต่อ กับอุปกรณ์ที่ใช้ Android ได้ เมื่อเชื่อมต่อแล้ว ผู้โจมตีจะทำการโจมตีเพิ่มเติมได้ เช่น การขโมยข้อมูล, DoS หรือการแทรกคำสั่ง

การลดปัญหา

ไม่ควรกำหนดให้ EXTRA_DISCOVERABLE_DURATION เป็น 0 หากไม่ได้ตั้งค่าพารามิเตอร์ EXTRA_DISCOVERABLE_DURATION โดยค่าเริ่มต้น Android จะทำให้อุปกรณ์ ค้นพบได้เป็นเวลา 2 นาที ค่าสูงสุดที่ตั้งค่าได้สำหรับพารามิเตอร์ EXTRA_DISCOVERABLE_DURATION คือ 2 ชั่วโมง (7200 วินาที) เราขอแนะนำให้กำหนดระยะเวลาที่ค้นพบได้ให้สั้นที่สุดเท่าที่จะเป็นไปได้ตาม Use Case ของแอปพลิเคชัน


ความเสี่ยง: NFC - ตัวกรอง Intent ที่โคลน

แอปพลิเคชันที่เป็นอันตรายสามารถลงทะเบียนตัวกรอง Intent เพื่ออ่านแท็ก NFC ที่เฉพาะเจาะจงหรืออุปกรณ์ที่เปิดใช้ NFC ได้ ตัวกรองเหล่านี้สามารถจำลองตัวกรองที่กำหนดโดยแอปพลิเคชันที่ถูกต้อง ทำให้ผู้โจมตีอ่านเนื้อหาของข้อมูล NFC ที่แลกเปลี่ยนได้ โปรดทราบว่าเมื่อกิจกรรม 2 รายการระบุ ตัวกรอง Intent เดียวกันสำหรับแท็ก NFC ที่เฉพาะเจาะจง ระบบจะแสดงตัวเลือกกิจกรรม ดังนั้นผู้ใช้จะต้องเลือกแอปพลิเคชันที่เป็นอันตรายเพื่อให้การโจมตีสำเร็จ อย่างไรก็ตาม การรวมตัวกรอง Intent กับการปกปิดยังคงทำให้สถานการณ์นี้เกิดขึ้นได้ การโจมตีนี้มีความสำคัญเฉพาะในกรณีที่ข้อมูลที่แลกเปลี่ยนผ่าน NFC ถือเป็นข้อมูลที่มีความละเอียดอ่อนสูง

การลดปัญหา

เมื่อใช้ความสามารถในการอ่าน NFC ภายในแอปพลิเคชัน คุณจะใช้ตัวกรอง Intent ร่วมกับบันทึกแอปพลิเคชัน Android (AAR) ได้ การฝังระเบียน AAR ไว้ในข้อความ NDEF จะช่วยให้มั่นใจได้ว่ามีเพียงแอปพลิเคชันที่ถูกต้องและกิจกรรมการจัดการ NDEF ที่เกี่ยวข้องเท่านั้นที่จะเริ่มต้น ซึ่งจะป้องกันไม่ให้แอปพลิเคชันหรือกิจกรรมที่ไม่ต้องการอ่านแท็กหรือข้อมูลอุปกรณ์ที่มีความละเอียดอ่อนสูง ซึ่งแลกเปลี่ยนผ่าน NFC


ความเสี่ยง: NFC - การตรวจสอบข้อความ NDEF ไม่สำเร็จ

เมื่ออุปกรณ์ที่ใช้ระบบ Android ได้รับข้อมูลจากแท็ก NFC หรืออุปกรณ์ที่เปิดใช้ NFC ระบบจะเรียกใช้แอปพลิเคชันหรือกิจกรรมที่เฉพาะเจาะจง ซึ่งกำหนดค่าให้จัดการข้อความ NDEF ที่อยู่ในแท็กหรืออุปกรณ์นั้นโดยอัตโนมัติ ตามตรรกะที่ใช้ในแอปพลิเคชัน ข้อมูลที่อยู่ในแท็ก หรือได้รับจากอุปกรณ์จะแสดงต่อกิจกรรมอื่นๆ เพื่อทริกเกอร์ การดำเนินการเพิ่มเติม เช่น การเปิดหน้าเว็บ

แอปพลิเคชันที่ไม่มีการตรวจสอบเนื้อหาข้อความ NDEF อาจอนุญาตให้ผู้โจมตี ใช้อุปกรณ์ที่เปิดใช้ NFC หรือแท็ก NFC เพื่อแทรกเพย์โหลดที่เป็นอันตรายภายใน แอปพลิเคชัน ซึ่งทำให้เกิดลักษณะการทำงานที่ไม่คาดคิดซึ่งอาจส่งผลให้มีการดาวน์โหลดไฟล์ที่เป็นอันตราย การแทรกคำสั่ง หรือ DoS

การลดปัญหา

ก่อนส่งข้อความ NDEF ที่ได้รับไปยังคอมโพเนนต์ของแอปพลิเคชันอื่นๆ ควรตรวจสอบข้อมูลภายในเพื่อให้แน่ใจว่าอยู่ในรูปแบบที่คาดไว้และมีข้อมูลที่คาดไว้ ซึ่งจะช่วยป้องกันไม่ให้มีการส่งข้อมูลที่เป็นอันตรายไปยังคอมโพเนนต์ของแอปพลิเคชันอื่นๆ โดยไม่มีการกรอง ซึ่งจะช่วยลดความเสี่ยงของลักษณะการทำงานที่ไม่คาดคิดหรือการโจมตีโดยใช้ข้อมูล NFC ที่มีการดัดแปลง

ข้อมูลโค้ดต่อไปนี้แสดงตรรกะการตรวจสอบข้อมูลตัวอย่างที่ใช้เป็นเมธอดที่มีข้อความ NDEF เป็นอาร์กิวเมนต์และดัชนีในอาร์เรย์ข้อความ เราได้ใช้โค้ดนี้กับตัวอย่างของนักพัฒนาแอป Android เพื่อรับข้อมูลจากแท็ก NDEF ของ NFC ที่สแกนแล้ว

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

แหล่งข้อมูล