MASTG-TEST-0001:测试本地存储中的敏感数据
已弃用测试
此测试已**弃用**,不应再使用。**原因**:MASTG V2 中提供新版本
请查看以下涵盖此 v1 测试的 MASTG v2 测试
概述¶
此测试用例侧重于识别应用程序存储的潜在敏感数据,并验证其是否安全存储。应执行以下检查
- 分析源代码中的数据存储。
- 务必触发应用程序中的所有可能功能(例如,通过点击所有可能的地方),以确保数据生成。
- 检查所有应用程序生成和修改的文件,并确保存储方法足够安全。
- 这包括
SharedPreferences
,数据库,内部存储,外部存储等。
- 这包括
注意:对于MASVS L1合规性,足以将数据未加密地存储在应用程序的内部存储目录(沙箱)中。对于L2合规性,需要使用在Android KeyStore中安全管理的加密密钥进行额外加密。这包括使用信封加密(DEK+KEK)或等效方法,或使用Android安全库的EncryptedFile
/EncryptedSharedPreferences
。
警告
包括EncryptedFile
和EncryptedSharedPreferences
类在内的Jetpack安全加密库已弃用。但是,由于尚未发布官方替代品,我们建议您在使用这些类直到有可用的替代品为止。
静态分析¶
首先,尝试确定Android应用程序使用的存储类型,并找出该应用程序是否不安全地处理敏感数据。
- 检查
AndroidManifest.xml
中的读/写外部存储权限,例如,uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
。 - 检查源代码中用于存储数据的关键字和API调用
- 文件权限,例如
MODE_WORLD_READABLE
或MODE_WORLD_WRITABLE
:应避免为文件使用MODE_WORLD_WRITEABLE
和MODE_WORLD_READABLE
,因为任何应用程序都可以读取或写入文件,即使这些文件存储在应用程序的私有数据目录中也是如此。如果必须与其他应用程序共享数据,请考虑内容提供程序。内容提供程序向其他应用程序提供读写权限,并且可以根据具体情况授予动态权限。
- 类和函数,例如
SharedPreferences
类(存储键值对)FileOutPutStream
类(使用内部或外部存储)getExternal*
函数(使用外部存储)getWritableDatabase
函数(返回用于写入的SQLiteDatabase)getReadableDatabase
函数(返回用于读取的SQLiteDatabase)getCacheDir
和getExternalCacheDirs
函数(使用缓存文件)
- 文件权限,例如
应使用经过验证的SDK函数来实现加密。以下描述了在源代码中要查找的不良做法
- 通过简单的位运算(如XOR或位翻转)“加密”的本地存储敏感信息。应避免这些操作,因为可以轻松恢复加密数据。
- 未使用或创建未经Android onboard功能(如Android KeyStore)的密钥
- 硬编码泄露的密钥
典型的滥用是硬编码的加密密钥。硬编码和世界可读的加密密钥大大增加了加密数据被恢复的可能性。一旦攻击者获取了数据,解密它就变得很简单。对称加密密钥必须存储在设备上,因此识别它们只是时间和精力的问题。考虑以下代码
this.db = localUserSecretStore.getWritableDatabase("SuperPassword123");
获取密钥很简单,因为它包含在源代码中,并且对于应用程序的所有安装都是相同的。以这种方式加密数据是没有益处的。查找硬编码的API密钥/私钥和其他有价值的数据;它们构成类似的风险。编码/加密的密钥代表了另一种使其更难但并非不可能获得皇冠上的宝石的尝试。
考虑以下代码
Java 示例
//A more complicated effort to store the XOR'ed halves of a key (instead of the key itself)
private static final String[] myCompositeKey = new String[]{
"oNQavjbaNNSgEqoCkT9Em4imeQQ=","3o8eFOX4ri/F8fgHgiy/BS47"
};
Kotlin 示例
private val myCompositeKey = arrayOf<String>("oNQavjbaNNSgEqoCkT9Em4imeQQ=", "3o8eFOX4ri/F8fgHgiy/BS47")
解码原始密钥的算法可能是这样的
Java 示例
public void useXorStringHiding(String myHiddenMessage) {
byte[] xorParts0 = Base64.decode(myCompositeKey[0],0);
byte[] xorParts1 = Base64.decode(myCompositeKey[1],0);
byte[] xorKey = new byte[xorParts0.length];
for(int i = 0; i < xorParts1.length; i++){
xorKey[i] = (byte) (xorParts0[i] ^ xorParts1[i]);
}
HidingUtil.doHiding(myHiddenMessage.getBytes(), xorKey, false);
}
Kotlin 示例
fun useXorStringHiding(myHiddenMessage:String) {
val xorParts0 = Base64.decode(myCompositeKey[0], 0)
val xorParts1 = Base64.decode(myCompositeKey[1], 0)
val xorKey = ByteArray(xorParts0.size)
for (i in xorParts1.indices)
{
xorKey[i] = (xorParts0[i] xor xorParts1[i]).toByte()
}
HidingUtil.doHiding(myHiddenMessage.toByteArray(), xorKey, false)
}
验证常见机密位置
- 资源(通常位于res/values/strings.xml中) 示例
<resources>
<string name="app_name">SuperApp</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string name="secret_key">My_Secret_Key</string>
</resources>
- 构建配置,例如在local.properties或gradle.properties中 示例
buildTypes {
debug {
minifyEnabled true
buildConfigField "String", "hiddenPassword", "\"${hiddenPassword}\""
}
}
动态分析¶
安装并使用该应用程序,至少执行一次所有功能。数据可以在用户输入、端点发送或随应用程序一起提供时生成。然后完成以下操作
- 检查内部和外部本地存储中是否存在任何包含敏感数据的应用程序创建的文件。
- 识别开发文件、备份文件和不应包含在生产版本中的旧文件。
- 确定SQLite数据库是否可用,以及它们是否包含敏感信息。SQLite数据库存储在
/data/data/<package-name>/databases
中。 - 确定SQLite数据库是否已加密。如果是,请确定如何生成和存储数据库密码,以及这是否像Keystore概述的“存储密钥”部分中所述的那样受到充分保护。
- 检查存储为XML文件的共享首选项(在
/data/data/<package-name>/shared_prefs
中)以获取敏感信息。默认情况下,共享首选项是不安全的且未加密。某些应用程序可能会选择使用secure-preferences来加密存储在共享首选项中的值。 - 检查
/data/data/<package-name>
中文件的权限。仅当您安装该应用程序时创建的用户和组(例如,u0_a82)才应具有用户读取、写入和执行权限(rwx
)。其他用户不应具有访问文件的权限,但他们可能具有目录的执行权限。 - 检查是否使用了任何Firebase实时数据库,并尝试通过发出以下网络调用来识别它们是否配置错误
https://_firebaseProjectName_.firebaseio.com/.json
- 确定
/data/data/<package-name>/files/
中是否存在Realm数据库,它是否未加密,以及它是否包含敏感信息。默认情况下,文件扩展名为realm
,文件名为default
。使用Realm Browser检查Realm数据库。