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

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

ภาพรวม

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

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

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

Android สร้าง API เฉพาะสำหรับการกำหนดค่าเครื่องไปยังเครื่องคอมพิวเตอร์ ให้นักพัฒนาซอฟต์แวร์เข้าถึงได้

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

ผลกระทบ

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

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

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

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

การผ่อนปรนชั่วคราว

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


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

ช่องทางการสื่อสารแบบไร้สายระหว่างอุปกรณ์กับอุปกรณ์ (บลูทูธคลาสสิก, BLE, NFC, Wi-Fi 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 ช่วงเวลานี้ ช่วยป้องกันอุปกรณ์ที่เป็นอันตรายไม่ให้ทำการโจมตีอย่าง "เป็ดยาง"

การตรวจสอบนี้ทำได้โดยการสร้างชุดข้อความใหม่เพื่อตรวจสอบเนื้อหาบัฟเฟอร์ เช่น เมื่อ 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 ชั่วโมง (7,200 วินาที) ใช่เลย แนะนำให้กำหนดระยะเวลาที่ค้นพบได้ให้สั้นที่สุด ตามกรณีการใช้งานของแอปพลิเคชัน


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

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

การลดปัญหา

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


ความเสี่ยง: NFC – ไม่มีการยืนยันข้อความ NDEF

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

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

แหล่งข้อมูล