Introduction: The Peril of Insecure Internal Storage
Android applications often store various types of data internally, ranging from user preferences and session tokens to sensitive API keys and even biometric data. While Android provides mechanisms for secure storage, developers sometimes make critical errors, leading to plaintext storage of sensitive information in areas accessible by the app itself. This ‘insecure internal storage’ vulnerability poses a significant risk, as a compromised device or a malicious app with sufficient permissions could potentially access and exfiltrate this data.
This article will guide you through identifying and exploiting these vulnerabilities using Frida, a powerful dynamic instrumentation toolkit. Frida allows us to inject custom scripts into running processes, hook into functions, and modify runtime behavior, making it an invaluable tool for Android application penetration testing.
Setting Up Your Android Penetration Testing Environment
Before diving into exploitation, we need a properly configured environment.
1. Rooted Android Device Preparation
You’ll need a rooted Android device or emulator. Root access is crucial for pushing the Frida server and accessing protected application directories.
- Ensure ADB (Android Debug Bridge) is installed on your workstation.
- Connect your Android device and verify ADB connectivity:
adb devices
List of devices attached
XXXXXXXXXXXX device
- Grant ADB root privileges, if your custom ROM allows (some production roots might not):
adb root
restarting adbd as root
2. Installing Frida Server
The Frida server runs on the Android device and communicates with your workstation’s Frida client.
- Determine your device’s CPU architecture (e.g., arm64-v8a, armeabi-v7a):
adb shell getprop ro.product.cpu.abi
arm64-v8a
- Download the appropriate `frida-server` for your architecture from the Frida GitHub releases page.
- Push the `frida-server` binary to a writable location on your device (e.g., `/data/local/tmp`):
adb push /path/to/frida-server /data/local/tmp/frida-server
/path/to/frida-server: 1 file pushed, 0 skipped. 16.5 MB/s (18260656 bytes in 1.050s)
- Set executable permissions and start the server:
adb shell
cd /data/local/tmp
chmod 755 frida-server
./frida-server &
The `&` puts it in the background. You can exit the ADB shell now.
3. Workstation Setup
Install `frida-tools` on your workstation:
pip install frida-tools
Verify Frida is communicating with your device:
frida-ps -U
PID NAME
[...]
Identifying Insecure Storage Practices
Before hooking, it’s beneficial to know where to look. Android apps typically store private data in `/data/data//`. Key subdirectories include `files`, `databases`, and `shared_prefs`.
1. Manual File System Inspection
Using `adb shell`, you can directly inspect these directories:
adb shell
ls -l /data/data/com.example.insecureapp/shared_prefs/
-rw-rw---- 1 u0_a232 u0_a232 1234 2023-10-27 10:30 user_settings.xml
ls -l /data/data/com.example.insecureapp/files/
-rw-rw---- 1 u0_a232 u0_a232 567 2023-10-27 10:35 sensitive_data.bin
Often, XML files in `shared_prefs` or `.json`, `.txt`, `.bin` files in `files` can contain plaintext sensitive data. Reading these files directly can sometimes reveal the vulnerability without Frida, but Frida is essential for dynamic data interception.
2. Common Vulnerabilities
- Plaintext Shared Preferences: Storing sensitive data like API keys, session tokens, or even passwords directly in `SharedPreferences` without encryption.
- Unencrypted Files: Writing sensitive data to application-specific files (`FileOutputStream`) without encryption.
Exploiting Shared Preferences with Frida
Let’s consider an application that stores a user’s API token in `SharedPreferences`. Our goal is to intercept this token as it’s being written.
Step 1: Identify the Target Application and Methods
First, find the package name of the target application using `frida-ps -Uai` (list installed apps).
We are interested in the `android.content.SharedPreferences$Editor` class, specifically its `putString` method, which is used to store string values, and its `commit()` or `apply()` methods, which save the changes.
Step 2: Crafting the Frida Script (`dump_prefs.js`)
Java.perform(function () {
console.log("[*] Hooking SharedPreferences.Editor.putString");
var SharedPreferencesEditor = Java.use("android.content.SharedPreferences$Editor");
SharedPreferencesEditor.putString.overload('java.lang.String', 'java.lang.String').implementation = function (key, value) {
console.log("--------------------------------------------------------------------------------");
console.log("[*] SharedPreferences.Editor.putString called!");
console.log(" Key: " + key);
console.log(" Value: " + value);
console.log(" Stack Trace:n" + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
console.log("--------------------------------------------------------------------------------");
return this.putString(key, value);
};
SharedPreferencesEditor.commit.implementation = function () {
console.log("[*] SharedPreferences.Editor.commit() called. Data likely persisted.");
return this.commit();
};
SharedPreferencesEditor.apply.implementation = function () {
console.log("[*] SharedPreferences.Editor.apply() called. Data likely persisted (asynchronously).");
return this.apply();
};
console.log("[*] SharedPreferences hooks loaded.");
});
Step 3: Executing the Script
Run the script, attaching to the target application’s package name. The `–no-pause` flag ensures the application starts immediately.
frida -U -l dump_prefs.js -f com.example.insecureapp --no-pause
Now, interact with the application. When it attempts to store data via `SharedPreferences.Editor.putString`, you will see output similar to this in your console:
[*] SharedPreferences.Editor.putString called!
Key: user_api_token
Value: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Stack Trace:...
--------------------------------------------------------------------------------
Congratulations, you’ve intercepted sensitive data from `SharedPreferences`!
Intercepting FileStream Writes for Sensitive Data
Sometimes, applications write sensitive data directly to files using `java.io.FileOutputStream`. We can hook the `write` methods to capture this data.
Step 1: Hooking FileOutputStream.write
We’ll target the `write(byte[] b)` and `write(byte[] b, int off, int len)` methods of `java.io.FileOutputStream`.
Step 2: Crafting the Frida Script (`intercept_file_write.js`)
Java.perform(function () {
console.log("[*] Hooking FileOutputStream.write");
var FileOutputStream = Java.use("java.io.FileOutputStream");
// Hooking write(byte[] b)
FileOutputStream.write.overload('[B').implementation = function (b) {
var data = Java.array('byte', b);
var decoded = String.fromCharCode.apply(null, data);
console.log("--------------------------------------------------------------------------------");
console.log("[*] FileOutputStream.write(byte[] b) called!");
console.log(" Data (String attempt): " + decoded);
console.log(" Data (Hex): " + Array.prototype.map.call(data, function (byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join(' '));
console.log(" Stack Trace:n" + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
console.log("--------------------------------------------------------------------------------");
return this.write(b);
};
// Hooking write(byte[] b, int off, int len)
FileOutputStream.write.overload('[B', 'int', 'int').implementation = function (b, off, len) {
var data = Java.array('byte', b);
// Extract only the relevant part of the byte array based on offset and length
var relevantData = data.slice(off, off + len);
var decoded = String.fromCharCode.apply(null, relevantData);
console.log("--------------------------------------------------------------------------------");
console.log("[*] FileOutputStream.write(byte[] b, int off, int len) called!");
console.log(" Offset: " + off + ", Length: " + len);
console.log(" Data (String attempt): " + decoded);
console.log(" Data (Hex): " + Array.prototype.map.call(relevantData, function (byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join(' '));
console.log(" Stack Trace:n" + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
console.log("--------------------------------------------------------------------------------");
return this.write(b, off, len);
};
console.log("[*] FileOutputStream hooks loaded.");
});
Step 3: Running the Interception
Execute the script, again attaching to the target application:
frida -U -l intercept_file_write.js -f com.example.insecureapp --no-pause
As the application writes data to files, you will see output in your console. The script attempts to decode the bytes to a string and also provides a hex dump, which is useful for non-textual data or for identifying encoding issues.
[*] FileOutputStream.write(byte[] b) called!
Data (String attempt): {"username":"testuser","password":"Pa$$w0rd!"}
Data (Hex): 7b 22 75 73 65 72 6e 61 6d 65 22 3a 22 74 65 73 74 75 73 65 72 22 2c 22 70 61 73 73 77 6f 72 64 22 3a 22 50 61 24 24 77 30 72 64 21 22 7d
Stack Trace:...
--------------------------------------------------------------------------------
Mitigation Strategies: Securing Internal Storage
For developers, preventing these vulnerabilities is paramount:
- Android KeyStore: Use the Android KeyStore system to securely store cryptographic keys, which can then be used to encrypt sensitive data before writing it to `SharedPreferences` or files.
- Encryption: Always encrypt sensitive data before storing it in internal storage. Implement robust encryption schemes (e.g., AES-256) and manage keys securely.
- Password-based Encryption: For highly sensitive user-provided data, derive encryption keys from user passwords, ensuring proper key stretching (e.g., PBKDF2).
- Third-party Libraries: Utilize well-vetted libraries designed for secure storage, which often abstract away complex cryptographic operations.
- `MODE_PRIVATE` and `Context.MODE_PRIVATE`: While `SharedPreferences` and `FileOutputStream` default to `MODE_PRIVATE`, this only prevents other applications from accessing the data. It does not protect against a rooted device or dynamic analysis.
Conclusion
Exploiting insecure internal storage on Android is a common and critical finding in penetration tests. Frida empowers security researchers and ethical hackers to dynamically intercept and analyze data as it’s being written, providing undeniable proof of concept for these vulnerabilities. By understanding the underlying mechanisms and employing powerful tools like Frida, you can effectively identify weaknesses and help build more secure Android applications. Always remember to use these techniques ethically and only on systems you have explicit permission to test.
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 →