跳转至

Biometric Authentication Bypass

生物特征认证绕过

描述

移动生物特征认证的安全实现保证了需要使用 Face ID 或 Touch ID 认证才能访问应用程序的敏感数据。这种安全的实现不仅仅是验证指纹或面部来进行登录,还包括使用生物特征数据加密应用程序的敏感数据。

这种加密增加了一层额外的保护,使得未经授权的个人极难访问或使用敏感信息。如果未经授权的一方通过恶意软件或物理访问获得了对设备的访问权限,使用生物特征数据进行加密就变得至关重要。

Android

Android 提供了强制执行生物特征认证以保护敏感信息的机制。随着时间的推移,生物特征认证不断发展,以提供更好的用户体验、开发者体验和更高的安全性。

以前使用 FingerprintManager 的实现已被弃用,并且不得再使用。正确的实现必须使用 BiometricManager 结合 BiometricPromptCryptoObject

CryptoObject 提供了用于加密、解密和签名验证的密码学原语。

在以下示例中,在没有 cryptoObject 的情况下调用 authenticate 方法容易受到认证绕过的攻击:

fun showBiometricPrompt(
    title: String = "Biometric Authentication",
    subtitle: String = "Enter biometric credentials to proceed.",
    description: String = "Input your Fingerprint or FaceID to ensure it's you!",
    activity: AppCompatActivity,
    listener: BiometricAuthListener,
    cryptoObject: BiometricPrompt.CryptoObject? = null,
    allowDeviceCredential: Boolean = false
) {
  // 1
  val promptInfo = setBiometricPromptInfo(
      title,
      subtitle,
      description,
      allowDeviceCredential
  )

  // 2
  val biometricPrompt = initBiometricPrompt(activity, listener)

  // 3
  biometricPrompt.apply {
    if (cryptoObject == null) authenticate(promptInfo)
    else authenticate(promptInfo, cryptoObject)
  }
}

iOS

Local Authentication 框架允许开发者向用户请求 Touch ID 认证。为了启动此过程,开发者可以使用 LAContext 类中的 evaluatePolicy 函数调用认证提示。然而,需要注意的是,这种方法是不安全的:该函数返回一个布尔值,而不是提供一个可用于解密存储在 Keychain 中的敏感数据的密码学对象。

如果没有密码学对象,攻击者可以篡改内存以绕过生物特征检查并成功登录应用程序。但是,如果敏感数据使用生物特征数据进行了加密,攻击者将无法解释或使用应用程序数据。这有助于维护应用程序敏感信息的机密性,从而保护用户数据的隐私和安全。

在下面来自 DVIA 的示例中,可以使用 frida 钩取(hooking)evaluatePolicy 来绕过生物特征认证:

+(void)authenticateWithTouchID {

LAContext *myContext = [[LAContext alloc] init];
NSError *authError = nil;
NSString *myLocalizedReasonString = @"Please authenticate yourself";

if ([myContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
    [myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
              localizedReason:myLocalizedReasonString
                        reply:^(BOOL success, NSError *error) {
                            if (success) {
                                dispatch_async(dispatch_get_main_queue(), ^{
                                [TouchIDAuthentication showAlert:@"Authentication Successful" withTitle:@"Success"];
                                });
                            } else {
                                dispatch_async(dispatch_get_main_queue(), ^{
                                   [TouchIDAuthentication showAlert:@"Authentication Failed !" withTitle:@"Error"];
                                });
                            }
                        }];
    } else {
        dispatch_async(dispatch_get_main_queue(), ^{
            [TouchIDAuthentication showAlert:@"Your device doesn't support Touch ID or you haven't configured Touch ID authentication on your device" withTitle:@"Error"];
        });
    }
}

建议

Kotlin

对于原生 Android,请使用 CryptoObject 来实现生物特征认证。

使用 CryptoObject 时,认证流程如下:

  • 应用程序在 KeyStore 中创建一个密钥,并进行如下设置:
  • setUserAuthenticationRequired 设置为 true
  • setInvalidatedByBiometricEnrollment 设置为 true
  • setUserAuthenticationValidityDurationSeconds 设置为 -1
val paramsBuilder = KeyGenParameterSpec.Builder(keyName, KeyProperties.PURPOSE_SIGN)
        paramsBuilder.apply {
            when {
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
                    setDigests(KeyProperties.DIGEST_SHA256)
                    setUserAuthenticationRequired(true)
                    setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) // ECDSA parameter (P-256) curve
                    setInvalidatedByBiometricEnrollment(true)
                    setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
                }
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q -> {
                    setDigests(KeyProperties.DIGEST_SHA256)
                    setUserAuthenticationRequired(true)
                    setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
                    setInvalidatedByBiometricEnrollment(true)
                    setUserAuthenticationValidityDurationSeconds(-1)
                }
                else -> {
                    setDigests(KeyProperties.DIGEST_SHA256)
                    setUserAuthenticationRequired(true)
                    setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
                    setUserAuthenticationValidityDurationSeconds(-1)
                }
            }
        }
  • KeyStore 中的密钥必须用于加密对用户进行身份验证的信息,如会话信息或认证令牌。

  • 在从 KeyStore 访问密钥以解密数据之前,必须先提供生物特征。通过 authenticate 方法和 CryptoObject 来验证生物特征。由于 KeyStore 中的密钥只有在成功的生物特征认证后才能使用,因此即使在已越狱(rooted)的设备上,该解决方案也无法被绕过。

fun showBiometricPrompt(
        title: String = "Biometric Authentication",
        subtitle: String = "Enter biometric credentials to proceed.",
        description: String = "Input your Fingerprint or FaceID
to ensure it's you!",
        activity: AppCompatActivity,
        listener: BiometricAuthListener
    ) {
        val promptInfo = setBiometricPromptInfo(title, subtitle, description)

        val biometricPrompt = initBiometricPrompt(activity, listener)

        biometricPrompt.authenticate(
            promptInfo, BiometricPrompt.CryptoObject(
                CryptoUtil.getOrCreateSignature()
            )
        )
    }
  • 如果 CryptoObject 未作为 authenticate 方法的一部分被使用,则可以通过使用调试器或 Frida 等工具进行动态插桩(dynamic instrumentation)来绕过认证。

Swift

使用 Keychain 来存储 secretKey,并强制要求使用生物特征认证才能从 Keychain 访问该项。

1- 创建受生物特征保护的 keychain 项:

使用 SecAccessControlCreateWithFlags 以如下参数创建一个 SecAccessControl:

  • kSecAttrAccessibleWhenUnlockedThisDeviceOnly: keychain 条目仅在 iOS 设备解锁时才可被读取。此外,它不会通过 iCloud 复制到其他设备,也不会被添加到备份中。
  • .biometryCurrentSet: 设定需要 Touch ID 或 Face ID 认证。这会将您的条目与当前注册的生物特征数据严格绑定。
static func getBioSecAccessControl() -> SecAccessControl {
       var access: SecAccessControl?
       var error: Unmanaged<CFError>?
           access = SecAccessControlCreateWithFlags(nil,
               kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
               .biometryCurrentSet,
               &error)
       precondition(access != nil, "SecAccessControlCreateWithFlags failed")
       return access!
   }


static func createBioProtectedEntry(key: String, data: Data) -> OSStatus {
       let query = [
           kSecClass as String: kSecClassGenericPassword as String,
           kSecAttrAccount as String: key,
           kSecAttrAccessControl as String: getBioSecAccessControl(),
           kSecValueData as String: data ] as CFDictionary
       return SecItemAdd(query as CFDictionary, nil)
   }

2- 读取受生物特征保护的条目:

static func loadBioProtected(key: String, context: LAContext? = nil,
                                prompt: String? = nil) -> Data? {

    var query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: key,
            kSecReturnData as String: kCFBooleanTrue,
            kSecAttrAccessControl as String: getBioSecAccessControl(),
            kSecMatchLimit as String: kSecMatchLimitOne ]

    if let context = context {
        query[kSecUseAuthenticationContext as String] = context
        query[kSecUseAuthenticationUI as String] = kSecUseAuthenticationUISkip
    }

    if let prompt = prompt {
        query[kSecUseOperationPrompt as String] = prompt
    }

    var dataTypeRef: AnyObject? = nil
    let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)

    if status == noErr {
        return (dataTypeRef! as! Data)
    } else {
        return nil
    }
}

static func redBioProtectedEntry(entryName: String) {
    let authContext = LAContext()
    let accessControl = SecAccessControlCreateWithFlags(nil,
                kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                .biometryCurrentSet,
                &error)
    authContext.evaluateAccessControl(accessControl, operation: .useItem, localizedReason: "Access sample keychain entry") {
        (success, error) in
        var result = ""
        if success, let data = loadBioProtected(key: entryName, context: authContext) {
            let result = String(decoding: data, as: UTF8.self)
        } else {
            result = "Can't read entry, error: \(error?.localizedDescription ?? "-")"
        }
    }
}

Flutter

对于 Flutter (包括 Android 和 iOS),biometric_storage 是一个插件,它允许使用生物特征认证在设备上写入和读取加密数据。

其底层实现应用了最佳实践,并使用带有正确 SecAccessControlCreateFlags 的 SecAccessControl 来通过 Touch ID 或 Face ID 限制访问。

  • 第一步是创建访问对象,在生物特征认证之后我们将在其中写入和读取数据:

```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.

Future _getStorageFile() async { final authStorage = await BiometricStorage().getStorage('authenticated_storage',options:StorageFileInitOptions( ///Always call it with authenticationRequired=trueandauthenticationValidityDurationSeconds = -1 to ensure the secure implementation of bioùetric authentication. authenticationValidityDurationSeconds: -1, authenticationRequired: true, androidBiometricOnly: true, )); return authStorage; } ```

  • 将数据写入安全存储:

```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.

Future createBioProtectedEntry(context) async { if (await _checkAuthenticate() == false) { showAlertDialog(context,const Text("Can't use biometric auth on this device.")); return ; } _storageFile = await _getStorageFile(); await _storageFile?.write(_my_secret_data); } ```

  • 读取数据:

```dart Future redBioProtectedEntry(context) async { if (await _checkAuthenticate() == false) { showAlertDialog(context,const Text("Can't use biometric auth on this device.")); return ; } if (_storageFile == null){ showAlertDialog(context,const Text("Enable authentication first.")); return ; } final data = await _storageFile?.read(); showAlertDialog(context,Text(data!));

} ```

链接

标准

  • OWASP_MASVS_L2:
    • MSTG_AUTH_8
  • GDPR:
    • ART_5
    • ART_32
  • PCI_STANDARDS:
    • REQ_6_2
    • REQ_6_3
    • REQ_8_3
  • OWASP_MASVS_v2_1:
    • MASVS_AUTH_2
  • HIPAA_CONTROLS:
    • SECURITY221
    • SECURITY212
    • SECURITY213
    • SECURITY251
  • SOC2_CONTROLS:
    • CC_2_1
    • CC_4_1
    • CC_6_7
    • CC_7_1
    • CC_7_2
    • CC_7_4
    • CC_7_5
  • CNIL_FOR_DEVELOPERS:
    • DEVELOPERS_4_1_2