Biometric Authentication Bypass
Contournement de l'authentification biométrique
Description
Une implémentation sécurisée de l'authentification biométrique mobile garantit la nécessité d'utiliser l'authentification Face ID ou Touch ID pour accéder aux données sensibles de l'application. Une telle implémentation sécurisée va au-delà de la simple vérification de l'empreinte digitale ou du visage pour se connecter. Elle inclut également le chiffrement des données sensibles de l'application à l'aide des données biométriques.
Ce chiffrement ajoute une couche de protection supplémentaire, ce qui rend l'accès ou l'utilisation d'informations sensibles très difficile pour les personnes non autorisées. Le chiffrement avec des données biométriques devient crucial au cas où une partie non autorisée accèderait à l'appareil, via un malware ou un accès physique.
Android
Android fournit des mécanismes pour imposer l'authentification biométrique afin de protéger les informations sensibles. L'authentification biométrique a évolué au fil du temps pour offrir une meilleure expérience utilisateur, une meilleure expérience développeur et une sécurité accrue.
L'implémentation précédente utilisant FingerprintManager est obsolète et ne doit pas être utilisée. Une implémentation appropriée doit utiliser BiometricManager avec BiometricPrompt et CryptoObject.
CryptoObject fournit des primitives cryptographiques pour le chiffrement, le déchiffrement et la validation de signature.
Dans l'exemple ci-dessous, appeler la méthode authenticate sans cryptoObject est vulnérable à un contournement de l'authentification :
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
Le framework Local Authentication permet aux développeurs de demander l'authentification Touch ID aux utilisateurs. Pour initier ce processus, les développeurs peuvent invoquer une invite d'authentification à l'aide de la fonction evaluatePolicy au sein de la classe LAContext. Cependant, il est important de noter que cette approche n'est pas sécurisée : la fonction renvoie une valeur booléenne, plutôt que de fournir un objet cryptographique pouvant être utilisé pour déchiffrer des données sensibles stockées dans le Keychain.
Sans objet cryptographique, un attaquant peut manipuler la mémoire pour contourner la vérification biométrique et se connecter avec succès à l'application. Cependant, il serait incapable d'interpréter ou d'utiliser les données de l'application si le chiffrement est utilisé avec les données biométriques. Cela permet de maintenir la confidentialité des informations sensibles de l'application, protégeant ainsi la vie privée et la sécurité des données des utilisateurs.
Dans l'exemple ci-dessous issu de DVIA, il est possible de contourner l'authentification biométrique en accrochant (hooking) evaluatePolicy à l'aide de 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"];
});
}
}
Recommandation
Kotlin
Pour Android natif, implémentez l'authentification biométrique avec l'utilisation de CryptoObject.
Le flux d'authentification serait le suivant lors de l'utilisation de CryptoObject :
- L'application crée une clé dans le KeyStore avec :
setUserAuthenticationRequireddéfini surtruesetInvalidatedByBiometricEnrollmentdéfini surtruesetUserAuthenticationValidityDurationSecondsdéfini sur-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 clé du keystore doit être utilisée pour chiffrer les informations qui authentifient l'utilisateur, comme les informations de session ou le jeton d'authentification.
-
La biométrie est présentée avant que la clé ne soit accédée à partir du KeyStore pour déchiffrer les données. La biométrie est validée avec la méthode
authenticateet leCryptoObject. Cette solution ne peut pas être contournée, même sur des appareils rootés, car la clé du keystore ne peut être utilisée qu'après une authentification biométrique réussie.
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
CryptoObjectn'est pas utilisé dans le cadre de la méthode authenticate, il peut être contourné à l'aide d'une instrumentation dynamique avec un débogueur ou avec des outils comme Frida.
Swift
Utilisez le Keychain pour stocker la secretKey et imposez l'utilisation de l'authentification biométrique pour accéder à l'élément à partir du Keychain.
1- Créer un élément de keychain protégé par la biométrie :
Utilisez SecAccessControlCreateWithFlags pour créer un SecAccessControl avec les paramètres suivants :
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly: l'entrée du keychain ne peut être lue que lorsque l'appareil iOS est déverrouillé. De plus, elle ne sera pas copiée sur d'autres appareils via iCloud et ne sera pas ajoutée aux sauvegardes.
- .biometryCurrentSet: définit l'exigence de l'authentification Touch ID ou Face ID. Cela lie strictement votre entrée aux données biométriques actuellement enregistrées.
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- Lire une entrée protégée par la biométrie :
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
Pour Flutter (Android et iOS), biometric_storage est un plugin qui permet d'utiliser l'authentification biométrique pour écrire et lire des données chiffrées sur l'appareil.
L'implémentation sous-jacente applique les meilleures pratiques et utilise un SecAccessControl avec les SecAccessControlCreateFlags appropriés pour restreindre l'accès avec Touch ID ou Face ID.
- La première étape consiste à créer l'objet d'accès où nous écrirons et lirons les données après l'authentification biométrique :
```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;
}
```
- Écrire des données dans le stockage sécurisé :
```dart /// Retrieves the given biometric storage file. Each store is completely separated and has its own encryption and biometric lock.
Future
- Pour lire les données :
```dart
Future
} ```
Liens
- 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
Normes
- 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