Android App Penetration Testing & Frida Hooks

Frida for Android Pen-Testers: Manipulating SharedPreferences Without Root

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SharedPreferences and Frida

Android applications frequently use SharedPreferences for storing lightweight key-value pairs, typically for user settings, feature flags, session tokens, or other application-specific configurations. While convenient for developers, these preferences can be a treasure trove for penetration testers. Gaining access to and manipulating SharedPreferences can reveal sensitive information, alter application behavior, bypass restrictions, or even escalate privileges within the application’s context.

Traditionally, accessing or modifying these files (located in /data/data/<package_name>/shared_prefs/) requires root access on the Android device, as they reside in the app’s private data directory. However, during a penetration test, root access isn’t always available or desirable, especially when testing devices where root might alert security mechanisms or simply be impractical. This is where Frida, a dynamic instrumentation toolkit, becomes an invaluable asset.

Frida allows you to inject scripts into running processes on Android, iOS, Windows, macOS, and Linux. By doing so, you can hook into application functions, modify their behavior, inspect data, and even call arbitrary methods at runtime—all without modifying the application binary or requiring root access for the specific manipulation of an app’s internal logic, as long as Frida itself can run on the device (which sometimes still requires root for `frida-server`, but the *manipulation itself* is performed in the app’s userland context).

Prerequisites and Setup

Before diving into the specifics, ensure you have a basic Frida setup operational. This typically involves:

  • A rooted Android device or emulator with `frida-server` running. (While the *manipulation* doesn’t require the app itself to be rooted, `frida-server` often needs root permissions to inject into other processes).
  • `frida-tools` installed on your host machine (`pip install frida-tools`).
  • ADB configured to communicate with your Android device.

You’ll also need to identify the package name of your target application (e.g., com.example.app). You can find this using `adb shell pm list packages -f` or `adb shell dumpsys window windows | grep -E ‘mCurrentFocus|mFocusedApp’`.

Understanding SharedPreferences Access

Android applications interact with SharedPreferences primarily through the android.content.SharedPreferences interface and its nested Editor interface. The common workflow involves:

  1. Obtaining a SharedPreferences instance, usually via Context.getSharedPreferences(name, mode) or Activity.getPreferences(mode).
  2. Reading values using methods like getString(key, defValue), getInt(key, defValue), getBoolean(key, defValue), etc.
  3. To write values, obtaining an Editor instance via SharedPreferences.edit().
  4. Putting values using methods like Editor.putString(key, value), Editor.putInt(key, value), etc.
  5. Committing changes using Editor.apply() (asynchronous) or Editor.commit() (synchronous).

Our Frida strategy will focus on intercepting these key methods to observe and alter the data flow.

Frida Hooking Strategy for SharedPreferences

Our goal is to dynamically intercept calls to getSharedPreferences to gain a handle on the SharedPreferences object. Once we have this, we can then hook its read methods (like getString, getBoolean) and the Editor‘s write methods (like putString, putBoolean) as well as the apply/commit methods to manipulate the data before it’s read by the app or before it’s written to disk.

1. Identifying the Target SharedPreferences

First, let’s write a simple Frida script to log every time an application requests a SharedPreferences instance. This helps us discover the names of preference files used by the app.

Java.perform(function() {    console.log("[*] SharedPreferences Discovery Script Started");    var ContextWrapper = Java.use('android.content.ContextWrapper');    ContextWrapper.getSharedPreferences.implementation = function(name, mode) {        console.log("[+] getSharedPreferences called: Name = " + name + ", Mode = " + mode);        return this.getSharedPreferences(name, mode);    };});

To run this script (let’s save it as `discover_prefs.js`), attach Frida to your target app:

frida -U -f com.target.package -l discover_prefs.js --no-pause

As you interact with the app, you’ll see output in your terminal indicating the names of the SharedPreferences files being accessed. For this tutorial, let’s assume we discovered a preference file named my_app_prefs.

2. Reading and Modifying Values Dynamically

Now, let’s craft a more comprehensive Frida script to not only log but also modify the values read from and written to my_app_prefs.

Java.perform(function() {    console.log("[*] Frida SharedPreferences Manipulation Script Initiated");    // Get references to necessary classes    var ContextWrapper = Java.use('android.content.ContextWrapper');    var SharedPreferences = Java.use('android.content.SharedPreferences');    var Editor = Java.use('android.content.SharedPreferences$Editor');    // Hook ContextWrapper.getSharedPreferences to intercept preference file access    ContextWrapper.getSharedPreferences.implementation = function(name, mode) {        console.log("----------------------------------------------------------------------------------------------------------------");        console.log("[HOOKED] Application is requesting SharedPreferences: '" + name + "' with mode: " + mode);        // Call the original method to get the actual SharedPreferences object        var prefs = this.getSharedPreferences(name, mode);        // Check if this is our target SharedPreferences file        if (name === "my_app_prefs") {            console.log("[INFO] Intercepting methods for target SharedPreferences: '" + name + "'");            // --- Hook Read Operations (e.g., getString, getBoolean) ---            prefs.getString.implementation = function(key, defValue) {                var originalValue = this.getString(key, defValue);                console.log("[READ] SP: '" + name + "' | Key: '" + key + "' | Original: '" + originalValue + "' | Default: '" + defValue + "'");                // Example: Manipulate a specific string value                if (key === "premium_feature_unlocked") {                    console.log("[MODIFY] Overriding 'premium_feature_unlocked' to 'true'!");                    return "true"; // Force it to return "true"                }                if (key === "user_api_key") {                    console.log("[MODIFY] Supplying a test API key for 'user_api_key'!");                    return "TEST_API_KEY_BY_FRIDA_12345";                }                return originalValue;            };            prefs.getBoolean.implementation = function(key, defValue) {                var originalValue = this.getBoolean(key, defValue);                console.log("[READ] SP: '" + name + "' | Key: '" + key + "' | Original Boolean: " + originalValue + " | Default: " + defValue);                // Example: Manipulate a specific boolean value                if (key === "is_admin_user") {                    console.log("[MODIFY] Forcing 'is_admin_user' to true!");                    return true;                }                return originalValue;            };            prefs.getInt.implementation = function(key, defValue) {                var originalValue = this.getInt(key, defValue);                console.log("[READ] SP: '" + name + "' | Key: '" + key + "' | Original Int: " + originalValue + " | Default: " + defValue);                if (key === "app_version_code") {                    console.log("[MODIFY] Setting 'app_version_code' to 9999 for testing!");                    return 9999;                }                return originalValue;            };            // --- Hook Write Operations (e.g., putString, putBoolean) via Editor ---            Editor.putString.implementation = function(key, value) {                console.log("[WRITE] SP Editor: '" + name + "' | Key: '" + key + "' | New Value: '" + value + "'");                // Example: Intercept and modify a value before it's saved                if (key === "user_preference_setting") {                    console.log("[MODIFY] Altering 'user_preference_setting' to 'frida_controlled_state'!");                    return this.putString(key, "frida_controlled_state");                }                return this.putString(key, value);            };            Editor.putBoolean.implementation = function(key, value) {                console.log("[WRITE] SP Editor: '" + name + "' | Key: '" + key + "' | New Boolean Value: " + value);                if (key === "telemetry_enabled") {                    console.log("[MODIFY] Forcing 'telemetry_enabled' to false, regardless of app's intent!");                    return this.putBoolean(key, false);                }                return this.putBoolean(key, value);            };            // --- Hook Apply/Commit to observe when changes are saved ---            Editor.apply.implementation = function() {                console.log("[SAVING] SharedPreferences Editor ('" + name + "') - Changes applied (asynchronous).");                return this.apply();            };            Editor.commit.implementation = function() {                console.log("[SAVING] SharedPreferences Editor ('" + name + "') - Changes committed (synchronous).");                return this.commit();            };        }        console.log("----------------------------------------------------------------------------------------------------------------");        return prefs;    };});

Save this script as `manipulate_prefs.js`. To execute it, you would again use Frida:

frida -U -f com.target.package -l manipulate_prefs.js --no-pause

Replace com.target.package with the actual package name of the application you are testing. The `–no-pause` flag ensures the application starts immediately after injection.

Execution and Observation

Once Frida is attached and the script is running, interact with the target application. You will observe detailed output in your terminal:

  • Messages indicating when getSharedPreferences is called.
  • Logs of values being read (`[READ]`) and, more importantly, when they are being manipulated (`[MODIFY]`).
  • Logs of values being written (`[WRITE]`) and any modifications applied by our script.
  • Confirmation messages when changes are saved (`[SAVING]`).

For instance, if the app tries to check `premium_feature_unlocked`, our script will force it to return `true`, potentially unlocking features without the app ever knowing the original value. Similarly, any attempt by the app to enable `telemetry_enabled` would be overridden to `false` before it’s saved.

Advanced Considerations

  • Different Data Types: The provided script primarily hooks `getString`, `getBoolean`, `getInt`, `putString`, `putBoolean`, and `putInt`. Remember to extend your hooks to other data types like `getLong`, `getFloat`, `getStringSet`, and their corresponding `put` methods if the application uses them.
  • Preventing Writes: You can entirely prevent a write operation from occurring by simply not calling the original `this.putString(key, value)` or `this.putBoolean(key, value)` within the `Editor` hook. This can be useful for blocking specific configuration changes.
  • Bypassing Integrity Checks: If an application performs integrity checks on SharedPreferences data (e.g., checksums), manipulating values at runtime can bypass these checks, as the check itself might read the manipulated value.
  • Conditional Manipulation: The script uses `if (key === “some_key”)` for conditional manipulation. You can extend this with more complex logic based on current app state, other preference values, or even user input received via Frida’s RPC API.

Conclusion

Frida provides an incredibly powerful and flexible way to interact with Android applications at runtime. By strategically hooking into SharedPreferences access methods, penetration testers can effectively manipulate application settings and data without requiring direct file system access via root. This technique is crucial for understanding an app’s internal logic, bypassing client-side controls, and identifying potential vulnerabilities that might otherwise remain hidden. Mastering dynamic instrumentation with Frida opens up a vast array of possibilities for in-depth mobile application security assessments.

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 →
Google AdSense Inline Placement - Content Footer banner