OWASP 类别: MASVS-CODE:代码质量
概述
通常会看到一些应用实现了允许用户使用射频 (RF) 通信或有线连接传输数据或与其他设备交互的功能。Android 中最常用的此类技术是经典蓝牙 (Bluetooth BR/EDR)、蓝牙低功耗 (BLE)、Wifi P2P、NFC 和 USB。
这些技术通常在预期与智能家居配件、健康监测设备、公共交通售票亭、支付终端和其他 Android 设备通信的应用中实现。
与任何其他通道一样,机器到机器通信也容易受到旨在破坏两个或多个设备之间建立的信任边界的攻击。恶意用户可以利用设备模拟等技术对通信通道发起大量攻击。
Android 为开发人员提供了用于配置机器到机器通信的特定 API。
应谨慎使用这些 API,因为在实现通信协议时出现的错误可能导致用户或设备数据暴露给未经授权的第三方。在最坏的情况下,攻击者可能能够远程接管一台或多台设备,从而完全访问设备上的内容。
影响
影响可能因应用中实现的设备到设备技术而异。
机器到机器通信通道的错误使用或配置可能会导致用户设备暴露于不受信任的通信尝试。这可能导致设备容易受到其他攻击,例如中间人 (MiTM) 攻击、命令注入攻击、拒绝服务攻击或模拟攻击。
风险:通过无线通道窃听敏感数据
在实现机器对机器通信机制时,应仔细考虑所使用的技术和应传输的数据类型。虽然对于此类任务而言,有线连接在实践中更加安全,因为它们需要在涉及的设备之间建立物理连接,但使用无线电频率的通信协议(例如经典蓝牙、BLE、NFC 和 Wifi P2P)可能会被拦截。攻击者可能能够冒充数据交换中涉及的终端或接入点之一,拦截无线通信,从而获取敏感的用户数据。此外,如果授予通信特定的运行时权限,设备上安装的恶意应用程序可能能够通过读取系统消息缓冲区来检索设备之间交换的数据。
缓解措施
如果应用程序确实需要通过无线信道进行敏感数据的机器对机器交换,则应在应用程序代码中实现应用程序层安全解决方案,例如加密。这将阻止攻击者嗅探通信信道并以明文形式检索交换的数据。有关其他资源,请参阅加密文档。
风险:无线恶意数据注入
无线机器对机器通信信道(经典蓝牙、BLE、NFC、Wifi P2P)可以使用恶意数据进行篡改。技能娴熟的攻击者可以识别正在使用的通信协议并篡改数据交换流程,例如通过模拟其中一个端点,发送专门设计的有效负载。这种恶意流量可能会降低应用程序的功能,并且在最坏的情况下,会导致应用程序和设备出现意外行为,或导致拒绝服务攻击、命令注入或设备接管等攻击。
缓解措施
Android 为开发者提供了强大的 API 来管理机器对机器通信,例如经典蓝牙、BLE、NFC 和 Wifi 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
设置为零会导致设备在应用程序在后台或前台运行时一直处于可发现状态。至于经典蓝牙规范,可发现设备会不断广播特定的发现消息,允许其他设备检索设备数据或连接到它。在这种情况下,恶意第三方可以拦截此类消息并连接到 Android 设备。连接后,攻击者可以执行进一步的攻击,例如数据盗窃、拒绝服务或命令注入。
缓解措施
EXTRA_DISCOVERABLE_DURATION
绝不应设置为零。如果未设置EXTRA_DISCOVERABLE_DURATION
参数,则 Android 默认情况下会使设备可发现 2 分钟。可以为EXTRA_DISCOVERABLE_DURATION
参数设置的最大值为 2 小时(7200 秒)。建议根据应用程序用例将可发现持续时间保持在尽可能短的时间内。
风险:NFC – 克隆的意图过滤器
恶意应用程序可以注册意图过滤器以读取特定的 NFC 标签或 NFC 启用设备。这些过滤器可以复制合法应用程序定义的过滤器,使攻击者能够读取交换的 NFC 数据内容。需要注意的是,当两个活动为特定 NFC 标签指定相同的意图过滤器时,会显示活动选择器,因此用户仍然需要选择恶意应用程序才能使攻击成功。尽管如此,结合意图过滤器和隐藏,这种情况仍然可能发生。此攻击仅对通过 NFC 交换的数据可以被认为是高度敏感的情况具有重大意义。
缓解措施
在应用程序中实现 NFC 读取功能时,可以将意图过滤器与Android 应用程序记录 (AAR) 一起使用。将 AAR 记录嵌入 NDEF 消息中将有力地保证只有合法的应用程序及其关联的 NDEF 处理活动才会启动。这将防止不需要的应用程序或活动读取通过 NFC 交换的高度敏感的标签或设备数据。
风险:NFC – 缺乏 NDEF 消息验证
当 Android 设备从NFC 标签 或 NFC 启用设备接收数据时,系统会自动触发配置为处理其中包含的 NDEF 消息的应用程序或特定活动。根据应用程序中实现的逻辑,标签中包含的数据或从设备接收的数据可以提供给其他活动以触发进一步的操作,例如打开网页。
缺少 NDEF 消息内容验证的应用程序可能允许攻击者使用 NFC 启用设备或 NFC 标签在应用程序中注入恶意有效负载,导致意外行为,这可能导致恶意文件下载、命令注入或拒绝服务。
缓解措施
在将接收到的 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;
}