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:
- 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).
- Access External Storage: Use `adb shell ls -la /sdcard/` to see what files are present and their content (`adb shell cat /sdcard/sensitive.txt`).
- 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 →