Last active
August 18, 2024 10:34
-
-
Save akabe1/c9d285ad3d07e7f47fc6d1599d01c8cf to your computer and use it in GitHub Desktop.
A script to detect Android Keystore calls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Android Keystore calls detection script | |
by Maurizio Siddu | |
Run with: | |
frida -U -f [APP_ID] -l keystore_spy.js --no-pause | |
*/ | |
setTimeout(function() { | |
Java.perform(function() { | |
console.log('') | |
console.log('======') | |
console.log('[#] Android Keystore Spy [#]') | |
console.log('======') | |
/* This tool allows to dynamically analyze the Android Keystore calls performed by the target app. | |
It helps to determine which Keystore is related to biometric authentication for Android apps | |
that use multiple keystores. | |
HINT: | |
the Keystore item related to biometric authentication is the one printed-out: | |
- after the call to the custom BiometricActivity (if you specify it), alternatively after the call to "Keystore.getKey()" | |
- and before the call to any of the biometric "authenticate()" methods. | |
*/ | |
// NOTE: | |
// Replace the placeholder value "com.example.app.BiometricActivity" with | |
// the name of the custom biometric activity class of the target app | |
var customBiometricActivity = 'com.example.app.BiometricActivity'; | |
// Starting the Biometric Keystore hunt | |
hookKeystoreGetKey(); | |
hookCipherInits(); | |
hookBiometricAuth(); | |
hookCustomBiometricactivity(customBiometricActivity); | |
// Ending the Biometric Keystore hunt | |
}); | |
},0); | |
function hookKeystoreGetKey() { | |
try { | |
// Hooking the Keystore.getKey(...) calls | |
var keystore = Java.use('java.security.KeyStore'); | |
var keystoreObj = keystore.getKey.overload('java.lang.String', '[C'); | |
keystoreObj.implementation = function(alias,password) { | |
var psw = 'null'; | |
if (password !== null) { | |
psw = password.join(''); | |
} | |
console.log('[+] Hooked KeyStore.getKey(alias,password) method'); | |
console.log('[+] Keystore item "'+alias+'" has password: '+psw); | |
//hookCryptoInit(); | |
var retval = this.getKey.overload('java.lang.String', '[C').call(this,alias,password); | |
return retval; | |
} | |
} catch (err) { | |
//console.log('[-] Method KeyStore.getKey(alias,password) not found'); | |
} | |
} | |
function hookCustomBiometricactivity(customBiometricActivity) { | |
try { | |
if (customBiometricActivity=='com.example.app.BiometricActivity') { | |
console.log('[W] Warning, you have not set the "customBiometricActivity" value, proceeding without it...') | |
} else { | |
// Hooking custom BiometricActivity | |
var biometricactivityClass = Java.use(customBiometricActivity); | |
biometricactivityClass.onCreate.overload('android.os.Bundle').implementation = function(param) { | |
console.log('\x1b[36m'+'[o] Biometric Authentication starting...'+'\x1b[0m'); | |
console.log('\x1b[36m'+'[o] Hooked custom BiometricActivity class: "'+customBiometricActivity+'"\x1b[0m'); | |
this.onCreate.overload('android.os.Bundle').call(this,param); | |
} | |
} | |
} catch (err) { | |
console.log('[-] Custom BiometricActivity class not found'); | |
} | |
} | |
function hookBiometricAuth() { | |
// Hooking the various Biometric authenticate(...) calls | |
try { | |
// BiometricPrompt authenticate(cancel,executor,callback) method | |
var biometricPrompt = Java.use('android.hardware.biometrics.BiometricPrompt'); | |
var biometricPrompt_auth = biometricPrompt.authenticate.overload('android.os.CancellationSignal', 'java.util.concurrent.Executor', 'android.hardware.biometrics.BiometricPrompt$AuthenticationCallback'); | |
biometricPrompt_auth.implementation = function(cancel,executor,callback) { | |
console.log('\x1b[36m'+'[o] Biometric Authentication occurring...'+'\x1b[0m'); | |
console.log('\x1b[36m'+'[o] called BiometricPrompt.authenticate(cancel,executor,callback)'+'\x1b[0m'); | |
this.authenticate.overload('android.os.CancellationSignal', 'java.util.concurrent.Executor', 'android.hardware.biometrics.BiometricPrompt$AuthenticationCallback').call(this,cancel,executor,callback); | |
} | |
} catch (err) { | |
//console.log('[-] BiometricPrompt.authenticate(cancel,executor,callback) not found'); | |
} | |
// BiometricPrompt authenticate(crypto,cancel,executor,callback) method | |
try { | |
var biometricPrompt = Java.use('android.hardware.biometrics.BiometricPrompt'); | |
var biometricPrompt_auth = biometricPrompt.authenticate.overload('android.hardware.biometrics.BiometricPrompt$CryptoObject', 'android.os.CancellationSignal', 'java.util.concurrent.Executor', 'android.hardware.biometrics.BiometricPrompt$AuthenticationCallback'); | |
biometricPrompt_auth.implementation = function(crypto,cancel,executor,callback) { | |
console.log('\x1b[36m'+'[o] Biometric Authentication occurring...'+'\x1b[0m'); | |
console.log('\x1b[36m'+'[o] called BiometricPrompt.authenticate(crypto,cancel,executor,callback)'+'\x1b[0m'); | |
this.authenticate.overload('android.hardware.biometrics.BiometricPrompt$CryptoObject', 'android.os.CancellationSignal', 'java.util.concurrent.Executor', 'android.hardware.biometrics.BiometricPrompt$AuthenticationCallback').call(this,crypto,cancel,executor,callback); | |
} | |
} catch (err) { | |
//console.log('[-] BiometricPrompt.authenticate(crypto,cancel,executor,callback)) not found'); | |
} | |
// FingerprintManager authenticate method (deprecated) | |
try { | |
// Trying to hook each FingerprintManager classes | |
try { | |
fingerprintManager = Java.use('android.hardware.fingerprint.FingerprintManager'); | |
} catch (err) { | |
try { | |
fingerprintManager = Java.use('androidx.core.hardware.fingerprint.FingerprintManager'); | |
} catch (err) { | |
//console.log('[-] FingerprintManager class not found'); | |
} | |
} | |
var fingerprintManager_auth = fingerprintManager.authenticate.overload('android.hardware.fingerprint.FingerprintManager$CryptoObject', 'android.os.CancellationSignal', 'int', 'android.hardware.fingerprint.FingerprintManager$AuthenticationCallback', 'android.os.Handler'); | |
fingerprintManager_auth.implementation = function(crypto,cancel,flags,callback,handler) { | |
console.log('\x1b[36m'+'[o] Biometric Authentication occurring...'+'\x1b[0m'); | |
console.log('\x1b[36m'+'[o] called FingerprintManager.authenticate(crypto,cancel,flags,callback,handler)'+'\x1b[0m'); | |
this.authenticate.overload('android.hardware.fingerprint.FingerprintManager$CryptoObject', 'android.os.CancellationSignal', 'int', 'android.hardware.fingerprint.FingerprintManager$AuthenticationCallback', 'android.os.Handler').call(this,crypto,cancel,flags,callback,handler); | |
} | |
} catch (err) { | |
//console.log('[-] FingerprintManager not found'); | |
} | |
// FingerprintManagerCompat authenticate method (deprecated) | |
try { | |
try { | |
fingerprintManagerCompat = Java.use('android.support.v4.hardware.fingerprint.FingerprintManagerCompat'); | |
} catch (err) { | |
try { | |
fingerprintManagerCompat = Java.use('androidx.core.hardware.fingerprint.FingerprintManagerCompat'); | |
} catch (err) { | |
//console.log('[-] FingerprintManagerCompat class not found'); | |
} | |
} | |
var fingerprintManagerCompat_auth = fingerprintManagerCompat.authenticate.overload('android.hardware.fingerprint.FingerprintManagerCompat$CryptoObject', 'int', 'android.os.CancellationSignal', 'android.hardware.fingerprint.FingerprintManagerCompat$AuthenticationCallback', 'android.os.Handler'); | |
fingerprintManagerCompat_auth.implementation = function(crypto,flags,cancel,callback,handler) { | |
console.log('\x1b[36m'+'[o] Biometric Authentication occurring...'+'\x1b[0m'); | |
console.log('\x1b[36m'+'[o] called FingerprintManagerCompat.authenticate(crypto,flags,cancel,callback,handler)'+'\x1b[0m'); | |
this.authenticate.overload('android.hardware.fingerprint.FingerprintManager$CryptoObject', 'int', 'android.os.CancellationSignal', 'android.hardware.fingerprint.FingerprintManager$AuthenticationCallback', 'android.os.Handler').call(this,crypto,flags,cancel,callback,handler); | |
} | |
} catch (err) { | |
//console.log('[-] FingerprintManagerCompat not found'); | |
} | |
} | |
function hookCipherInits() { | |
// Hooking the various Cipher.init(...) calls | |
const targetCls = Java.use('javax.crypto.Cipher'); | |
const targetFunc = 'init'; | |
var keystoreList = []; | |
var overloads = targetCls[targetFunc].overloads; | |
var params = []; | |
for (var i=0; i<overloads.length; i++) { | |
for (var j in overloads[i].argumentTypes) { | |
params.push(overloads[i].argumentTypes[j].className); | |
} | |
var argLog = generateLogs(params); | |
targetCls[targetFunc].overloads[i].implementation = function() { | |
var opmodeString = this.getOpmodeString(arguments[0]); | |
var algo = this.getAlgorithm(); | |
// Debugging logs | |
console.log('[+] Hooked Cipher.init('+argLog+') call'); | |
console.log('[+] opmode: '+opmodeString); | |
//console.log('[+] key: '+key.$className); | |
fcTracer(arguments, keystoreList, algo); | |
var retval; | |
// Re-calling the original method | |
retval = this[targetFunc].apply(this, arguments); | |
} | |
} | |
} | |
function generateLogs(params) { | |
var argLog = 'opcode'; | |
// Check if the second parameter is a certificate | |
if (params[1].toString() === 'java.security.cert.Certificate') { | |
params.length === 3 ? argLog=argLog.concat(',certificate,random') : argLog=argLog.concat(',certificate'); | |
} else { | |
// Then the second parameter is a key | |
argLog = argLog.concat(',key'); | |
if (params.length === 4) { | |
if (params[2].toString() === 'java.security.spec.AlgorithmParameterSpec') { | |
argLog = argLog.concat('key,paramspec,random'); | |
} else { | |
argLog = argLog.concat('key,param,random'); | |
} | |
} else if (params.length === 3) { | |
if (params[2].toString() === 'java.security.spec.AlgorithmParameterSpec') { | |
argLog = argLog.concat('key,paramspec'); | |
} if (params[2].toString() === 'java.security.spec.AlgorithmParameters') { | |
argLog = argLog.concat('key,param'); | |
} else { | |
argLog = argLog.concat('key,random'); | |
} | |
} | |
} | |
return argLog; | |
} | |
function fcTracer(args, keystoreList, algo) { | |
const keyFactoryCls = Java.use('java.security.KeyFactory'); | |
const keyInfoCls = Java.use('android.security.keystore.KeyInfo'); | |
const keySecretKeyFactoryCls = Java.use('javax.crypto.SecretKeyFactory'); | |
let keystores = ['android.security.keystore.AndroidKeyStoreSecretKey', 'android.security.keystore2.AndroidKeyStoreSecretKey', | |
'android.security.keystore2.AndroidKeyStorePrivateKey', 'android.security.keystore2.AndroidKeyStoreRSAPrivateKey', | |
'android.security.keystore2.AndroidKeyStoreECPrivateKey', 'android.security.keystore2.AndroidKeyStoreEdECPrivateKey', | |
'android.security.keystore2.AndroidKeyStoreXDHPrivateKey']; | |
// Check for Android Keystore usage | |
if (keystores.includes(args[1].$className)) { | |
var keyFactoryObj = null; | |
try { | |
keyFactoryObj = keyFactoryCls.getInstance(args[1].getAlgorithm(), 'AndroidKeyStore'); | |
} catch (err) { | |
keyFactoryObj = keySecretKeyFactoryCls.getInstance(args[1].getAlgorithm(), 'AndroidKeyStore'); | |
} | |
var keyInfo = keyFactoryObj.getKeySpec(args[1], keyInfoCls.class); | |
var keyInfoObj = Java.cast(keyInfo, keyInfoCls); | |
var keystoreAlias = keyInfoObj.getKeystoreAlias(); | |
// Printing keystore data | |
if (keystoreList.includes(keystoreAlias)) { | |
printKeystoreData(keystoreAlias, keyInfoObj, algo, true); | |
} else { | |
keystoreList.push(keystoreAlias); | |
printKeystoreData(keystoreAlias, keyInfoObj, algo, false); | |
} | |
} | |
//console.log('[!] Found the class: '+args[1].$className); | |
} | |
// Keystore data printer | |
function printKeystoreData(keystoreAlias, keyInfoObj, algorithm, alreadyCalled) { | |
// LEGENDA KEY-PROPERTIES: | |
// Main Origin values: | |
// 1=GENERATED-IN-KEYSTORE, 2=IMPORTED-PLAINTEXT, 4=UNKNOWN, 8=SECURELY-IMPORTED | |
// Main Purposes values: | |
// 1=ENCRYPT, 2=DECRYPT, 4=SIGN-MAC, 8=VERIFY-MAC | |
// Main Security-Level values: | |
// 2=SECURITY_LEVEL_STRONGBOX, 1=SECURITY_LEVEL_TRUSTED_ENVIRONMENT, 0=SECURITY_LEVEL_SOFTWARE, -1=SECURITY_LEVEL_UNKNOWN_SECURE, -2=SECURITY_LEVEL_UNKNOWN | |
// Main User-Authentication-Type values: | |
// 0=UNDEFINED, 1=AUTH_DEVICE_CREDENTIAL, 2=AUTH_BIOMETRIC_STRONG | |
// Special value of remainingUsageCount: | |
// -1=UNRESTRICTED_USAGE_COUNT | |
// References: https://developer.android.com/reference/android/security/keystore/KeyProperties | |
if (alreadyCalled==true){ | |
console.log('*****************************'); | |
console.log('[+] Hooked an already called keystore item: '+ keystoreAlias); | |
console.log('*****************************'); | |
} else { | |
console.log('\x1b[33m'+'*****************************'+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] Hooked keystore with alias: '+keystoreAlias+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] Algorithm: '+algorithm+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] Keysize: '+keyInfoObj.getKeySize().toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] BlockModes: '+keyInfoObj.getBlockModes().toString()+'\x1b[0m'); | |
var digests = keyInfoObj.getDigests().toString(); | |
if (digests == '') digests = '[]'; | |
console.log('\x1b[33m'+'[+] Digests: '+digests+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] EncryptionPaddings: '+keyInfoObj.getEncryptionPaddings().toString()+'\x1b[0m'); | |
var keyValidityForConsumptionEnd = keyInfoObj.getKeyValidityForConsumptionEnd(); | |
if (keyValidityForConsumptionEnd != null) keyValidityForConsumptionEnd = keyValidityForConsumptionEnd.toString(); | |
console.log('\x1b[33m'+'[+] keyValidityForConsumptionEnd: '+keyValidityForConsumptionEnd+'\x1b[0m'); | |
var keyValidityForOriginationEnd = keyInfoObj.getKeyValidityForOriginationEnd(); | |
if (keyValidityForOriginationEnd != null) keyValidityForOriginationEnd = keyValidityForOriginationEnd.toString(); | |
console.log('\x1b[33m'+'[+] keyValidityForOriginationEnd: '+keyValidityForOriginationEnd+'\x1b[0m'); | |
var keyValidityStart = keyInfoObj.getKeyValidityStart(); | |
if (keyValidityStart != null) keyValidityStart = keyValidityStart.toString(); | |
console.log('\x1b[33m'+'[+] keyValidityStart: '+keyValidityStart+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] Origin: '+keyInfoObj.getOrigin().toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] Purposes: '+keyInfoObj.getPurposes().toString()+'\x1b[0m'); | |
var signaturePaddings = keyInfoObj.getSignaturePaddings().toString(); | |
if (signaturePaddings == '') signaturePaddings = '[]'; | |
console.log('\x1b[33m'+'[+] signaturePaddings: '+signaturePaddings+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] userAuthenticationValidityDurationSeconds: '+keyInfoObj.getUserAuthenticationValidityDurationSeconds().toString()+'\x1b[0m'); | |
// The isInsideSecureHardware is deprecated and superseded by getSecurityLevel | |
console.log('\x1b[33m'+'[+] isInsideSecureHardware: '+keyInfoObj.isInsideSecureHardware().toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] isInvalidatedByBiometricEnrollment: '+keyInfoObj.isInvalidatedByBiometricEnrollment().toString()+'\x1b[0m'); | |
try { var isTrustedUserPresenceRequired = keyInfoObj.isTrustedUserPresenceRequired(); } catch (err) { } | |
if (isTrustedUserPresenceRequired != null) console.log('\x1b[33m'+'[+] isTrustedUserPresenceRequired: '+isTrustedUserPresenceRequired.toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] isUserAuthenticationRequired: '+keyInfoObj.isUserAuthenticationRequired().toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] isUserAuthenticationRequirementEnforcedBySecureHardware: '+keyInfoObj.isUserAuthenticationRequirementEnforcedBySecureHardware().toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'[+] isUserAuthenticationValidWhileOnBody: '+keyInfoObj.isUserAuthenticationValidWhileOnBody().toString()+'\x1b[0m'); | |
try { var isUserConfirmationRequired = keyInfoObj.isUserConfirmationRequired(); } catch (err) { } | |
if (isUserConfirmationRequired != null) console.log('\x1b[33m'+'[+] isUserConfirmationRequired: '+isUserConfirmationRequired.toString()+'\x1b[0m'); | |
try { var securityLevel = keyInfoObj.getSecurityLevel(); } catch (err) { } | |
if (securityLevel != null) console.log('\x1b[33m'+'[+] securityLevel: '+securityLevel.toString()+'\x1b[0m'); | |
try { var remainingUsageCount = keyInfoObj.getRemainingUsageCount(); } catch (err) { } | |
if (remainingUsageCount != null) console.log('\x1b[33m'+'[+] remainingUsageCount: '+remainingUsageCount.toString()+'\x1b[0m'); | |
// The user authentication types, it is only applied for private/secret keys when isUserAuthenticationRequired is enabled | |
try { var userAuthenticationType = keyInfoObj.getUserAuthenticationType(); } catch (err) { } | |
if (userAuthenticationType != null) console.log('\x1b[33m'+'[+] userAuthenticationType: '+userAuthenticationType.toString()+'\x1b[0m'); | |
console.log('\x1b[33m'+'*****************************'+'\x1b[0m'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment