Android App Penetration Testing & Frida Hooks

Deep Dive: Reconstructing Android App Runtime Data from Raw Frida Memory Dumps

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: Unveiling Hidden Runtime Secrets

Android application penetration testing often hits a wall when critical data resides only in memory, dynamically generated, or heavily obfuscated. While static analysis provides insights into an app’s structure, runtime memory dumping with tools like Frida offers an unparalleled vantage point into the live state of an application. This expert-level guide delves into the intricate process of extracting raw memory dumps using Frida and subsequently reconstructing meaningful data, from session tokens to encryption keys, that would otherwise remain hidden.

Understanding an app’s runtime memory can expose sensitive information that is never written to disk, or is processed in a transient, unencrypted form before being secured for storage or transmission. Our focus will be on practical techniques to identify target memory regions, perform efficient dumps, and then leverage post-processing to piece together complex data structures.

Prerequisites for Memory Forensics

Before we dive into the technical details, ensure you have the following tools and knowledge:

  • Rooted Android Device or Emulator: Essential for running Frida’s `frida-server`.
  • Frida-tools: Installed on your host machine (`pip install frida-tools`).
  • ADB (Android Debug Bridge): For device communication.
  • Python: For scripting Frida interactions and post-processing.
  • Basic JavaScript Knowledge: For writing Frida scripts.
  • Hex Editor / Disassembler (Optional but Recommended): Tools like HxD, 010 Editor, Ghidra, or IDA Pro can assist in understanding memory layouts and data structures.

Identifying Target Memory Regions with Frida

The first crucial step is to pinpoint *where* the data of interest resides in the app’s memory space. This often involves a combination of dynamic and static analysis.

Dynamic Analysis: Exploring Live Objects

Frida’s JavaScript API allows us to interact directly with the Java Virtual Machine (JVM) and native libraries. We can enumerate instances of specific classes, hook methods to observe object states, or scan for byte patterns.

Consider an app that stores an authentication token in a custom Java object. We can hook the object’s constructor or a getter method to get a reference to it.

Java.perform(function () {  var TargetClass = Java.use("com.example.myapp.AuthTokenManager");  TargetClass.$init.implementation = function () {    // Call the original constructor    this.$init();    console.log("AuthTokenManager instance created!");    // Get a reference to the instance's memory address    var instancePtr = this.handle;    console.log("Instance pointer: " + instancePtr);    // Let's assume the token is a String field at a known offset    // For demonstration, we'll dump the entire object's memory region    var memorySize = 0x100; // Example size, needs to be determined via RE    var dump = Memory.readByteArray(instancePtr, memorySize);    send({"type": "memory_dump", "address": instancePtr.toString(), "size": memorySize}, dump);  };});

Static Analysis: Understanding Memory Layouts

For more complex data structures, especially those in native libraries (C/C++), a disassembler is invaluable. Ghidra or IDA Pro can help reverse engineer native functions to understand how structs are defined, what fields they contain, and their respective offsets. This knowledge is critical for correctly parsing raw byte arrays.

Performing Automated Memory Dumps

Once you have a Frida script sending memory dumps, you’ll need a Python script to interact with Frida and save the received data to disk.

import fridaimport sysimport osdef on_message(message, data):    if message["type"] == "send":        payload = message["payload"]        if payload["type"] == "memory_dump":            address = payload["address"]            size = payload["size"]            filename = f"dump_{address}_{size}.bin"            with open(filename, "wb") as f:                f.write(data)            print(f"[*] Dumped {size} bytes from {address} to {filename}")    else:        print(message)def attach_and_dump(process_name_or_pid):    try:        session = frida.get_usb_device().attach(process_name_or_pid)    except frida.ServerNotRunningError:        print("Frida server not running. Start it on the device: 'frida-server'")        sys.exit(1)    except frida.NotSupportedError:        print("Device not found or not supported. Ensure adb is running and device is connected.")        sys.exit(1)    script_path = "dump_script.js" # Your Frida JS script    with open(script_path, "r") as f:        script_code = f.read()    script = session.create_script(script_code)    script.on("message", on_message)    script.load()    print(f"[*] Attached to {process_name_or_pid}. Press Ctrl+C to detach.")    sys.stdin.read() # Keep the script running until interrupted    session.detach()if __name__ == "__main__":    if len(sys.argv) != 2:        print("Usage: python dump_manager.py <process_name_or_pid>")        sys.exit(1)    attach_and_dump(sys.argv[1])

To run this:

  1. Save the JavaScript code as `dump_script.js`.
  2. Save the Python code as `dump_manager.py`.
  3. Ensure `frida-server` is running on your Android device/emulator.
  4. Execute: `python dump_manager.py com.example.myapp` (replace with target app package name or PID).

Post-Processing: Reconstructing Data from Raw Dumps

This is where the real challenge and art of memory forensics lies. Raw byte arrays are meaningless without context. You need to know the structure of the data you’ve dumped.

Example: Reconstructing a Simple Java Object

Let’s assume we dumped a custom Java object like this:

public class UserSession {    private String token;    private int userId;    private long expiryTime;}

When a Java object is in memory, it has an object header, followed by its fields. The exact layout (offsets, sizes) depends on the JVM implementation (ART for Android) and architecture. For simplicity, let’s assume `String` is stored as a pointer (4 or 8 bytes depending on architecture) and `int`/`long` are their standard sizes. We’ll simulate a 64-bit ART environment where pointers are 8 bytes.

A `String` in Java is itself an object, containing a pointer to its underlying `char[]` array and other metadata (like length). When you dump the `UserSession` object, you’ll see a pointer for `token`. You’d need to follow that pointer and dump the `String` object, and then its `char[]` data.

For our demonstration, let’s assume we’re dumping a very simple custom native struct directly or a byte array that holds known data types.

import structdef parse_user_data_dump(filepath):    try:        with open(filepath, "rb") as f:            raw_data = f.read()    except FileNotFoundError:        print(f"Error: File not found at {filepath}")        return    # Assuming a simple native struct:    # struct UserData {    #   char username[32]; // 32 bytes    #   uint32_t id;      // 4 bytes    #   float score;      // 4 bytes    # };    # Total size = 32 + 4 + 4 = 40 bytes    # Define the unpack format string:    # '<' for little-endian, '32s' for 32-byte string, 'I' for unsigned int, 'f' for float    if len(raw_data) < 40:        print(f"Error: Dumped data too small ({len(raw_data)} bytes) for expected struct (40 bytes).")        return    unpacked_data = struct.unpack("<32sIf", raw_data[:40])    username_bytes = unpacked_data[0]    user_id = unpacked_data[1]    score = unpacked_data[2]    # Null-terminate and decode username    username = username_bytes.split(b'x00', 1)[0].decode('utf-8')    print(f"Reconstructed User Data:")    print(f"  Username: {username}")    print(f"  User ID: {user_id}")    print(f"  Score: {score}")# Example usage:parse_user_data_dump("dump_0x7f8d42a000_200.bin") # Replace with your actual dump filename

In a real-world Java object reconstruction, you’d be parsing Java object headers, field offsets (which can be derived by inspecting the ART runtime or by dynamic analysis), and then recursively parsing referenced objects (like `String` objects pointing to `char[]`). This often requires a more sophisticated parser that understands ART’s internal memory representation.

Reconstructing Complex Structures

For more complex scenarios, such as reconstructing a `List` or `Map` of objects, the process involves:

  1. Dumping the container object: Get the raw bytes of the `List` or `Map` instance.
  2. Identifying internal pointers: The container will hold pointers to its elements. These pointers need to be followed.
  3. Recursively dumping elements: For each element pointer, perform another memory dump of that element’s memory region.
  4. Parsing each element: Apply the same reconstruction logic as above to each individual element.

This iterative process often leads to a

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