OWASP 类别: MASVS-CODE:代码质量
概览
我们经常会看到一些应用实现了允许用户使用射频 (RF) 通信或有线连接传输数据或与其他设备互动的功能。Android 中用于此目的的最常见技术是传统蓝牙(蓝牙 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 渠道发送的数据。此类攻击可能会让恶意用户向设备发送按键,或执行应用 activity,在最糟糕的情况下,可能会导致远程代码执行或下载垃圾软件。
缓解措施
应实现应用级验证逻辑。此逻辑应过滤通过 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 设备。连接后,攻击者可以执行进一步的攻击,例如数据窃取、DoS 攻击或命令注入攻击。
缓解措施
EXTRA_DISCOVERABLE_DURATION 绝不应设置为零。如果未设置 EXTRA_DISCOVERABLE_DURATION 参数,默认情况下,Android 会使设备在 2 分钟内可被发现。可以为 EXTRA_DISCOVERABLE_DURATION 参数设置的最大值为 2 小时(7200 秒)。建议根据应用用例将可发现时长保持在尽可能短的时间。
风险:NFC - 克隆的 intent 过滤器
恶意应用可以注册 intent 过滤器来读取特定的 NFC 标签或支持 NFC 的设备。这些过滤器可以复制合法应用定义的过滤器,使攻击者能够读取交换的 NFC 数据的内容。需要注意的是,当两个 activity 为特定的 NFC 标签指定相同的 intent 过滤器时,系统会显示 activity 选择器,因此用户仍需要选择恶意应用才能成功发起攻击。不过,如果将 intent 过滤器与隐身相结合,这种情况仍然可能发生。只有当通过 NFC 交换的数据被认为是高度敏感数据时,此攻击才具有重大意义。
缓解措施
在应用内实现 NFC 读取功能时,intent 过滤器 可以与 Android 应用记录 (AAR) 结合使用。将 AAR 记录嵌入 NDEF 消息中可以有力地确保仅启动合法应用及其关联的 NDEF 处理 activity。 这样可以防止不需要的应用或 activity 读取通过 NFC 交换的高度敏感的标签或设备数据。
风险:NFC - 缺少 NDEF 消息验证
当 Android 设备从 NFC 标签或支持 NFC 的 设备接收数据时,系统会自动触发配置为处理其中包含的 NDEF 消息的应用或特定 activity。 根据应用中实现的逻辑,标签中包含的数据或从设备接收的数据可以提供给其他 activity,以触发进一步的操作,例如打开网页。
缺少 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;
}