Skip to content

Instantly share code, notes, and snippets.

@akabe1
Last active April 10, 2024 14:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save akabe1/c9d285ad3d07e7f47fc6d1599d01c8cf to your computer and use it in GitHub Desktop.
Save akabe1/c9d285ad3d07e7f47fc6d1599d01c8cf to your computer and use it in GitHub Desktop.
A script to detect Android Keystore calls
/* 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');
var k1 = 'android.security.keystore.AndroidKeyStoreSecretKey';
var k2 = 'android.security.keystore2.AndroidKeyStoreSecretKey';
// Check for Android Keystore usage
if (args[1].$className === k1 || args[1].$className === k2) {
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);
}
}
}
// 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