Introduction to Android SharedPreferences and Frida
Android applications frequently store small collections of key-value data using SharedPreferences. This mechanism is often used for user settings, session tokens, flags, or other non-sensitive information. However, security misconfigurations can lead to sensitive data being stored here, making it a prime target during penetration testing. Frida, a dynamic instrumentation toolkit, provides an unparalleled ability to hook into application runtime and observe or manipulate calls to Android APIs, including SharedPreferences.
This guide will take you through the practical steps of using Frida to intercept and manipulate Android SharedPreferences access, from enumerating preference files to logging and modifying read/write operations. By the end, you’ll be equipped to leverage Frida for deep analysis of an app’s data storage patterns.
Prerequisites
- A rooted Android device or an emulator (e.g., Android Studio AVD, Genymotion)
- Frida server running on the Android device/emulator
- Frida-tools installed on your host machine (
pip install frida-tools) - Basic understanding of Android application structure and Java/Kotlin
- A target Android application to test against (or a simple test app you create)
Understanding Android SharedPreferences
SharedPreferences allow applications to save and retrieve persistent key-value pairs of primitive data types. This data is stored in XML files within the application’s private data directory (typically /data/data/com.example.app/shared_prefs/). An application can create multiple SharedPreferences files, each identified by a unique name.
Key methods involved:
Context.getSharedPreferences(String name, int mode): Retrieves a SharedPreferences instance for the given name and mode.SharedPreferences.getString(String key, String defValue): Retrieves a string value from the preferences.SharedPreferences.Editor.putString(String key, String value): Writes a string value to the editor.SharedPreferences.Editor.apply()orSharedPreferences.Editor.commit(): Saves the changes made to the editor back to the SharedPreferences file.
Setting up Frida
Ensure Frida server is running on your Android device. First, find the correct Frida server binary for your device’s architecture (e.g., frida-server-*-android-arm64) from the Frida releases page. Push it to the device and execute it:
adb push 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 from your host machine:
frida-ps -U
Hooking Context.getSharedPreferences to Enumerate Files
The first step in understanding an app’s SharedPreferences usage is to identify which files it interacts with. We can hook Context.getSharedPreferences to log the name of each preference file requested.
Frida Script: enumerate_prefs.js
Java.perform(function () {
var Context = Java.use("android.content.Context");
Context.getSharedPreferences.overload("java.lang.String", "int").implementation = function (name, mode) {
console.log("[+] getSharedPreferences called for name: " + name + ", mode: " + mode);
return this.getSharedPreferences(name, mode);
};
console.log("[*] SharedPreferences enumeration hook loaded.");
});
Running the script:
frida -U -l enumerate_prefs.js -f com.example.targetapp --no-pause
Replace com.example.targetapp with the package name of your target application. As the app runs, you’ll see output similar to:
[+] getSharedPreferences called for name: my_app_prefs, mode: 0
[+] getSharedPreferences called for name: user_settings, mode: 0
Hooking Read Operations: Intercepting Data Retrieval
Once you know the SharedPreferences files, the next step is to observe what data is being read from them. We’ll focus on getString but the principle applies to getInt, getBoolean, etc.
Frida Script: log_prefs_read.js
Java.perform(function () {
var SharedPreferences = Java.use("android.content.SharedPreferences");
// Hook getString
SharedPreferences.getString.overload("java.lang.String", "java.lang.String").implementation = function (key, defValue) {
var value = this.getString(key, defValue);
var spName = this.toString().match(/.*?Impl{(.*?)}/)[1] || 'UNKNOWN'; // Extract SP name from toString()
console.log("[+] SharedPreferences READ from " + spName + " - Key: " + key + ", Value: " + value + ", Default: " + defValue);
return value;
};
// Hook getInt, getBoolean, etc. similarly if needed
SharedPreferences.getInt.overload("java.lang.String", "int").implementation = function (key, defValue) {
var value = this.getInt(key, defValue);
var spName = this.toString().match(/.*?Impl{(.*?)}/)[1] || 'UNKNOWN';
console.log("[+] SharedPreferences READ from " + spName + " - Key: " + key + ", Value: " + value + ", Default: " + defValue);
return value;
};
console.log("[*] SharedPreferences read hook loaded.");
});
Note: Extracting the actual SharedPreferences file name from this (the SharedPreferences object) can be tricky. The toString() method often contains enough information to infer it for standard implementations. For custom implementations, you might need a more involved approach, such as hooking Context.getSharedPreferences, storing the returned instances in a map, and then looking them up. For this guide, a simple regex against toString() is sufficient for most cases.
Running the script:
frida -U -l log_prefs_read.js -f com.example.targetapp --no-pause
You’ll see output like:
[+] SharedPreferences READ from my_app_prefs - Key: username, Value: testuser, Default: null
[+] SharedPreferences READ from user_settings - Key: enable_notifications, Value: true, Default: false
Hooking Write Operations: Intercepting and Modifying Data Storage
Observing write operations allows you to understand what data an app intends to store. You can also dynamically modify this data before it’s committed, potentially bypassing security checks or injecting malicious settings.
Frida Script: intercept_prefs_write.js
Java.perform(function () {
var SharedPreferencesEditor = Java.use("android.content.SharedPreferences$Editor");
// Hook putString
SharedPreferencesEditor.putString.overload("java.lang.String", "java.lang.String").implementation = function (key, value) {
console.log("[+] SharedPreferences WRITE (putString) - Key: " + key + ", Original Value: " + value);
// Example: Modify a specific value
if (key === "session_token" && value !== "MY_MODIFIED_TOKEN") {
console.warn("[!] Modifying session_token from: " + value + " to: MY_MODIFIED_TOKEN");
value = "MY_MODIFIED_TOKEN";
}
return this.putString(key, value);
};
// Hook apply() and commit() to know when changes are saved
SharedPreferencesEditor.apply.implementation = function () {
console.log("[+] SharedPreferences Editor: apply() called. Changes saved asynchronously.");
return this.apply();
};
SharedPreferencesEditor.commit.implementation = function () {
console.log("[+] SharedPreferences Editor: commit() called. Changes saved synchronously.");
return this.commit();
};
console.log("[*] SharedPreferences write hook loaded.");
});
Running the script:
frida -U -l intercept_prefs_write.js -f com.example.targetapp --no-pause
When the app attempts to write data, you will see output like:
[+] SharedPreferences WRITE (putString) - Key: username, Original Value: newuser
[+] SharedPreferences Editor: apply() called. Changes saved asynchronously.
If you’ve implemented the modification logic, you’ll also see:
[+] SharedPreferences WRITE (putString) - Key: session_token, Original Value: original_token_xyz
[!] Modifying session_token from: original_token_xyz to: MY_MODIFIED_TOKEN
[+] SharedPreferences Editor: commit() called. Changes saved synchronously.
This demonstrates how you can not only observe but also actively tamper with data as it’s being written to SharedPreferences.
Advanced Scenarios and Considerations
-
Encrypted SharedPreferences:
Some applications use libraries like
EncryptedSharedPreferences(part of AndroidX Security) or custom encryption. In such cases, directly hookinggetStringorputStringmight only reveal the encrypted bytes. You would need to shift your focus to hook the encryption/decryption methods themselves, often found in helper classes or specific methods within the encryption library. -
Runtime Modifications:
Beyond simple logging, Frida allows you to alter the return values of read operations or the arguments of write operations. This can be powerful for testing different application states or bypassing logic that relies on specific SharedPreferences values.
-
Multiple Hooks:
You can combine multiple Frida scripts or integrate various hooks into a single, comprehensive script to get a holistic view of the app’s behavior.
-
Error Handling:
For production-grade Frida scripts, consider adding error handling and more robust ways to identify objects or method overloads, especially when dealing with obfuscated applications.
Conclusion
Frida is an indispensable tool for dynamic analysis of Android applications, and its capabilities for hooking SharedPreferences are particularly useful for uncovering sensitive data and understanding an app’s persistence mechanisms. By following this guide, you’ve learned how to identify SharedPreferences files, intercept both read and write operations, and even modify data on the fly. This knowledge empowers you to conduct more thorough penetration tests and gain deeper insights into the security posture of Android applications.
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 →