你的 Android Keystore 认证有多安全?(上)
安卓 Keystore 和应用程序HOOK
安卓 Keystore 一直被认为是安全的,因为我们不能访问密钥信息。 但是,攻击者实际上可能并不需要密钥内容。 Keystore API 可用于检索密钥引用,然后可以使用它们初始化 Cipher 对象,然后可以使用它们对应用程序存储进行解密或加密。
是的,这是可能的,而且大多数应用程序都很容易受到这类攻击,正如具有设备实际访问权限或特权恶意软件的攻击者可以做到的那样:
a) 启动受害者应用程序
b) 使用 Frida 在受害者应用程序的上下文中执行代码,HOOK 受害者应用程序如下:
1. 使用 Keystore API 检索对 AndroidKeystore 密钥的引用
2. 使用检索到的密钥引用初始化密码对象
3. 在应用程序存储中解密/加密/签名数据
使用 Android Keystore 并不能保证二进制安全性。 为了防止这种攻击,开发人员必须将keystore 密钥标记为只有在以下情况下才能访问:
1. 设备已经解锁
2. 指纹或其他生物识别已被验证
对于此配置,开发人员必须在生成密钥期间将 setUserAuthenticationRequired() 设置为 true。 另一个重要属性是 setUserAuthenticationValidityDurationSeconds ()。 如果设置为 -1,那么只有使用指纹或生物识别技术才能解锁密钥。 如果它被设置为任何其他值,也可以使用设备屏幕锁解锁密钥。
在设备屏幕锁定的情况下,首先通过调用 KeyguardManager.createConfirmDeviceCredentialIntent() 来访问密钥。
需要注意的是,KeyguardManager API 不允许开发人员检查配置了哪种类型的屏幕锁,也不允许验证密码/PIN码/手势图案策略。 因此,这个设备可能有一个不安全的屏幕锁:
1. 简单的手势图案(在大多数设备上是3x3,可以通过尝试常见的图案或检查屏幕上的指纹进行猜测)
2. 简单的PIN码(通常是4-5个数字,常见的PIN码有0000或1234)
3. 另外也可以使用生活中的一些东西作为猜测密码(例如:你的狗的名字)
因此,建议对于银行应用程序等高度敏感的应用程序,密码管理器或安全通讯等应用程序在设置 setUserAuthenticationValidityDurationSeconds() 的值时不应该有除 -1以外的任何值。
这个脚本可用于使用 KeyguardManager 触发“设备解锁”状态,并解锁未将有效期设置为 -1的密钥。
生物特征 / 指纹认证
生物身份认证,特别是指纹身份验证,是在 Android 6.0(API 23)中引入的。 要使用指纹认证,必须符合以下条件:
1.设置需要支持 API 23或之后的API
2.设备上有可用的指纹传感器
3.至少有一个注册的指纹
4.应用程序需要将指纹权限包含在Manifest.xml 文件
生物身份认证可以通过 FingerprintManager 或 BiometricPrompt 类及其嵌套类来实现,这些类管理身份验证机制和应用程序对话,要求用户进行身份验证。 正如其名称所暗示的那样,FingerprintManager 只支持指纹认证。 FingerprintManager 类是在 API 23中引入的,自从发布 BiometricPrompt 的 API 28之后就不再支持了。在Android6.0(API23)的时候,Android系统加入了指纹识别的API接口,即FingerprintManager,定义了最基础的指纹识别接口。在AndroidP(API28)的时候,官方不再推荐使用FingerprintManager,在代码中添加了@Deprecated。在AndroidP中,原来的FingerprintManager将被BiometricPrompt类替换,Google旨在统一生物识别的方式(虽然目前api中还没有看到虹膜、面部识别等),包括UI,UI也不允许自定义了,必须使用BiometricPrompt.Builder来创建对话框,其中可以自定义title、subtitle、description和一个NegativeButton(也就是cancel键)。
BiometricPrompt 的使用非常类似于 FingerprintManager。使用 FingerprintManager 需要的权限为 android.permission.USE_FINGERPRINT,而使用 BiometricPrompt 则需要 android.permission.USE_BIOMETRIC。生物身份认证最重要的部分是以下方法:
public void authenticate (BiometricPrompt.CryptoObject crypto, CancellationSignal cancel, Executor executor, BiometricPrompt.AuthenticationCallback callback)
该方法使生物识别的硬件开始工作,并开始扫描生物识别身份验证尝试。该方法有两个重要参数:
· crypto——它包含对应该解锁的 keystore 条目的引用。 为了以安全的方式实现生物认证,必须将密钥存储在这个加密对象中,用于某些应用程序关键的加密操作。
· callback——当用户将手指放在指纹传感器上或取消提示时,操作系统将调用这个回调结构
BiometricPrompt.AuthenticationCallback 参数用作回调结构,实现如下方法:
· onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result)
当系统成功地对用户进行身份验证时,onAuthenticationSucceded 方法会触发。 大多数遇到的生物身份认证实现都依赖于这个方法的调用,而不关心 CryptoObject。 负责解锁应用程序的应用程序逻辑通常包含在这个回调方法中。 该方法通过HOOK应用程序流程,直接调用 onAuthenticationSucceded 方法,从而在不提供有效生物特征的情况下解锁应用程序。
在接受评估的使用指纹认证的应用程序中,约有70% 可以解锁手机,甚至不需要有效的指纹。 此外,在50% 的情况下,解锁应用程序后,应用程序存储的数据被成功解密。
易受攻击的实现通常包含类似于下面所示的代码:
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { Toast.makeText(getActivity(), "Access granted",Toast.LENGTH_LONG).show(); accessGranted(); }
上面列出的代码不使用 AuthenticationResult 中传递的 CryptoObject,而只是假设身份验证成功,因为调用了 onAuthenticationSucceeded。
为了验证这个测试用例,我们创建了以下2个 Frida 脚本,它们可以用来测试不安全的生物身份认证 / 程序实现并绕过它们:
· 指纹绕过 ——该脚本将在不使用 crypto 对象时绕过身份验证。 身份验证实现依赖于回调 onAuthenticationSucceded 被调用。
· 通过异常处理绕过指纹——当使用CryptoObject但使用方式不正确时,此脚本将尝试绕过身份验证。这个问题的详细描述可以在“加密对象异常处理”一节中找到。
上面的脚本大多使用了钩子,用于重新实现 onAuthenticationSucceeded 回调的 authenticate 方法,而不是使用 onAuthenticationFailed 回调。
但是有可能以安全的方式实现本地身份验证吗?
是的。可以使用 AndroidKeystore。只需按照下面列出的步骤操作:
1. 将setUserAuthenticationRequired和setInvalidatedByBiometricEnrollment设置为true来创建Android密钥存储库。另外,setUserAuthenticationValidityDurationSeconds应该设置为-1。
2. 使用上面创建的密钥库密钥初始化cipher对象。
3. 使用前面步骤中的cipher对象创建BiometricPrompt.CryptoObject。
4. 实现BiometricPrompt.AuthenticationCallback.onAuthenticationSucceeded 回调,该回调将从参数中检索cipher对象,并使用该cipher对象来解密其他一些关键数据,如会话密钥,或将用于解密应用程序数据的辅助对称密钥。
5. 调用 BiometricPrompt。使用在步骤3和步骤4中创建的crypto对象和回调函数进行身份验证。
这有那么难吗? :)
加密对象异常处理
有些开发人员使用 CryptoObject,但他们不加密 / 解密对应用程序正确运行至关重要的数据。 因此,我们可以完全跳过身份验证步骤,继续使用应用程序。
针对这种情况,我们发现了一种不同的绕过方法。 脚本需要做的就是手动调用一个未经认证的(不是由指纹解锁的) CryptoObject 成功完成身份验证。 但是,如果应用程序将尝试使用一个锁定的 cipher 对象,将抛出 javax.crypto.IllegalBlockSizeException 异常。 但是,没有什么可以阻止我们在 Frida 脚本中处理异常。
此脚本将尝试调用 onAuthenticationSucceded 并在 Cipher 类中捕获 javax.crypto.IllegalBlockSizeExceptionexceptions 异常。 因此,如果应用程序没有使用这个密钥来解密关键数据,那么你可能会在没有身份验证的情况下进入应用程序;)
那么,又该如何解决这个问题呢? 没有唯一的答案,这取决于本地身份验证的目的。 对于数据存储,最好的解决方案是使用受指纹保护的密钥存储库密钥,该密钥将用于... ... 解密二级对称密钥(因此,每次需要进行加密操作时,不会提示用户)。 此对称密钥应用于解密应用程序存储。 然而,如果你只是需要调用 authenticate,例如为了授权一个交易,你可以使用一个非对称私钥来加签数据,这些数据随后会被发送到验证签名服务端的服务器。
身份验证超时
有时候第一个身份验证调用可能不是欺骗性的,因为代码实现需要对“二级”解密密钥进行解密(如前所述)。 但是,在此之后的所有调用(例如应用程序超时)有时都是可欺骗的。 这是因为许多移动应用程序将加密密钥保存在内存中,直到进程被终止。 因此,如果在解锁应用程序之后尝试绕过身份验证,那么应用程序很有可能会使用已经存储在内存中的密钥:)
示例参考应用程序
在掌握了 IT 安全知识和一些开发技能后,我们决定创建一个项目,以正确的方式实现生物身份验证/服务。 下面的项目旨在创建一个应用程序,可用作安全本地验证的参考。
https://github.com/mwrlabs/android-keystore-audit/tree/master/keystorecrypto-app
是的,它可以在我们的 github 公共帐户上使用,你也可以支持这个项目!
参考资料:
· Biometric-Auth-Sample (不使用 CipherObject 的脆弱库,可以绕过身份验证)
· android-FingerprintDialog (Google 的示例应用程序使用 CipherObject 对静态字符串进行加密,base64编码的结果显示在用户界面上,但应用程序无需正常运行。 对于生产环境的应用,结果应该在服务器端进行验证,否则结果的正确性对应用程序至关重要。 可以跳过该示例来显示“Purchase successful”消息。)
本文翻译自:https://labs.f-secure.com/blog/how-secure-is-your-android-keystore-authentication/如若转载,请注明原文地址