Introduction: Unveiling Android’s Cryptographic Secrets with Frida
In the realm of Android application penetration testing, understanding and manipulating an app’s cryptographic operations is paramount. Many applications rely on client-side encryption for sensitive data, and without insight into these processes, bypassing security controls or extracting valuable information can be impossible. This guide delves into using Frida, a dynamic instrumentation toolkit, to hook Android’s core cryptographic APIs (AES and RSA) to extract keys, initialization vectors (IVs), and unencrypted data.
Frida’s power lies in its ability to inject JavaScript into running processes, allowing us to inspect, modify, and even bypass functions in real-time. For cryptographic analysis, this means we can intercept calls to `Cipher.init()`, `Cipher.doFinal()`, and key generation methods, exposing the underlying cryptographic primitives and parameters.
Prerequisites
- An Android device or emulator with root access.
- Frida server running on the Android device.
- Frida command-line tools installed on your host machine (`pip install frida-tools`).
- Android Debug Bridge (ADB) installed and configured.
- Basic understanding of Java/Android development concepts.
Setting Up Frida on Android
First, ensure your Android device is properly set up with Frida. Download the appropriate `frida-server` for your device’s architecture (e.g., `frida-server-*-android-arm64`) from the Frida releases page.
adb push /path/to/frida-server /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"
Verify Frida is running by listing processes:
frida-ps -U
Hooking AES Encryption/Decryption and Extracting Keys
AES (Advanced Encryption Standard) is a symmetric block cipher widely used in Android applications. We’ll focus on intercepting `javax.crypto.Cipher` methods, specifically `init()` to get the key and IV, and `doFinal()` to capture plaintext/ciphertext.
Identifying Target Methods
The `javax.crypto.Cipher` class is the central point. The most interesting overloads for `init` are:
- `init(int opmode, Key key)`
- `init(int opmode, Key key, AlgorithmParameterSpec params)`
- `init(int opmode, Key key, IvParameterSpec iv)`
And for `doFinal`:
- `doFinal(byte[] input)`
- `doFinal(byte[] input, int inputOffset, int inputLen)`
Frida Script for AES Hooking
Let’s create a Frida script (`aes_hook.js`) to intercept these methods:
Java.perform(function() {
console.log("[*] Starting AES Hooking Script");
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").implementation = function(opmode, key, params) {
var opmodeStr = (opmode == 1) ? "ENCRYPT_MODE" : ((opmode == 2) ? "DECRYPT_MODE" : "UNKNOWN_MODE");
console.log("[*] Cipher.init(opmode=" + opmodeStr + ", key=" + key + ", params=" + params + ") called");
// Extract AES Key
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
if (key.$instanceof(SecretKeySpec)) {
var secretKeyBytes = Java.cast(key, SecretKeySpec).getEncoded();
console.log(" [+] AES Key (Hex): " + Array.from(secretKeyBytes).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] AES Key (Base64): " + Java.use("android.util.Base64").encodeToString(secretKeyBytes, 0));
}
// Extract IV
var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
if (params.$instanceof(IvParameterSpec)) {
var ivBytes = Java.cast(params, IvParameterSpec).getIV();
console.log(" [+] AES IV (Hex): " + Array.from(ivBytes).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] AES IV (Base64): " + Java.use("android.util.Base64").encodeToString(ivBytes, 0));
}
return this.init(opmode, key, params);
};
// Hooking the second common init overload (without AlgorithmParameterSpec)
Cipher.init.overload("int", "java.security.Key").implementation = function(opmode, key) {
var opmodeStr = (opmode == 1) ? "ENCRYPT_MODE" : ((opmode == 2) ? "DECRYPT_MODE" : "UNKNOWN_MODE");
console.log("[*] Cipher.init(opmode=" + opmodeStr + ", key=" + key + ") called");
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
if (key.$instanceof(SecretKeySpec)) {
var secretKeyBytes = Java.cast(key, SecretKeySpec).getEncoded();
console.log(" [+] AES Key (Hex): " + Array.from(secretKeyBytes).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] AES Key (Base64): " + Java.use("android.util.Base64").encodeToString(secretKeyBytes, 0));
}
return this.init(opmode, key);
};
Cipher.doFinal.overload("[B").implementation = function(input) {
var ret = this.doFinal(input);
console.log("[*] Cipher.doFinal([B]) called");
console.log(" [+] Input (Hex): " + Array.from(input).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] Input (Base64): " + Java.use("android.util.Base64").encodeToString(input, 0));
console.log(" [+] Output (Hex): " + Array.from(ret).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] Output (Base64): " + Java.use("android.util.Base64").encodeToString(ret, 0));
// Attempt to decrypt if in ENCRYPT_MODE or encrypt if in DECRYPT_MODE (requires known key/IV)
return ret;
};
Cipher.doFinal.overload("[B", "int", "int").implementation = function(input, inputOffset, inputLen) {
var ret = this.doFinal(input, inputOffset, inputLen);
console.log("[*] Cipher.doFinal([B, int, int]) called");
var actualInput = input.slice(inputOffset, inputOffset + inputLen);
console.log(" [+] Input (Hex): " + Array.from(actualInput).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] Input (Base64): " + Java.use("android.util.Base64").encodeToString(actualInput, 0));
console.log(" [+] Output (Hex): " + Array.from(ret).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] Output (Base64): " + Java.use("android.util.Base64").encodeToString(ret, 0));
return ret;
};
console.log("[*] AES Hooking Script Loaded. Waiting for activity.");
});
Running the AES Hook
To run this script against a target Android application (replace `com.example.targetapp` with the actual package name):
frida -U -f com.example.targetapp -l aes_hook.js --no-pause
Frida will attach to the app, load the script, and then resume the app. All AES operations will now be logged to your console.
Hooking RSA Encryption/Decryption and Key Extraction
RSA is an asymmetric cipher, commonly used for secure key exchange or digital signatures. We’re interested in extracting public and private keys and observing their usage.
Identifying Target Methods for RSA
Similar to AES, `javax.crypto.Cipher` is used for RSA operations. Additionally, we’ll look at `java.security.KeyFactory` for generating keys from specifications and `java.security.KeyPairGenerator` for generating new key pairs.
Frida Script for RSA Hooking
Create `rsa_hook.js`:
Java.perform(function() {
console.log("[*] Starting RSA Hooking Script");
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.init.overload("int", "java.security.Key", "java.security.SecureRandom").implementation = function(opmode, key, random) {
var opmodeStr = (opmode == 1) ? "ENCRYPT_MODE" : ((opmode == 2) ? "DECRYPT_MODE" : "UNKNOWN_MODE");
console.log("[*] Cipher.init(RSA, opmode=" + opmodeStr + ", key=" + key + ") called");
var PublicKey = Java.use("java.security.PublicKey");
var PrivateKey = Java.use("java.security.PrivateKey");
if (key.$instanceof(PublicKey)) {
var pubKeyBytes = Java.cast(key, PublicKey).getEncoded();
console.log(" [+] RSA Public Key (X.509, Base64): " + Java.use("android.util.Base64").encodeToString(pubKeyBytes, 0));
} else if (key.$instanceof(PrivateKey)) {
var privKeyBytes = Java.cast(key, PrivateKey).getEncoded();
console.log(" [+] RSA Private Key (PKCS#8, Base64): " + Java.use("android.util.Base64").encodeToString(privKeyBytes, 0));
}
return this.init(opmode, key, random);
};
Cipher.init.overload("int", "java.security.Key").implementation = function(opmode, key) {
var opmodeStr = (opmode == 1) ? "ENCRYPT_MODE" : ((opmode == 2) ? "DECRYPT_MODE" : "UNKNOWN_MODE");
console.log("[*] Cipher.init(RSA, opmode=" + opmodeStr + ", key=" + key + ") called");
var PublicKey = Java.use("java.security.PublicKey");
var PrivateKey = Java.use("java.security.PrivateKey");
if (key.$instanceof(PublicKey)) {
var pubKeyBytes = Java.cast(key, PublicKey).getEncoded();
console.log(" [+] RSA Public Key (X.509, Base64): " + Java.use("android.util.Base64").encodeToString(pubKeyBytes, 0));
} else if (key.$instanceof(PrivateKey)) {
var privKeyBytes = Java.cast(key, PrivateKey).getEncoded();
console.log(" [+] RSA Private Key (PKCS#8, Base64): " + Java.use("android.util.Base64").encodeToString(privKeyBytes, 0));
}
return this.init(opmode, key);
};
// Hooking KeyFactory.generatePublic to catch key generation from specs
var KeyFactory = Java.use("java.security.KeyFactory");
KeyFactory.generatePublic.implementation = function(keySpec) {
var ret = this.generatePublic(keySpec);
console.log("[*] KeyFactory.generatePublic(keySpec=" + keySpec + ") called");
var pubKeyBytes = ret.getEncoded();
console.log(" [+] Generated Public Key (X.509, Base64): " + Java.use("android.util.Base64").encodeToString(pubKeyBytes, 0));
return ret;
};
KeyFactory.generatePrivate.implementation = function(keySpec) {
var ret = this.generatePrivate(keySpec);
console.log("[*] KeyFactory.generatePrivate(keySpec=" + keySpec + ") called");
var privKeyBytes = ret.getEncoded();
console.log(" [+] Generated Private Key (PKCS#8, Base64): " + Java.use("android.util.Base64").encodeToString(privKeyBytes, 0));
return ret;
};
// Hooking KeyPairGenerator.generateKeyPair to catch dynamically generated pairs
var KeyPairGenerator = Java.use("java.security.KeyPairGenerator");
KeyPairGenerator.generateKeyPair.implementation = function() {
var keyPair = this.generateKeyPair();
console.log("[*] KeyPairGenerator.generateKeyPair() called");
var pubKeyBytes = keyPair.getPublic().getEncoded();
var privKeyBytes = keyPair.getPrivate().getEncoded();
console.log(" [+] Generated Public Key (X.509, Base64): " + Java.use("android.util.Base64").encodeToString(pubKeyBytes, 0));
console.log(" [+] Generated Private Key (PKCS#8, Base64): " + Java.use("android.util.Base64").encodeToString(privKeyBytes, 0));
return keyPair;
};
// Cipher.doFinal for RSA is similar to AES, but ensure context is RSA
// (could check algorithm via cipher.getAlgorithm() if needed)
Cipher.doFinal.overload("[B").implementation = function(input) {
var ret = this.doFinal(input);
if (this.getAlgorithm().indexOf("RSA") != -1) {
console.log("[*] RSA Cipher.doFinal([B]) called");
console.log(" [+] Input (Hex): " + Array.from(input).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] Input (Base64): " + Java.use("android.util.Base64").encodeToString(input, 0));
console.log(" [+] Output (Hex): " + Array.from(ret).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join(''));
console.log(" [+] Output (Base64): " + Java.use("android.util.Base64").encodeToString(ret, 0));
}
return ret;
};
console.log("[*] RSA Hooking Script Loaded. Waiting for activity.");
});
Running the RSA Hook
frida -U -f com.example.targetapp -l rsa_hook.js --no-pause
Advanced Considerations and Challenges
While these scripts provide a strong foundation, real-world applications often employ techniques to hinder analysis:
- Obfuscation: Tools like ProGuard or DexGuard rename classes and methods, making it harder to find the exact targets. You might need to use static analysis (decompilers like Jadx or Ghidra) to identify the obfuscated names.
- Anti-Frida Measures: Apps can detect Frida by checking for `frida-server` processes, specific memory regions, or network ports. Bypassing these often involves modifying Frida’s behavior or patching the app’s detection logic.
- Custom Cryptography: Some applications implement their own cryptographic primitives, bypassing standard Android APIs. In such cases, you might need to hook lower-level native functions (e.g., in JNI libraries) or analyze the custom implementation statically.
- Key Derivation Functions (KDFs): Keys are often not stored directly but derived from passwords or other secrets using KDFs (e.g., PBKDF2). You would need to hook the KDF itself to extract the final derived key.
Conclusion
Frida is an indispensable tool for dynamic analysis of Android applications, particularly when it comes to understanding and manipulating cryptographic operations. By strategically hooking `Cipher.init`, `Cipher.doFinal`, and key generation methods, you can gain deep insights into an app’s security mechanisms, extract crucial cryptographic parameters, and effectively bypass client-side encryption. This practical guide provides a solid starting point for any security researcher or penetration tester looking to enhance their Android crypto analysis capabilities.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →