Biometric Authentication Bypass
Evasión de la autenticación biométrica
Descripción
Una implementación segura de la autenticación biométrica móvil garantiza la necesidad de usar la autenticación mediante Face ID o Touch ID para acceder a los datos sensibles de la aplicación. Dicha implementación segura va más allá de simplemente verificar la huella digital o el rostro para iniciar sesión. También incluye el cifrado de los datos sensibles de la aplicación utilizando los datos biométricos.
Este cifrado añade una capa extra de protección, lo que dificulta enormemente que personas no autorizadas accedan o utilicen información sensible. El cifrado con datos biométricos se vuelve crucial en caso de que una parte no autorizada obtenga acceso al dispositivo, ya sea a través de malware o acceso físico.
Android
Android proporciona mecanismos para hacer cumplir la autenticación biométrica con el fin de proteger la información sensible. La autenticación biométrica ha evolucionado con el tiempo para ofrecer una mejor experiencia de usuario, una mejor experiencia para el desarrollador y una seguridad mejorada.
La implementación anterior que utilizaba FingerprintManager está obsoleta y no debe emplearse. La implementación adecuada debe utilizar BiometricManager junto con BiometricPrompt y CryptoObject.
CryptoObject proporciona primitivas criptográficas para el cifrado, el descifrado y la validación de firmas.
En el siguiente ejemplo, llamar al método authenticate sin cryptoObject es vulnerable a la evasión de la autenticación:
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
El framework Local Authentication permite a los desarrolladores solicitar la autenticación de Touch ID a los usuarios. Para iniciar este proceso, los desarrolladores pueden invocar un aviso de autenticación utilizando la función evaluatePolicy dentro de la clase LAContext. Sin embargo, es importante tener en cuenta que este enfoque no es seguro: la función devuelve un valor booleano, en lugar de proporcionar un objeto criptográfico que pueda utilizarse para descifrar datos sensibles almacenados dentro del Keychain.
Sin un objeto criptográfico, un atacante puede manipular la memoria para evadir la verificación biométrica e iniciar sesión exitosamente en la aplicación. No obstante, serían incapaces de interpretar o utilizar los datos de la aplicación si el cifrado se realiza con los datos biométricos. Esto ayuda a mantener la confidencialidad de la información sensible de la aplicación, salvaguardando así la privacidad y la seguridad de los datos de los usuarios.
En el siguiente ejemplo extraído de DVIA, es posible evadir la autenticación biométrica interceptando (hooking) evaluatePolicy usando frida:
+(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"];
});
}
}
Recomendación
Kotlin
Para Android nativo, implemente la autenticación biométrica utilizando CryptoObject.
El flujo de autenticación sería el siguiente al utilizar CryptoObject:
- La aplicación crea una clave en el KeyStore con:
setUserAuthenticationRequiredestablecido entruesetInvalidatedByBiometricEnrollmentestablecido entruesetUserAuthenticationValidityDurationSecondsestablecido en-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)
}
}
}
-
La clave del KeyStore debe utilizarse para cifrar información que autentique al usuario, como la información de la sesión o el token de autenticación.
-
La biometría se presenta antes de que se acceda a la clave desde el KeyStore para descifrar los datos. La biometría se valida con el método
authenticatey elCryptoObject. Esta solución no se puede evadir, incluso en dispositivos rooteados, ya que la clave del KeyStore solo se puede utilizar después de una autenticación biométrica exitosa.
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()
)
)
}
- Si
CryptoObjectno se utiliza como parte del método authenticate, puede evadirse empleando instrumentación dinámica con un depurador o con herramientas como Frida.
Swift
Utilice el Keychain para almacenar la secretKey, y exija el uso de la autenticación biométrica para acceder al elemento desde el Keychain.
1- Crear un elemento del keychain protegido por biometría:
Utilice SecAccessControlCreateWithFlags para crear un SecAccessControl con los siguientes parámetros:
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly: la entrada del keychain solo se puede leer cuando el dispositivo iOS está desbloqueado. Además, no se copiará a otros dispositivos mediante iCloud ni se agregará a las copias de seguridad.
- .biometryCurrentSet: establece el requisito de autenticación mediante Touch ID o Face ID. Esto vincula estrictamente su entrada a los datos biométricos registrados actualmente.
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- Leer una entrada protegida por biometría:
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
Para Flutter (tanto en Android como en iOS), biometric_storage es un plugin que permite utilizar la autenticación biométrica para escribir y leer datos cifrados en el dispositivo.
La implementación subyacente aplica las mejores prácticas y utiliza un SecAccessControl con las SecAccessControlCreateFlags adecuadas para restringir el acceso con Touch ID o Face ID.
- El primer paso consiste en crear el objeto de acceso en el que escribiremos y leeremos los datos después de la autenticación biométrica:
```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.
FutureauthenticationRequired=trueandauthenticationValidityDurationSeconds = -1 to ensure the secure implementation of bioùetric authentication.
authenticationValidityDurationSeconds: -1,
authenticationRequired: true,
androidBiometricOnly: true,
));
return authStorage;
}
```
- Escribir datos en el almacenamiento seguro:
```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.
Future
- Para leer los datos:
```dart
Future
} ```
Enlaces
- Secure Mobile Biometric Authentication
- Bypass Biometric Authentication
- Using BiometricPrompt with CryptoObject: How and Why
- Android Biometric API: Getting Started
- MOBILE PENTESTING 101 – BYPASSING BIOMETRIC AUTHENTICATION
Estándares
- 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