All Research/Android Security
Android SecuritySep 04, 20268 min read

Deep Dive: Reversing Android Keystore Implementations with Frida

Intercepting cryptographic operations, hardware-backed keys, and biometric bypasses.

“A hands-on walkthrough showing how modern Android applications store sensitive tokens in the Keystore, how hardware-backed TEEs protect them, and how researchers hook Cipher.doFinal() using Frida runtime instrumentation.”

1. Android Keystore Architecture & TEE

The Android Keystore system lets developers store cryptographic keys in a container to make them more difficult to extract from the device. When keys are created with the setKeyStoreProvider flag, key material is generated directly inside a Hardware-Backed Keystore, such as a Trusted Execution Environment (TEE) or StrongBox Keymaster.

Even if an attacker gains root access to the Linux kernel running on the application processor, the raw private key material never enters host RAM. However, what attackers cannot steal at rest, they often intercept during runtime memory operations.

StrongBox Keymaster operates on dedicated tamper-resistant hardware with isolated CPU, RAM, and true hardware random number generators (TRNG).

2. Identifying Cryptographic Targets with JADX

Before injecting runtime hooks, we decompile the APK using JADX to locate where javax.crypto.Cipher is initialized and invoked. We search for references to Cipher.getInstance('AES/GCM/NoPadding') and examine the KeyGenParameterSpec configurations.

Pay close attention to user authentication requirements. If setUserAuthenticationRequired(false) was specified during key generation, the key can be used by any process context without user presence prompts.

CryptoHelper.java (Decompiled Snippet)
java
// Target function identified in decompiled APK
public byte[] decryptAuthToken(byte[] encryptedData, byte[] iv) {
    try {
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        SecretKey key = (SecretKey) keyStore.getKey("user_session_key", null);
        
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(128, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, spec);
        return cipher.doFinal(encryptedData); // Target for runtime hooking
    } catch (Exception e) {
        Log.e("Security", "Decryption failed", e);
        return null;
    }
}

3. Writing the Frida Hook for Cipher.doFinal

Because the hardware TEE returns the decrypted bytes back to the Java runtime through the JNI bridge, we can hook javax.crypto.Cipher.doFinal() right when plaintext is returned to the app.

Below is the JavaScript snippet we inject into the running process using Frida. It intercepts calls to doFinal, prints the input ciphertext, and displays the plaintext result in hex and UTF-8 strings.

hook_cipher.js
javascript
Java.perform(function () {
    const Cipher = Java.use('javax.crypto.Cipher');
    
    // Hook Cipher.doFinal(byte[])
    Cipher.doFinal.overload('[B').implementation = function (inputBytes) {
        console.log('[*] Cipher.doFinal([B) intercepted!');
        console.log('[*] Input bytes length: ' + inputBytes.length);
        
        // Execute original method to obtain decrypted bytes
        const result = this.doFinal(inputBytes);
        
        // Convert decrypted output to string
        const ByteString = Java.use('java.lang.String');
        const plaintext = ByteString.$new(result);
        console.log('[✓] Decrypted Plaintext: ' + plaintext);
        
        return result;
    };
});

4. Circumventing CryptoObject Biometrics

Many developers make the mistake of using BiometricPrompt only as a visual gate, evaluating onAuthenticationSucceeded() as a boolean flag without binding the Cipher to the BiometricPrompt.CryptoObject.

When crypto objects are unbound, an attacker can simply hook the callback class and invoke onAuthenticationSucceeded(null), completely bypassing fingerprint or face unlock barriers.

Never rely on boolean UI checks for biometrics. Always require a valid Cryptographic Key initialized with setUserAuthenticationRequired(true) that unlocks the payload only after hardware authorization.

5. Developer Hardening & Countermeasures

To harden your Android app against dynamic instrumentation and runtime keystore interception, adopt defense-in-depth measures:

Key Remediation Checklist:
  • Mandate StrongBox Keymaster backed key generation for high-value secrets.
  • Implement active Frida and ptrace anti-debugging checks in native C/C++ libraries.
  • Always bind BiometricPrompt with a valid CryptoObject.
  • Leverage SafetyNet / Play Integrity API to detect compromised, rooted, or emulated runtimes.
  • Ensure sensitive tokens in memory are zeroized (Arrays.fill(bytes, (byte) 0)) immediately after use.
KT
Written by
Kshitija & Research Team
OffSec Lead // VAPT & IoT Researcher
// RELATED_INTELLIGENCE

Continue Reading

IoT Security7 min read

Extracting IoT Firmware with Binwalk & Auditing Hardcoded Shadow Hashes

Learn how hardware hackers dump SPI Flash chips, unpack SquashFS and JFFS2 filesystems with Binwalk, and crack legacy DES/MD5 shadow hashes found on commercial smart devices.

By KshitijaRead Analysis
API Security6 min read

Hunting BOLA/IDOR in GraphQL Microservices: A Methodology

Broken Object Level Authorization (BOLA) remains the #1 vulnerability on modern APIs. Here is our step-by-step methodology for mapping authorization graphs and discovering cross-tenant data leaks in GraphQL.

By TechnicalRead Analysis