Introduction to Android Shared Preferences and Their Security Implications
Android’s SharedPreferences provide a lightweight mechanism for applications to store private primitive data in key-value pairs. This data is stored in XML files within the application’s private directory (typically /data/data/com.package.name/shared_prefs/). While designed for simple settings and configurations, developers sometimes inadvertently store sensitive information such as user tokens, API keys, session IDs, or even unencrypted personal data in Shared Preferences.
For penetration testers and security researchers, inspecting Shared Preferences is a crucial step in understanding an application’s internal workings and identifying potential vulnerabilities. While static analysis and direct file system access (on rooted devices) can reveal these files, dynamic instrumentation with tools like Frida offers a more powerful and versatile approach, especially when dealing with non-rooted devices or when real-time data inspection is required.
Why Frida for Dumping Shared Preferences?
Traditional methods for accessing Shared Preferences often involve:
- `adb pull` on rooted devices: This allows directly pulling XML files from
/data/data//shared_prefs/. However, it requires a rooted device and the app must have already written the preferences to disk. - Static analysis: Decompiling the APK to find where
SharedPreferencesare used. This tells you *what* might be stored but not the *current values* at runtime.
Frida, a dynamic instrumentation toolkit, overcomes these limitations by injecting into a running application process. This allows us to hook Java methods, inspect memory, and even modify runtime behavior. For Shared Preferences, Frida provides the unique ability to:
- Identify all
SharedPreferencesinstances created by the application, even if they’re not immediately written to disk. - Dump the *current* contents of these preferences directly from memory, regardless of file system permissions or rooting status (as long as Frida server is running).
- Monitor real-time changes to preferences as the user interacts with the app.
Prerequisites and Setup
Required Tools
- A rooted or non-rooted Android device (physical or emulator) with Frida Server installed and running.
- ADB (Android Debug Bridge) installed on your host machine.
- Frida client installed on your host machine (
pip install frida-tools). - A target Android application for testing. For this tutorial, we’ll assume
com.example.targetapp.
Basic Frida Setup Verification
First, ensure your Android device is connected and recognized by ADB:
adb devices
You should see your device listed. Next, verify that Frida Server is running and accessible. If you haven’t started it, push the Frida server binary to your device and execute it. Assuming it’s running, check processes:
frida-ps -U
This command lists all processes on the connected USB device. If successful, you’re ready to proceed.
The Strategy: Hooking Shared Preferences Access
Our primary goal is to identify whenever an application accesses or creates a SharedPreferences instance and then extract all its current key-value pairs. We’ll achieve this by hooking key Android API methods.
Identifying `SharedPreferences` Instances
Applications typically obtain a SharedPreferences object using `Context.getSharedPreferences(String name, int mode)` or `PreferenceManager.getDefaultSharedPreferences(Context context)`. By hooking these methods, we can intercept the creation of new preference sets and retrieve their names.
Dumping All Key-Value Pairs
Once we have a reference to a SharedPreferences object, the Java API provides the getAll() method, which returns a `Map` containing all entries. We can simply call this method on the hooked instance to dump its entire content.
Real-time Monitoring of Changes (Advanced)
For a more comprehensive analysis, we can also hook `SharedPreferences.Editor.commit()` and `SharedPreferences.Editor.apply()`. These methods are called when changes made via a `SharedPreferences.Editor` are persisted. By hooking these, we can log individual changes as they happen.
Crafting the Frida Script
Let’s put this strategy into a Frida JavaScript script. This script will intercept calls to getSharedPreferences, log the preference file name, and then dump all current contents of that preference set.
Java.perform(function () { console.log('Frida script loaded: Dumping SharedPreferences'); // Hook ContextWrapper.getSharedPreferences to intercept preference file names var ContextWrapper = Java.use('android.content.ContextWrapper'); ContextWrapper.getSharedPreferences.implementation = function (name, mode) { console.log('[+] Intercepted SharedPreferences: "' + name + '"'); // Call the original method to get the SharedPreferences object var sharedPrefs = this.getSharedPreferences(name, mode); // Dump all entries from this SharedPreferences object dumpSharedPreferences(sharedPrefs, name); return sharedPrefs; }; // Hook PreferenceManager.getDefaultSharedPreferences for cases where default preferences are used // This might require hooking a specific Activity or Application context depending on usage var PreferenceManager = Java.use('android.preference.PreferenceManager'); PreferenceManager.getDefaultSharedPreferences.implementation = function (context) { var sharedPrefs = this.getDefaultSharedPreferences(context); console.log('[+] Intercepted DefaultSharedPreferences.'); // Since default preferences don't have an explicit 'name' parameter in this call, // we need to infer it or just label it as 'default'. // The default name is usually related to the package name. var packageName = context.getPackageName(); var defaultPrefsName = packageName + '_preferences'; // Common default naming convention dumpSharedPreferences(sharedPrefs, defaultPrefsName); return sharedPrefs; }; function dumpSharedPreferences(prefsObject, prefsName) { if (prefsObject === null) { console.log('[-] SharedPreferences object is null for: ' + prefsName); return; } try { var allEntries = prefsObject.getAll(); var map = Java.cast(allEntries, Java.use('java.util.Map')); if (map.size() == 0) { console.log(' No entries found for "' + prefsName + '"'); return; } console.log(' --- Dumping contents of "' + prefsName + '" (size: ' + map.size() + ') ---'); var iterator = map.entrySet().iterator(); while (iterator.hasNext()) { var entry = iterator.next(); var key = entry.getKey(); var value = entry.getValue(); // Handle null values gracefully var valueStr = (value !== null) ? value.toString() : 'null'; // Attempt to detect if value is an object and stringify if possible if (Java.isJavaObject(value) && value.$className !== undefined) { if (value.$className.startsWith('java.lang.')) { // Primitive wrappers, handled by toString() } else { // Other objects, try to convert to JSON if possible, else toString() try { valueStr = JSON.stringify(Java.cast(value, Java.use('java.lang.Object'))); } catch (e) { valueStr = value.$className + '@' + value.hashCode() + ' (Object, toString: ' + value.toString() + ')'; } } } console.log(' Key: "' + key + '", Value: "' + valueStr + '"'); } console.log(' --- End dump for "' + prefsName + '" ---'); } catch (e) { console.error(' Error dumping SharedPreferences "' + prefsName + '": ' + e.message); } }})
Executing the Script and Analyzing Output
Running the Frida Script
Save the above code as shared_prefs_dump.js. To execute it against your target application (e.g., com.example.targetapp), use the following command:
frida -U -l shared_prefs_dump.js --no-pause -f com.example.targetapp
-U: Attaches to a USB device.-l shared_prefs_dump.js: Loads our Frida script.--no-pause: Prevents Frida from pausing the application immediately after injection, allowing it to run normally.-f com.example.targetapp: Spawns and attaches to the specified application package name.
Once the app launches, interact with it. Perform actions that you suspect might involve reading or writing to Shared Preferences (e.g., logging in, changing settings, navigating screens). As the app calls `getSharedPreferences`, Frida will intercept these calls, dump the contents, and print them to your console.
Interpreting the Results
The output in your console will show messages similar to this:
[+] Intercepted SharedPreferences: "user_session_data" --- Dumping contents of "user_session_data" (size: 3) --- Key: "username", Value: "testuser" Key: "session_token", Value: "s3cr3tTok3n123" Key: "is_logged_in", Value: "true" --- End dump for "user_session_data" ---[+] Intercepted SharedPreferences: "app_settings" No entries found for "app_settings"
This output clearly identifies the name of the SharedPreferences file (e.g.,
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 →