OWASP 类别:MASVS-CODE:代码质量
概览
我们经常会看到一些应用实现了允许用户使用射频 (RF) 通信或有线连接传输数据或与其他设备互动等功能。最常用的技术 适合此用途的 Android 设备是经典蓝牙(蓝牙 BR/EDR)、低功耗蓝牙 节能 (BLE)、Wi-Fi 点对点、NFC 和 USB。
这些技术通常在预期与智能家居配件、健康监测设备、公共交通信息亭、付款终端和其他 Android 设备通信的应用中实现。
与任何其他渠道一样,机器对机器通信也容易受到攻击,攻击目的是破坏两台或更多设备之间建立的信任边界。设备模拟等技术可用于 对通信进行大量攻击 。
Android 提供了用于配置机器到机器的特定 API 。
应谨慎使用这些 API,因为实现通信协议时出现错误可能会导致用户数据或设备数据泄露给未经授权的第三方。在最糟糕的情况下,攻击者或许能够 接管一台或多台设备,从而获得内容的完整访问权限 。
影响
具体影响可能因应用中实现的点对点技术而异。
机器对机器的使用或配置错误 信道可能会使用户设备暴露给不受信任的 通信尝试。这可能会导致设备容易受到攻击 如中间人 (MiTM)、命令注入、DoS 或 进行模拟攻击
风险:通过无线通道窃听敏感数据
实现机器对机器通信机制时,应仔细考虑所用技术和应传输的数据类型。虽然在实践中,对于此类任务,有线连接更安全,因为它们需要相关设备之间存在物理链接,但使用无线频率(例如传统蓝牙、BLE、NFC 和 Wi-Fi P2P)的通信协议可能会被拦截。攻击者也许能够冒充某个 数据交换所涉及终端或接入点的信息,拦截 从而接触到敏感的用户 数据。此外,如果设备上安装的恶意应用被授予特定于通信的运行时权限,则可能能够通过读取系统消息缓冲区来检索设备之间交换的数据。
缓解措施
如果应用确实需要通过无线通道进行机器到机器的敏感数据交换,则应在应用代码中实现应用层安全解决方案(例如加密)。这将阻止攻击者对通信通道进行嗅探,并以明文形式检索交换的数据。如需其他资源,请参阅 加密文档。
风险:无线恶意数据注入
无线机器对机器通信通道(传统蓝牙、BLE、NFC、 Wi-Fi 点对点连接)。足够熟练 攻击者可以识别正在使用的通信协议并篡改 数据交换流,例如通过模拟其中一个端点、发送 专门构建的载荷此类恶意流量可能会降低应用的功能,在最糟糕的情况下,还会导致应用和设备出现意外行为,或者导致 DoS 攻击、命令注入或设备控制等攻击。
缓解措施
Android 为开发者提供了强大的 API 机器对机器通信,如传统蓝牙、BLE、NFC 和 Wifi P2P。这些逻辑应与精心实现的数据验证逻辑结合使用 来清理两台设备之间交换的所有数据
此解决方案应在应用级别实现,并且应包含 检查数据是否具有预期的长度、格式,以及是否包含 可由应用解读的有效载荷。
以下代码段展示了示例数据验证逻辑。此功能是基于 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 设备。连接后,攻击者可以进一步
例如数据盗窃、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) 搭配使用。将 NDEF 消息中的 AAR 记录可以有力保证,只有 合法应用及其相关的 NDEF 处理活动启动。 这可防止不需要的应用或活动读取通过 NFC 交换的高度敏感的标签或设备数据。
风险:NFC - 缺少 NDEF 消息验证
当 Android 设备接收来自 NFC 标签或已启用 NFC 的数据时 系统会自动触发应用或特定事件 配置为处理其中包含的 NDEF 消息的 activity。 根据应用中实现的逻辑,代码中包含的数据或从设备接收的数据可以提供给其他 activity,以触发进一步的操作,例如打开网页。
如果应用缺少 NDEF 消息内容验证,攻击者可能会使用支持 NFC 的设备或 NFC 标签在应用中注入恶意载荷,从而导致意外行为,进而导致恶意文件下载、命令注入或 DoS 攻击。
缓解措施
在将收到的 NDEF 消息发送给任何其他应用组件之前, 内的数据,确保其格式符合预期,且包含 预期的信息。这样可以避免将恶意数据未经过滤地传递给其他应用的组件,从而降低使用篡改的 NFC 数据导致意外行为或攻击的风险。
以下代码段展示了作为 方法,并使用 NDEF 消息作为参数及其在 messages 数组中的索引。 这是在 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;
}