Categoría de OWASP: MASVS-CODE: Calidad de código
Descripción general
No es raro ver aplicaciones que implementan funciones que permiten a los usuarios transferir datos o interactuar con otros dispositivos mediante comunicaciones de radiofrecuencia (RF) o conexiones por cable. Las tecnologías más comunes que se usan en Android para este propósito son Bluetooth clásico (Bluetooth BR/EDR), Bluetooth de bajo consumo (BLE), Wi-Fi P2P, NFC y USB.
Por lo general, estas tecnologías se implementan en aplicaciones que se espera que se comuniquen con accesorios para casas inteligentes, dispositivos de supervisión de estado, quioscos de transporte público, terminales de pago y otros dispositivos con Android.
Al igual que con cualquier otro canal, las comunicaciones de máquina a máquina son susceptibles a ataques que buscan comprometer el límite de confianza establecido entre dos o más dispositivos. Los usuarios maliciosos pueden aprovechar técnicas como la suplantación de identidad del dispositivo para lograr una gran cantidad de ataques contra el canal de comunicación.
Estas APIs se deben usar con cuidado, ya que los errores durante la implementación de protocolos de comunicación pueden provocar que los datos del usuario o del dispositivo se expongan a terceros no autorizados. En el peor de los casos, los atacantes podrían tomar el control de uno o más dispositivos de forma remota y, en consecuencia, obtener acceso completo al contenido del dispositivo.
Impacto
El impacto puede variar según la tecnología de dispositivo a dispositivo implementada en la aplicación.
El uso o la configuración incorrectos de los canales de comunicación de máquina a máquina pueden dejar el dispositivo del usuario expuesto a intentos de comunicación no confiables. Esto puede hacer que el dispositivo sea vulnerable a ataques adicionales, como ataques de intermediarios (MiTM), inyección de comandos, DoS o suplantación de identidad.
Riesgo: Escucha de datos sensibles a través de canales inalámbricos
Cuando se implementan mecanismos de comunicación de máquina a máquina, se debe tener en cuenta la tecnología utilizada y el tipo de datos que se deben transmitir. Si bien las conexiones por cable son más seguras en la práctica para este tipo de tareas, ya que requieren un vínculo físico entre los dispositivos involucrados, se pueden interceptar los protocolos de comunicación que usan frecuencias de radio, como Bluetooth clásico, BLE, NFC y Wi-Fi P2P. Un atacante podría suplantar la identidad de una de las terminales o los puntos de acceso involucrados en el intercambio de datos, interceptar la comunicación de forma inalámbrica y, en consecuencia, obtener acceso a datos sensibles del usuario. Además, las aplicaciones maliciosas instaladas en el dispositivo, si se les otorgan los permisos de tiempo de ejecución específicos de la comunicación, podrían recuperar los datos intercambiados entre los dispositivos leyendo los búferes de mensajes del sistema.
Mitigaciones
Si la aplicación requiere el intercambio de datos sensibles de máquina a máquina a través de canales inalámbricos, se deben implementar soluciones de seguridad de la capa de aplicación, como el encriptado, en el código de la aplicación. Esto evitará que los atacantes rastreen el canal de comunicación y recuperen los datos intercambiados en texto simple. Para obtener recursos adicionales, consulta la documentación de Criptografía.
Riesgo: Inyección de datos maliciosos inalámbricos
Los canales de comunicación de máquina a máquina inalámbricos (Bluetooth clásico, BLE, NFC, Wi-Fi P2P) se pueden manipular con datos maliciosos. Los atacantes con las habilidades suficientes pueden identificar el protocolo de comunicación en uso y manipular el flujo de intercambio de datos, por ejemplo, suplantando la identidad de uno de los extremos y enviando cargas útiles diseñadas específicamente. Este tipo de tráfico malicioso puede degradar la funcionalidad de la aplicación y, en el peor de los casos, provocar un comportamiento inesperado de la aplicación y el dispositivo, o generar ataques como DoS, inyección de comandos o toma de control del dispositivo.
Mitigaciones
Android proporciona a los desarrolladores APIs potentes para administrar comunicaciones de máquina a máquina, como Bluetooth clásico, BLE, NFC y Wi-Fi P2P. Estos se deben combinar con una lógica de validación de datos implementada con cuidado para desinfectar los datos intercambiados entre dos dispositivos.
Esta solución se debe implementar a nivel de la aplicación y debe incluir verificaciones que comprueben si los datos tienen la longitud y el formato esperados, y si contienen una carga útil válida que la aplicación pueda interpretar.
En el siguiente fragmento, se muestra un ejemplo de lógica de validación de datos. Se implementó sobre el ejemplo de desarrolladores de Android para implementar la transferencia de datos por 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;
}
}
Riesgo: Inyección de datos maliciosos por USB
Un usuario malicioso interesado en interceptar comunicaciones puede atacar las conexiones USB entre dos dispositivos. En este caso, el vínculo físico requerido constituye una capa de seguridad adicional, ya que el atacante debe obtener acceso al cable que conecta las terminales para poder escuchar cualquier mensaje. Otro vector de ataque está representado por dispositivos USB no confiables que, ya sea de forma intencional o no, se conectan al dispositivo.
Si la aplicación filtra los dispositivos USB con PID/VID para activar funciones específicas en la app, los atacantes podrían manipular los datos enviados a través del canal USB suplantando la identidad del dispositivo legítimo. Los ataques de este tipo pueden permitir que los usuarios maliciosos envíen pulsaciones de teclas al dispositivo o ejecuten actividades de la aplicación que, en el peor de los casos, pueden provocar la ejecución remota de código o la descarga de software no deseado.
Mitigaciones
Se debe implementar una lógica de validación a nivel de la aplicación. Esta lógica debe filtrar los datos enviados a través de USB y verificar que la longitud, el formato y el contenido coincidan con el caso de uso de la aplicación. Por ejemplo, un monitor de frecuencia cardíaca no debería poder enviar comandos de pulsación de teclas.
Además, cuando sea posible, se debe considerar restringir la cantidad de paquetes USB que la aplicación puede recibir del dispositivo USB. Esto evita que los dispositivos maliciosos realicen ataques como el de rubber ducky.
Esta validación se puede lograr creando un nuevo subproceso para verificar el contenido del
búfer, por ejemplo, en un 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();
}
}
Riesgos específicos
En esta sección, se recopilan los riesgos que requieren estrategias de mitigación no estándar o que se mitigaron en cierto nivel de SDK y están aquí para lograr una integridad.
Riesgo: Bluetooth: Tiempo de visibilidad incorrecto
Como se destaca en la documentación de Bluetooth para desarrolladores de Android, cuando se
configura la interfaz de Bluetooth dentro de la aplicación, usar el
startActivityForResult(Intent, int) método para habilitar la
visibilidad del dispositivo y establecer EXTRA_DISCOVERABLE_DURATION en cero hará que el
dispositivo sea visible mientras la aplicación se ejecute en segundo plano o en primer plano. En cuanto a la especificación de Bluetooth clásico, los dispositivos visibles transmiten constantemente mensajes de detección específicos
que permiten que otros dispositivos recuperen datos del dispositivo o se conecten a él. En este caso, un tercero malicioso puede interceptar esos mensajes y conectarse al dispositivo con Android. Una vez conectado, un atacante puede realizar más ataques, como robo de datos, DoS o inyección de comandos.
Mitigaciones
EXTRA_DISCOVERABLE_DURATION nunca debe establecerse en cero. Si no se establece el parámetro EXTRA_DISCOVERABLE_DURATION, de forma predeterminada, Android hace que los dispositivos sean visibles durante 2 minutos. El valor máximo que se puede establecer para el parámetro EXTRA_DISCOVERABLE_DURATION es de 2 horas (7,200 segundos). Se recomienda mantener el tiempo de duración visible en el tiempo más corto posible, según el caso de uso de la aplicación.
Riesgo: NFC: Filtros de intents clonados
Una aplicación maliciosa puede registrar filtros de intents para leer etiquetas NFC específicas o dispositivos habilitados para NFC. Estos filtros pueden replicar los definidos por una aplicación legítima, lo que permite que un atacante lea el contenido de los datos NFC intercambiados. Se debe tener en cuenta que, cuando dos actividades especifican los mismos filtros de intents para una etiqueta NFC específica, se presenta el Selector de actividad, por lo que el usuario deberá elegir la aplicación maliciosa para que el ataque tenga éxito. Sin embargo, si se combinan los filtros de intents con el encubrimiento, este caso sigue siendo posible. Este ataque es significativo solo para los casos en los que los datos intercambiados a través de NFC se pueden considerar altamente sensibles.
Mitigaciones
Cuando se implementan capacidades de lectura de NFC dentro de una aplicación, se pueden usar filtros de intents junto con registros de aplicaciones para Android (AARs). Si se incorpora el registro AAR dentro de un mensaje NDEF, se garantiza que solo se iniciará la aplicación legítima y su actividad de control de NDEF asociada. Esto evitará que las aplicaciones o actividades no deseadas lean datos altamente sensibles de etiquetas o dispositivos intercambiados a través de NFC.
Riesgo: NFC: Falta de validación de mensajes NDEF
Cuando un dispositivo con Android recibe datos de una etiqueta NFC o un dispositivo habilitado para NFC, el sistema activa automáticamente la aplicación o la actividad específica que está configurada para controlar el mensaje NDEF que contiene. Según la lógica implementada en la aplicación, los datos contenidos en la etiqueta o recibidos del dispositivo se pueden entregar a otras actividades para activar más acciones, como abrir páginas web.
Una aplicación que no valida el contenido de los mensajes NDEF puede permitir que los atacantes usen dispositivos habilitados para NFC o etiquetas NFC para inyectar cargas útiles maliciosas dentro de la aplicación, lo que provoca un comportamiento inesperado que puede generar la descarga de archivos maliciosos, la inyección de comandos o DoS.
Mitigaciones
Antes de enviar el mensaje NDEF recibido a cualquier otro componente de la aplicación, se deben validar los datos para que tengan el formato esperado y contengan la información esperada. Esto evita que los datos maliciosos se pasen a otros componentes de la aplicación sin filtrar, lo que reduce el riesgo de comportamiento inesperado o ataques con datos NFC manipulados.
En el siguiente fragmento, se muestra un ejemplo de lógica de validación de datos implementada como un método con un mensaje NDEF como argumento y su índice en el array de mensajes. Se implementó sobre el ejemplo de desarrolladores de Android ejemplo para obtener datos de una etiqueta NDEF de NFC escaneada:
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;
}
Recursos
- Permisos de tiempo de ejecución
- Guías de conectividad
- Ejemplo
- bulkTransfer
- Criptografía
- Configurar Bluetooth
- Conceptos básicos de NFC
- Registros de aplicaciones para Android
- Especificación de Bluetooth clásico