Categoria do OWASP: MASVS-CODE - Qualidade do código (link em inglês)
Visão geral
Não é raro ver aplicativos que implementam funcionalidades permitindo que os usuários transfiram dados ou interajam com outros dispositivos usando comunicações por radiofrequência (RF) ou conexões com fio. As tecnologias mais comuns usadas no Android para essa finalidade são Bluetooth clássico (Bluetooth BR/EDR), Bluetooth de baixa energia (BLE), Wi-Fi P2P, NFC e USB.
Essas tecnologias geralmente são implementadas em aplicativos que precisam se comunicar com acessórios de casa inteligente, dispositivos de monitoramento de saúde, quiosques de transporte público, terminais de pagamento e outros dispositivos Android.
Assim como qualquer outro canal, as comunicações de máquina para máquina estão sujeitas a ataques que visam comprometer o limite de confiança estabelecido entre dois ou mais dispositivos. Técnicas como a personificação de dispositivos podem ser usadas por usuários maliciosos para realizar vários ataques contra o canal de comunicação.
O Android disponibiliza APIs específicas para desenvolvedores configurarem comunicações de máquina para máquina.
Essas APIs precisam ser usadas com cuidado, já que erros na implementação de protocolos de comunicação podem expor dados do usuário ou dados do dispositivo a terceiros não autorizados. No pior cenário, os invasores podem assumir o controle remoto de um ou mais dispositivos, ganhando acesso total ao conteúdo deles.
Impacto
O impacto pode variar de acordo com a tecnologia de dispositivo para dispositivo implementada no aplicativo.
O uso ou a configuração incorreta de canais de comunicação de máquina para máquina pode deixar o dispositivo do usuário exposto a tentativas de comunicação não confiáveis. Isso pode tornar o dispositivo vulnerável a outros ataques, como man-in-the-middle (MiTM), injeção de comando, DoS ou ataques de representação.
Risco: escuta de dados sensíveis em canais sem fio
Ao implementar mecanismos de comunicação entre máquinas, é preciso considerar cuidadosamente a tecnologia usada e o tipo de dados que devem ser transmitidos. Embora as conexões com fio sejam mais seguras na prática para essas tarefas, já que exigem um link físico entre os dispositivos envolvidos, os protocolos de comunicação que usam radiofrequências, como Bluetooth clássico, BLE, NFC e Wi-Fi P2P, podem ser interceptados. Um invasor pode se passar por um dos terminais ou pontos de acesso envolvidos na troca de dados, interceptando a comunicação pelo ar e, consequentemente, ganhando acesso a dados sensíveis do usuário. Além disso, aplicativos maliciosos instalados no dispositivo, se receberem permissões de execução específicas para comunicação, poderão recuperar dados trocados entre os dispositivos lendo buffers de mensagens do sistema.
Mitigações
Se o aplicativo exigir troca de dados sensíveis entre máquinas por canais sem fio, implemente soluções de segurança na camada de aplicativos, como criptografia, no código do aplicativo. Isso impede que invasores rastreiem o canal de comunicação e recuperem os dados trocados em texto simples. Para mais recursos, consulte a documentação de criptografia.
Risco: injeção de dados maliciosos sem fio
Os canais de comunicação sem fio entre máquinas (Bluetooth clássico, BLE, NFC, Wi-Fi P2P) podem ser adulterados com dados maliciosos. Invasores com habilidades suficientes podem identificar o protocolo de comunicação em uso e adulterar o fluxo de troca de dados, por exemplo, se passando por um dos endpoints e enviando payloads criados especificamente. Esse tipo de tráfego malicioso pode prejudicar a funcionalidade do aplicativo e, no pior cenário, causar comportamentos inesperados no aplicativo e no dispositivo ou resultar em ataques como DoS, injeção de comandos ou tomada de controle do dispositivo.
Mitigações
O Android oferece aos desenvolvedores APIs eficientes para gerenciar comunicações de máquina para máquina, como Bluetooth clássico, BLE, NFC e Wi-Fi P2P. Eles precisam ser combinados com uma lógica de validação de dados implementada com cuidado para limpar os dados trocados entre dois dispositivos.
Essa solução precisa ser implementada no nível do aplicativo e incluir verificações para confirmar se os dados têm o comprimento e o formato esperados e se contêm uma carga útil válida que pode ser interpretada pelo aplicativo.
O snippet a seguir mostra um exemplo de lógica de validação de dados. Isso foi implementado no exemplo para desenvolvedores do Android de implementação da transferência de dados 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;
}
}
Risco: injeção de dados maliciosos por USB
As conexões USB entre dois dispositivos podem ser alvo de um usuário malicioso interessado em interceptar comunicações. Nesse caso, o link físico necessário constitui uma camada de segurança adicional, já que o invasor precisa acessar o cabo que conecta os terminais para poder interceptar qualquer mensagem. Outro vetor de ataque é representado por dispositivos USB não confiáveis que são conectados ao dispositivo, intencionalmente ou não.
Se o aplicativo filtrar dispositivos USB usando PID/VID para acionar funcionalidades específicas no app, os invasores poderão adulterar os dados enviados pelo canal USB se passando pelo dispositivo legítimo. Ataques desse tipo podem permitir que usuários mal-intencionados enviem pressionamentos de tecla ao dispositivo ou executem atividades de aplicativos que, na pior das hipóteses, podem levar à execução remota de código ou ao download de software indesejado.
Mitigações
Uma lógica de validação no nível do aplicativo precisa ser implementada. Essa lógica precisa filtrar os dados enviados por USB, verificando se o comprimento, o formato e o conteúdo correspondem ao caso de uso do aplicativo. Por exemplo, um monitor cardíaco não pode enviar comandos de pressionamento de tecla.
Além disso, quando possível, considere restringir o número de pacotes USB que o aplicativo pode receber do dispositivo USB. Isso impede que dispositivos maliciosos realizem ataques como o rubber ducky.
Essa validação pode ser feita criando uma nova linha de execução para verificar o
conteúdo do buffer, por exemplo, em um 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();
}
}
Riscos específicos
Esta seção reúne riscos que exigem estratégias de mitigação não padrão ou que foram mitigados em determinados níveis do SDK e estão aqui para referência.
Risco: Bluetooth – tempo de capacidade de descoberta incorreto
Conforme destacado na documentação do Bluetooth para desenvolvedores Android, ao
configurar a interface Bluetooth no aplicativo, usar o método
startActivityForResult(Intent, int) para ativar a capacidade de
detecção do dispositivo e definir EXTRA_DISCOVERABLE_DURATION como zero
faz com que o dispositivo seja detectável enquanto o aplicativo estiver em execução
em segundo ou primeiro plano. Já na especificação Bluetooth clássica, os dispositivos detectáveis transmitem constantemente mensagens de descoberta específicas que permitem que outros dispositivos recuperem dados do dispositivo ou se conectem a eles. Nesse cenário, um terceiro malicioso pode interceptar essas mensagens e se conectar ao dispositivo Android. Depois de conectado, um invasor pode realizar outros
ataques, como roubo de dados, DoS ou injeção de comando.
Mitigações
O EXTRA_DISCOVERABLE_DURATION nunca pode ser definido como zero. Se o parâmetro
EXTRA_DISCOVERABLE_DURATION não estiver definido, por padrão, o Android vai tornar
os dispositivos detectáveis por dois minutos. O valor máximo que pode ser definido para o parâmetro EXTRA_DISCOVERABLE_DURATION é de 2 horas (7.200 segundos). Recomendamos manter a duração detectável o mais curta possível, de acordo com o caso de uso do aplicativo.
Risco: NFC – filtros de intent clonados
Um aplicativo malicioso pode registrar filtros de intent para ler tags NFC específicas ou dispositivos compatíveis com NFC. Esses filtros podem replicar os definidos por um aplicativo legítimo, permitindo que um invasor leia o conteúdo dos dados NFC trocados. Vale lembrar que, quando duas atividades especificam os mesmos filtros de intent para uma etiqueta NFC específica, o seletor de atividades é apresentado. Portanto, o usuário ainda precisa escolher o aplicativo malicioso para que o ataque seja bem-sucedido. No entanto, combinando filtros de intent com técnicas de cloaking, esse cenário ainda é possível. Esse ataque é significativo apenas para casos em que os dados trocados por NFC podem ser considerados altamente sensíveis.
Mitigações
Ao implementar recursos de leitura NFC em um aplicativo, os filtros de intent podem ser usados com registros de app Android (AARs). Incorporar o registro AAR em uma mensagem NDEF garante que apenas o aplicativo legítimo e a atividade de processamento NDEF associada sejam iniciados. Isso impede que aplicativos ou atividades indesejadas leiam dados altamente sensíveis de tags ou dados do dispositivo trocados por NFC.
Risco: NFC – falta de validação de mensagens NDEF
Quando um dispositivo Android recebe dados de uma tag NFC ou de um dispositivo compatível com NFC, o sistema aciona automaticamente o aplicativo ou a atividade específica configurada para processar a mensagem NDEF contida nele. De acordo com a lógica implementada no aplicativo, os dados contidos na tag ou recebidos do dispositivo podem ser veiculados a outras atividades para acionar mais ações, como abrir páginas da Web.
Um aplicativo sem validação de conteúdo de mensagens NDEF pode permitir que invasores usem dispositivos ou tags NFC para injetar payloads maliciosos no aplicativo, causando um comportamento inesperado que pode resultar em download de arquivos maliciosos, injeção de comandos ou DoS.
Mitigações
Antes de enviar a mensagem NDEF recebida para qualquer outro componente de aplicativo, os dados precisam ser validados para estar no formato esperado e conter as informações esperadas. Isso evita que dados maliciosos sejam transmitidos para componentes de outros aplicativos sem filtragem, reduzindo o risco de comportamento inesperado ou ataques usando dados NFC adulterados.
O snippet a seguir mostra um exemplo de lógica de validação de dados implementada como um método com uma mensagem NDEF como argumento e o índice dela na matriz de mensagens. Isso foi implementado no exemplo para desenvolvedores Android (link em inglês) para receber dados de uma tag NDEF NFC lida:
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
- Permissões de tempo de execução
- Guias de conectividade
- Exemplo
- bulkTransfer
- Criptografia
- Configurar o Bluetooth
- Fundamentos da NFC
- Registros de apps Android
- Especificação do Bluetooth clássico