OWASP 카테고리: MASVS-CODE: 코드 품질
개요
무선 주파수 (RF) 통신 또는 케이블 연결을 사용하여 사용자가 데이터를 전송하거나 다른 기기와 상호작용할 수 있는 기능을 구현하는 애플리케이션은 드물지 않습니다. 이러한 목적으로 Android에서 사용되는 가장 일반적인 기술은 기본 블루투스 (Bluetooth BR/EDR), 저전력 블루투스 (BLE), Wi-Fi P2P, NFC, USB입니다.
이러한 기술은 일반적으로 스마트 홈 액세서리, 건강 모니터링 기기, 대중교통 키오스크, 결제 단말기, 기타 Android 구동 기기와 통신할 것으로 예상되는 애플리케이션에 구현됩니다.
다른 채널과 마찬가지로 머신 간 통신은 두 개 이상의 기기 간에 설정된 신뢰 경계를 손상시키려는 공격에 취약합니다. 기기 가장과 같은 기술은 악의적인 사용자가 통신 채널에 대한 광범위한 공격을 달성하는 데 활용할 수 있습니다.
Android는 머신 간 통신을 구성하기 위한 특정 API를 개발자에게 제공합니다.
통신 프로토콜을 구현하는 동안 오류가 발생하면 사용자 또는 기기 데이터가 승인되지 않은 서드 파티에 노출될 수 있으므로 이러한 API를 신중하게 사용해야 합니다. 최악의 경우 공격자가 하나 이상의 기기를 원격으로 장악하여 기기의 콘텐츠에 대한 전체 액세스 권한을 얻을 수 있습니다.
영향
영향은 애플리케이션에 구현된 기기 간 기술에 따라 다를 수 있습니다.
머신 간 통신 채널을 잘못 사용하거나 구성하면 사용자 기기가 신뢰할 수 없는 통신 시도에 노출될 수 있습니다. 이로 인해 기기가 중간자(MiTM), 명령어 삽입, 서비스 거부(DoS) 또는 가장 공격과 같은 추가 공격에 취약해질 수 있습니다.
위험: 무선 채널을 통한 민감한 정보 도청
머신 간 통신 메커니즘을 구현할 때는 사용된 기술과 전송해야 하는 데이터 유형을 모두 신중하게 고려해야 합니다. 케이블 연결은 관련 기기 간에 물리적 링크가 필요하므로 이러한 작업에 더 안전하지만 기본 블루투스, BLE, NFC, Wi-Fi P2P와 같은 무선 주파수를 사용하는 통신 프로토콜은 가로챌 수 있습니다. 공격자는 데이터 교환에 관련된 단말기 또는 액세스 포인트 중 하나를 가장하여 무선 통신을 가로채고 결과적으로 민감한 사용자 데이터에 액세스할 수 있습니다. 또한 기기에 설치된 악성 애플리케이션은 통신 관련 런타임 권한이 부여된 경우 시스템 메시지 버퍼를 읽어 기기 간에 교환된 데이터를 검색할 수 있습니다.
완화 조치
애플리케이션에 무선 채널을 통한 민감한 정보의 머신 간 교환이 필요한 경우 암호화와 같은 애플리케이션 계층 보안 솔루션을 애플리케이션의 코드에 구현해야 합니다. 이렇게 하면 공격자가 통신 채널을 스니핑하고 교환된 데이터를 일반 텍스트로 검색하지 못합니다. 추가 리소스는 암호화 문서를 참고하세요.
위험: 무선 악성 데이터 삽입
무선 머신 간 통신 채널 (기본 블루투스, BLE, NFC, Wi-Fi P2P)은 악성 데이터를 사용하여 변조할 수 있습니다. 숙련된 공격자는 사용 중인 통신 프로토콜을 식별하고 예를 들어 엔드포인트 중 하나를 가장하여 특별히 제작된 페이로드를 전송하는 방식으로 데이터 교환 흐름을 변조할 수 있습니다. 이러한 악성 트래픽은 애플리케이션의 기능을 저하시키고 최악의 경우 예기치 않은 애플리케이션 및 기기 동작을 일으키거나 서비스 거부(DoS), 명령어 삽입 또는 기기 장악과 같은 공격을 초래할 수 있습니다.
완화 조치
Android는 개발자에게 강력한 API를 제공하여 기본 블루투스, BLE, NFC, Wi-Fi P2P와 같은 머신 간 통신을 관리합니다. 이러한 API는 신중하게 구현된 데이터 유효성 검사 로직과 결합하여 두 기기 간에 교환되는 데이터를 삭제해야 합니다.
이 솔루션은 애플리케이션 수준에서 구현해야 하며 데이터의 길이가 예상대로인지, 형식이 올바른지, 애플리케이션에서 해석할 수 있는 유효한 페이로드가 포함되어 있는지 확인하는 검사를 포함해야 합니다.
다음 스니펫은 데이터 유효성 검사 로직의 예를 보여줍니다. 이는 블루투스 데이터 전송 구현에 관한 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 기기로 표시됩니다.
애플리케이션이 PID/VID를 사용하여 USB 기기를 필터링하여 특정 인앱 기능을 트리거하는 경우 공격자는 합법적인 기기를 가장하여 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 - 클론된 인텐트 필터
악성 애플리케이션은 특정 NFC 태그 또는 NFC 지원 기기를 읽기 위해 인텐트 필터를 등록할 수 있습니다. 이러한 필터는 합법적인 애플리케이션에서 정의한 필터를 복제하여 공격자가 교환된 NFC 데이터의 콘텐츠를 읽을 수 있도록 합니다. 두 활동이 특정 NFC 태그에 동일한 인텐트 필터를 지정하면 활동 선택기가 표시되므로 공격이 성공하려면 사용자가 악성 애플리케이션을 선택해야 합니다. 그럼에도 불구하고 인텐트 필터를 클로킹과 결합하면 이 시나리오가 여전히 가능합니다. 이 공격은 NFC를 통해 교환된 데이터를 매우 민감한 것으로 간주할 수 있는 경우에만 중요합니다.
완화 조치
애플리케이션 내에서 NFC 읽기 기능을 구현할 때는 인텐트 필터 를 Android 애플리케이션 레코드 (AAR)와 함께 사용할 수 있습니다. NDEF 메시지 내에 AAR 레코드를 삽입하면 합법적인 애플리케이션과 연결된 NDEF 처리 활동만 시작된다는 강력한 보장을 제공합니다. 이렇게 하면 원치 않는 애플리케이션 또는 활동이 NFC를 통해 교환된 매우 민감한 태그 또는 기기 데이터를 읽지 못합니다.
위험: NFC - NDEF 메시지 유효성 검사 부족
Android 구동 기기가 NFC 태그 또는 NFC 지원 기기에서 데이터를 수신하면 시스템은 포함된 NDEF 메시지를 처리하도록 구성된 애플리케이션 또는 특정 활동을 자동으로 트리거합니다. 애플리케이션에 구현된 로직에 따라 태그에 포함되거나 기기에서 수신된 데이터는 웹페이지 열기와 같은 추가 작업을 트리거하기 위해 다른 활동에 제공될 수 있습니다.
NDEF 메시지 콘텐츠 유효성 검사가 없는 애플리케이션을 사용하면 공격자가 NFC 지원 기기 또는 NFC 태그를 사용하여 애플리케이션 내에 악성 페이로드를 삽입하여 악성 파일 다운로드, 명령어 삽입 또는 서비스 거부(DoS)를 초래할 수 있는 예기치 않은 동작을 일으킬 수 있습니다.
완화 조치
수신된 NDEF 메시지를 다른 애플리케이션 구성요소에 전달하기 전에 메시지 내의 데이터가 예상 형식인지, 예상 정보가 포함되어 있는지 확인해야 합니다. 이렇게 하면 악성 데이터가 필터링되지 않은 상태로 다른 애플리케이션 구성요소에 전달되지 않으므로 변조된 NFC 데이터를 사용하는 예기치 않은 동작 또는 공격의 위험이 줄어듭니다.
다음 스니펫은 NDEF 메시지를 인수로 사용하는 메서드와 메시지 배열의 색인으로 구현된 데이터 유효성 검사 로직의 예를 보여줍니다. 이는 검사된 NFC NDEF 태그에서 데이터를 가져오는 Android 개발자 예시를 기반으로 구현되었습니다.
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;
}