Android App Penetration Testing & Frida Hooks

Building a Frida Toolkit: Custom Scripts for Efficient Android Insecure Data Storage Assessment

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Peril of Insecure Data Storage

In the realm of Android application penetration testing, identifying and exploiting insecure data storage vulnerabilities remains a critical aspect. Applications often store sensitive user data, authentication tokens, or configuration settings directly on the device’s file system without proper protection. If this data is stored in world-readable locations or is accessible to other applications via insecure permissions, it can lead to severe data breaches. Frida, a dynamic instrumentation toolkit, provides an unparalleled capability to observe and manipulate an application’s runtime behavior, making it an invaluable tool for uncovering these issues.

This article will guide you through building a custom Frida toolkit focused on efficiently identifying insecure data storage practices within Android applications. We’ll explore hooking common Android APIs related to file I/O, SharedPreferences, and SQLite databases to detect when and where sensitive information might be at risk.

Setting Up Your Frida Environment

Before diving into script development, ensure your Frida environment is correctly set up. You’ll need:

  • A rooted Android device or an emulator.
  • Frida server running on the Android device.
  • Frida client (Python `frida-tools`) on your host machine.

Frida Server Installation (on device)

Download the appropriate Frida server for your device’s architecture (e.g., frida-server-16.1.4-android-arm64) from the Frida releases page. Push it to the device, set execute permissions, and run it:

adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"

Frida Client Installation (on host)

Install the Frida client using pip:

pip install frida-tools

Verify connectivity:

frida-ps -U

Understanding Android Data Storage Mechanisms

Android provides several ways for applications to store data. Insecure storage often stems from misconfigurations or improper use of these mechanisms:

  • SharedPreferences: Key-value pairs stored in XML files, usually in /data/data/<package_name>/shared_prefs/.
  • Internal Storage: Private files stored in /data/data/<package_name>/files/ or /data/data/<package_name>/cache/. By default, these are private to the app.
  • External Storage: Publicly accessible storage (e.g., SD card or shared internal storage) often located in /sdcard/ or mounted at /storage/emulated/0/. Data here is world-readable/writable.
  • SQLite Databases: Structured data storage in /data/data/<package_name>/databases/.

The primary goal is to detect when sensitive data lands in easily accessible locations like external storage or in `SharedPreferences` files with world-readable permissions.

Developing Custom Frida Scripts for Data Storage Assessment

Our toolkit will focus on hooking relevant API calls to log data being written and the paths involved.

1. Hooking SharedPreferences Operations

SharedPreferences are a common vector for insecure storage. We’ll hook methods that write to and read from them.

Java.perform(function() {    console.log("[+] Hooking SharedPreferences");    var SharedPreferences = Java.use("android.content.SharedPreferences");    var Editor = Java.use("android.content.SharedPreferences$Editor");    // Hook SharedPreferences.Editor.putString    Editor.putString.overload('java.lang.String', 'java.lang.String').implementation = function(key, value) {        console.log("[*] SharedPreferences.Editor.putString: Key="" + key + "", Value="" + value + """);        return this.putString(key, value);    };    // Hook SharedPreferences.Editor.putInt, etc. (add as needed)    Editor.putInt.overload('java.lang.String', 'int').implementation = function(key, value) {        console.log("[*] SharedPreferences.Editor.putInt: Key="" + key + "", Value=" + value);        return this.putInt(key, value);    };    // Hook SharedPreferences.getString    SharedPreferences.getString.overload('java.lang.String', 'java.lang.String').implementation = function(key, defValue) {        var result = this.getString(key, defValue);        console.log("[*] SharedPreferences.getString: Key="" + key + "", Retrieved Value="" + result + """);        return result;    };    // You might also want to hook Context.getSharedPreferences to identify the file name});

This script will print any key-value pairs being stored or retrieved via `SharedPreferences`, allowing you to inspect sensitive data. Later, you can manually check the `shared_prefs` directory for permission issues.

2. Hooking File I/O Operations

For more general file operations, we’ll target `FileOutputStream` and `FileInputStream` to observe data written to and read from files.

Java.perform(function() {    console.log("[+] Hooking File I/O");    var FileOutputStream = Java.use("java.io.FileOutputStream");    var FileInputStream = Java.use("java.io.FileInputStream");    var File = Java.use("java.io.File");    // Hook FileOutputStream constructor to get the file path    FileOutputStream.$init.overload('java.io.File').implementation = function(file) {        var path = file.getAbsolutePath();        console.log("[+] FileOutputStream created for: " + path);        if (path.includes("/sdcard/") || path.includes("/storage/emulated/")) {            console.warn("[!!!] Potential Insecure Storage: Data written to external storage at " + path);        }        return this.$init(file);    };    FileOutputStream.$init.overload('java.lang.String').implementation = function(path) {        console.log("[+] FileOutputStream created for (string path): " + path);        if (path.includes("/sdcard/") || path.includes("/storage/emulated/")) {            console.warn("[!!!] Potential Insecure Storage: Data written to external storage at " + path);        }        return this.$init(path);    };    // Hook FileOutputStream.write to log data (for smaller writes)    FileOutputStream.write.overload('[B').implementation = function(b) {        var data = Java.array('byte', b);        var stringData = String.fromCharCode.apply(null, data); // Attempt to convert to string        console.log("[+] FileOutputStream.write data to " + this.fd.value.getAbsolutePath() + ": " + stringData.substring(0, 100) + "..."); // Log first 100 chars        return this.write(b);    };    // Hook FileInputStream to detect reads    FileInputStream.$init.overload('java.io.File').implementation = function(file) {        console.log("[+] FileInputStream opened for: " + file.getAbsolutePath());        return this.$init(file);    };});

This script will alert you if an application attempts to write data to external storage paths and logs the data itself. You can extend `FileOutputStream.write` to handle other overloads like `write(byte[], int, int)`.

3. Hooking SQLite Database Operations

SQLite databases often hold structured sensitive data. We’ll hook methods that execute SQL or insert/update data.

Java.perform(function() {    console.log("[+] Hooking SQLite Database Operations");    var SQLiteDatabase = Java.use("android.database.sqlite.SQLiteDatabase");    // Hook execSQL    SQLiteDatabase.execSQL.overload('java.lang.String').implementation = function(sql) {        console.log("[+] SQLiteDatabase.execSQL: " + sql);        return this.execSQL(sql);    };    SQLiteDatabase.execSQL.overload('java.lang.String', '[Ljava.lang.Object;').implementation = function(sql, bindArgs) {        console.log("[+] SQLiteDatabase.execSQL with args: " + sql + " Args: " + JSON.stringify(bindArgs));        return this.execSQL(sql, bindArgs);    };    // Hook insert    SQLiteDatabase.insert.overload('java.lang.String', 'java.lang.String', 'android.content.ContentValues').implementation = function(table, nullColumnHack, values) {        var dbPath = this.getPath();        console.log("[+] SQLiteDatabase.insert to table '" + table + "' in DB: " + dbPath);        console.log("    Values: " + values.toString()); // ContentValues doesn't have a direct data dump, need to iterate        var result = this.insert(table, nullColumnHack, values);        return result;    };    // Hook query    SQLiteDatabase.query.overload('java.lang.String', '[Ljava.lang.String;', 'java.lang.String', '[Ljava.lang.String;', 'java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(table, columns, selection, selectionArgs, groupBy, having, orderBy) {        var dbPath = this.getPath();        console.log("[+] SQLiteDatabase.query from table '" + table + "' in DB: " + dbPath);        console.log("    Selection: " + selection + ", Args: " + JSON.stringify(selectionArgs));        var cursor = this.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);        return cursor;    };});

This script logs SQL queries and insertion values, helping you understand what data is being written to and read from the application’s databases. The `ContentValues.toString()` might not give full data; for deeper inspection, you might need to hook `ContentValues.get()` methods or use an `attach` method if `values` is an instance of a custom class.

Combining the Toolkit and Running the Scripts

To use these scripts effectively, you can combine them into a single Frida JavaScript file (e.g., `insecure_storage_hooks.js`).

// insecure_storage_hooks.js
Java.perform(function() {
    // SharedPreferences Hooks
    console.log("[+] Initializing SharedPreferences Hooks");
    var SharedPreferences = Java.use("android.content.SharedPreferences");
    var Editor = Java.use("android.content.SharedPreferences$Editor");
    Editor.putString.overload('java.lang.String', 'java.lang.String').implementation = function(key, value) {
        console.log("[SHP] putString: Key="" + key + "", Value="" + value + """);
        return this.putString(key, value);
    };
    SharedPreferences.getString.overload('java.lang.String', 'java.lang.String').implementation = function(key, defValue) {
        var result = this.getString(key, defValue);
        console.log("[SHP] getString: Key="" + key + "", Retrieved Value="" + result + """);
        return result;
    };

    // File I/O Hooks
    console.log("[+] Initializing File I/O Hooks");
    var FileOutputStream = Java.use("java.io.FileOutputStream");
    var File = Java.use("java.io.File");
    FileOutputStream.$init.overload('java.io.File').implementation = function(file) {
        var path = file.getAbsolutePath();
        console.log("[FILE] FileOutputStream created: " + path);
        if (path.includes("/sdcard/") || path.includes("/storage/emulated/")) {
            console.warn("[!!!] Insecure Storage: External storage write at " + path);
        }
        return this.$init(file);
    };
    FileOutputStream.write.overload('[B').implementation = function(b) {
        var data = Java.array('byte', b);
        var stringData = String.fromCharCode.apply(null, data); // Attempt to convert to string
        console.log("[FILE] write data: " + stringData.substring(0, 100) + "...");
        return this.write(b);
    };

    // SQLite Database Hooks
    console.log("[+] Initializing SQLite Database Hooks");
    var SQLiteDatabase = Java.use("android.database.sqlite.SQLiteDatabase");
    SQLiteDatabase.execSQL.overload('java.lang.String').implementation = function(sql) {
        console.log("[SQL] execSQL: " + sql);
        return this.execSQL(sql);
    };
    SQLiteDatabase.insert.overload('java.lang.String', 'java.lang.String', 'android.content.ContentValues').implementation = function(table, nullColumnHack, values) {
        var dbPath = this.getPath();
        console.log("[SQL] insert to table '" + table + "' in DB: " + dbPath + ", Values: " + values.toString());
        return this.insert(table, nullColumnHack, values);
    };
});

To run this script against a target application (e.g., `com.example.insecureapp`):

frida -U -l insecure_storage_hooks.js -f com.example.insecureapp --no-pause

This command will inject your script into the target application and keep it running. As you interact with the application, Frida will print any detected storage operations to your console. Look for sensitive data in the logs, especially if it’s being written to external storage paths.

Manual Verification and Post-Exploitation

Once Frida identifies suspicious storage activities, manual verification is crucial:

  1. Check File Permissions: Use `adb shell ls -la /data/data/<package_name>/shared_prefs/` or other relevant directories to inspect permissions. Look for `-rw-rw-rw-` (world-readable/writable).
  2. Access External Storage: Use `adb shell ls -la /sdcard/` to see what files are present and their content (`adb shell cat /sdcard/sensitive.txt`).
  3. Database Inspection: Pull the database file (`adb pull /data/data/<package_name>/databases/app.db .`) and use a SQLite browser to inspect its contents.

Conclusion

Building a custom Frida toolkit for insecure data storage assessment significantly enhances your ability to identify vulnerabilities in Android applications. By dynamically hooking critical API calls, you gain real-time insights into how and where an application handles its data. This proactive approach, combined with manual verification of file permissions and content, forms a robust methodology for uncovering and remediating one of the most common and impactful mobile security flaws. Continuously refine your scripts to target specific application behaviors and integrate them into your automated testing workflows for maximum efficiency.

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