rustFrida Documentation

repository·master·Indexed 18 days ago

https://github.com/kkkbbb/rustfrida

An ARM64 Android dynamic instrumentation framework that allows developers to hook Java and native functions, monitor JNI registrations, and perform instruction tracing. It features a self-contained high-performance agent, a JavaScript REPL, and an HTTP RPC server for remote function calls. Supports PID injection, spawn mode, and eBPF-based SO watching.

Tokens
68.6K
Snippets
192
Records
274
Agent score
60%

What's inside rustfrida

  1. Understand Java to JS type marshalling rules

    master

    rustFrida automatically converts types between the Java (JNI) and JavaScript runtimes.

    Java → JS (Automatic Conversion)

    • Primitives: boolean $\rightarrow$ boolean, int/short/byte $\rightarrow$ number, long $\rightarrow$ BigInt, float/double $\rightarrow$ number.
    • Strings: java.lang.String $\rightarrow$ string.
    • Arrays: Primitive arrays ([I, [B, etc.) $\rightarrow$ Array of numbers. Object arrays $\rightarrow$ Array of wrappers.
    • Objects: Most objects are returned as wrappers {__jptr, __jclass}. You must use .method() or .field.value to interact with them.
    • Boxed Types: Integer, Long, etc., are NOT automatically unboxed. You must call .intValue(), .doubleValue(), etc., to get the primitive value.

    JS → Java (Automatic Conversion)

    • Primitives: JS number or BigInt are automatically converted to the target JNI type.
    • Strings: JS string is converted to java.lang.String.
    • Arrays: JS Array is converted to the target Java array type. For primitive arrays, rustFrida uses bulk copying for efficiency.
    • Autoboxing: If a target signature is a boxed type (e.g., Ljava/lang/Integer;), JS primitives are automatically autoboxed using Xxx.valueOf().
    • Ambiguity: For overloaded array methods (e.g., foo(byte[]) vs foo(int[])), rustFrida scores the JS input array based on whether the elements fit within the target type's range.
  2. Root Cause of WalkStack 'StackMap not found for 0' Aborts

    master

    The StackMap not found for 0 fatal error in ART (Android Runtime) occurs during stack walking when the runtime encounters an optimized method but cannot find a valid StackMap for the current Program Counter (PC).

    In rustfrida, this is triggered when the WalkStack mechanism attempts to traverse a stack containing custom thunks (hooked methods). If the runtime's StackVisitor::GetDexPc determines the method is optimized but the StackMap is invalid, it calls LOG(FATAL).

    Key contributing factors include:

    • Drain Deadlocks: The drain_thunk_in_flight() mechanism can hang indefinitely if Java threads are blocked (e.g., via Object.wait, Looper, or IO) while holding the JS_ENGINE lock, preventing the in-flight counter from reaching zero.
    • JNI/Monitor Deadlocks: Circular dependencies between JS engine locks and Java monitors.
    • Missing Metadata: Custom thunks do not naturally exist in the OAT file or JIT cache, so ART fails to find the necessary OatQuickMethodHeader and CodeInfo to validate the stack frame.
  3. Rules for using orig() in Managed DSL

    master

    The orig() function calls the original method. Because it is routed through a generated managed __rf_orig method, there are strict structural requirements:

    1. Consistent Return Paths: If your DSL program uses orig(), every possible execution path must end with return orig(); or return orig(...). You cannot mix orig() returns with direct returns (e.g., return null;) in the same program.
    2. Argument Handling:
      • orig(): Calls the original method with the original receiver and arguments.
      • orig(arg0, arg1): Calls the original method with explicit replacement arguments. The argument count must exactly match the hooked method's parameter count (instance receivers are supplied automatically).
    3. Top-level Restriction: The statement let x = orig(...) is restricted. It must be the first top-level statement in your DSL and cannot be nested. It cannot be mixed with return orig(...) patterns.
    4. Instance vs Static: For instance hooks, do not pass this to orig(...); only pass the Java method parameters. For static hooks, pass all static method parameters.
  4. Use HTTP RPC for remote function calls

    master

    You can register methods in your script using the Frida-style rpc.exports object. These methods can then be invoked from a host machine via HTTP POST requests. This is ideal for using the agent as a persistent service for UI, automation, or testing frameworks.

    Key Constraints

    • JSON-safe return values: Return values are processed via JSON.stringify. Functions, circular references, and undefined will be omitted. When returning Java objects, manually convert them to strings or plain objects (e.g., String(obj.method())) to avoid getting only a pointer literal.
    • Synchronous only: The RPC mechanism does not support async or Promise. Promises will be stringified to {}.
    • Concurrency: Requests within the same session are executed serially (queued). Requests across different sessions run in parallel.
    • Timeout: Calls have a 30-second timeout. For long-running tasks, use a polling pattern instead.
    // Registering methods via rpc.exports
    rpc.exports = {
        ping: function() { return "pong"; },
        add: function(a, b) { return a + b; },
        getAppName: function() {
            var ActivityThread = Java.use("android.app.ActivityThread");
            var app = ActivityThread.currentApplication();
            var ctx = app.getApplicationContext();
            var pm = ctx.getPackageManager();
            return {
                packageName: String(ctx.getPackageName()),
                label: String(pm.getApplicationLabel(ctx.getApplicationInfo())),
            };
        }
    };
    
    // Or append individually
    rpc.export('version', function() { return "1.0.0"; });
  5. Read and write memory using Memory or NativePointer

    master

    The project supports two styles of memory access: the Memory.* style and the NativePointer prototype style (recommended for chaining). Both are fully compatible with standard Frida.

    Important Constraints:

    • I-Cache: After writing executable code, you must call Memory.flushCodeCache(addr, size) to ensure the CPU sees the changes.
    • Permissions: writeXxx methods do not automatically call mprotect. If you attempt to write to a read-only segment, it will throw an error. Use Memory.protect(addr, size, "rwx") first.
    • Limits: readCString is limited to 4096 bytes. Memory.alloc is limited to 256MB.
    // Recommended: NativePointer style with chaining
    var p = ptr("0x7f1234");
    p.add(8).readPointer().readCString();
    
    // Memory style
    var pid = Memory.readU32(ptr("0x7f1234"));
    Memory.writeU64(dst, 0xdeadbeefn);
    
    // Writing code and flushing cache
    var code = Memory.alloc(16);
    code.writeU32(0xd65f03c0); // ret
    Memory.flushCodeCache(code, 16);
  6. Use Managed DSL for high-frequency Java hooks

    master

    The Managed DSL is a high-performance alternative to standard impl hooks. It compiles JS/Java-style code into a dex callback that executes on the ART/Java side, avoiding the overhead of entering the QuickJS runtime for every call.

    When to use DSL

    • Use impl: For low-frequency hooks, debugging, or when you need JS features like closures, console.log, or Promises.
    • Use DSL: For high-frequency hot paths where you only need to perform logic like conditional checks, parameter modification, return value changes, or simple counting.

    DSL Constraints

    • Cannot access JS variables, closures, console.log, setInterval, or Promise.
    • send(channel, value) is used for communication. value must be an int or java.lang.String.
    • Communication follows a "hot path writes to buffer, cold path reads in bulk" model. Use method.dslRead(max) in JS to pull messages.
    • The buff size must be a power of 2 (default 4096, max 1048576).

    DSL Built-in Names

    • this: The current object (instance methods only).
    • arg0, arg1, ...: Java method arguments.
    • orig() / orig(a, b, ...): Call the original method.
    • count("name"): Increments a high-speed counter.
    • send("channel", value): Writes a message to the ring buffer.
    Java.ready(function () {
        var HashMap = Java.use("java.util.HashMap");
        var put = HashMap.put.overload("java.lang.Object", "java.lang.Object");
    
        put.dsl({ buff: 4096 }).dslImpl = `
            count("put");
            let n: int = this.size();
            if ((n & 1023) == 0) {
                send("size", n);
            }
            return orig(arg0, arg1);
        `;
    
        // JS side: low-frequency polling
        var drained = HashMap.put.dslRead(64);
        for (var i = 0; i < drained.length; i++) {
            var m = drained[i];
            console.log(m.name, m.value);
        }
    });
  7. Use CModule for high-performance native callbacks

    master

    For high-frequency hot paths, use CModule to compile C code directly in the target process. This moves the logic from the JS engine to a native C callback, significantly reducing overhead.

    Important Lifecycle Note

    CModule objects must be kept in a global variable (e.g., globalThis.my_cm = cm) as long as the hook is active. If the JS object is garbage collected, the compiled code memory may be released, causing crashes.

    Two Modes of C Callbacks:

    1. Replace Mode (hookNative): The original function is not called automatically. You must manually call hook_invoke_trampoline(ctx, ctx->trampoline) inside your C code to execute the original function.
    2. Attach Mode (attachNative): The engine automatically executes the original function. You provide onEnter and/or onLeave callbacks.

    HookContext Structure

    CModule automatically injects stdint.h, stddef.h, stdbool.h, string.h, and rfhook.h. The HookContext provides access to registers (x[31], d[8]), sp, pc, and the trampoline.

    typedef struct {
        uint64_t x[31];
        uint64_t sp;
        uint64_t pc;
        uint64_t nzcv;
        void *trampoline;
        uint64_t d[8];
        uint64_t intercept_leave;
    } HookContext;
    var cm = new CModule(`
        #include <rfhook.h>
    
        void on_getuid(HookContext *ctx, void *user_data) {
            uint64_t real = hook_invoke_trampoline(ctx, ctx->trampoline);
            ctx->x[0] = (real == 0 ? 0 : 20000);
        }
    `);
    
    globalThis.keep_getuid_cmodule = cm; // Prevent GC
    
    var getuid = Module.findExportByName("libc.so", "getuid");
    var trampoline = hookNative(getuid, cm.on_getuid);
  8. Fix 'StackMap not found' using Pseudo OAT Headers

    master

    To prevent ART from aborting during stack walking, rustfrida implements a "Pseudo OAT Header" strategy. This makes custom thunk frames appear as valid, native methods to the ART runtime.

    The Strategy

    1. Fake Header Injection: Prepend a fake OatQuickMethodHeader and CodeInfo to the thunk memory. This ensures ArtMethod::GetOatQuickMethodHeader(pc) returns a valid header that Contains(pc) validates.
    2. Native Method Masking: To bypass the StackMap check entirely, the thunk's "found path" must overwrite the Stack Pointer (SP+0) with a replacement (native) value before calling art_router_stack_check. When WalkStack reads the frame, IsNative() returns true, causing ToDexPc to early-exit without checking for a StackMap.

    Thunk Memory Layout

    The memory is structured to satisfy ART's expectations:

    • thunk_mem[0..8]: Fake CodeInfo (8 bytes).
    • thunk_mem[8..12]: Fake OatQuickMethodHeader (4 bytes).
    • thunk_mem[12..]: The actual thunk body (router prologue, scan, and paths).
  9. Start the HTTP RPC server

    master

    To enable the HTTP RPC server, use the --rpc-port flag. This works in both legacy single-session mode and --server multi-session mode.

    • Legacy mode: Attach to a PID and load a script. The session ID is fixed at 0.
    • Server mode: Multiple sessions share the same RPC port, and requests are routed by session ID.

    Use adb forward to access the server from your local machine if running on an Android device.

    # Legacy mode: attach + load script + open RPC port
    ./rustfrida --pid 1234 -l rpc_test.js --rpc-port 9191
    
    # Server mode: multi-session sharing one RPC port
    ./rustfrida --server --rpc-port 127.0.0.1:9191
    
    # Forward port for local access
    adb forward tcp:9191 tcp:9191
  10. Build and Deploy rustfrida for Android 16 (API 36)

    master

    To test the pseudo-header implementation and verify that HashMap.put or Throwable.fillInStackTrace no longer triggers StackMap not found aborts, follow these steps:

    1. Build the components:
      cargo build -p agent --release
      cargo build -p rust_frida --release
    2. Push to device:
      adb -s <device_serial> push target/aarch64-linux-android/release/rustfrida /data/local/tmp/
    3. Run the tool:
      ./rustfrida --name com.example -l test_hashmap_put_hook.js
    # Build
    cargo build -p agent --release
    cargo build -p rust_frida --release
    
    # Deploy
    # Replace <device_serial> with your actual ADB serial
    adb -s <device_serial> push target/aarch64-linux-android/release/rustfrida /data/local/tmp/
    
    # Execute
    ./rustfrida --name com.example -l test_hashmap_put_hook.js
  11. Deploy and Run rustFrida

    master

    After building, push the binary to your Android device and use the following commands to interact with processes.

    Deployment

    adb push target/aarch64-linux-android/release/rustfrida /data/local/tmp/

    Execution Modes

    PID Injection (Attach to running process):

    ./rustfrida --pid <pid>
    ./rustfrida --pid <pid> -l script.js

    Spawn Mode (Inject at startup):

    ./rustfrida --spawn com.example.app
    ./rustfrida --spawn com.example.app -l script.js

    eBPF Mode (Wait for specific SO loading):

    ./rustfrida --watch-so libnative.so

    Useful Flags

    • --verbose: Enable detailed logging.
    • -o <path>: Synchronize output logs to a file (e.g., ./rustfrida --pid <pid> -l script.js -o /data/local/tmp/rustfrida.log).
    • --rpc-port <port>: Enable HTTP RPC server.
    adb push target/aarch64-linux-android/release/rustfrida /data/local/tmp/
    ./rustfrida --pid <pid> -l script.js
    ./rustfrida --spawn com.example.app -l script.js
    ./rustfrida --watch-so libnative.so
  12. Use Java.managedHookDsl for high-frequency Java hooks

    master

    Java.managedHookDsl compiles hook logic into a generated DEX helper and routes the target method through a managed direct thunk. Use this instead of standard JS callbacks for high-frequency Java method hooks where JS overhead is too expensive or unstable under heavy application traffic.

    To use it, you must first prepare the method using Java.compileMethod and reset ART route statistics using Java._resetArtRouteStats() to ensure the managed path is correctly utilized.

    Java.ready(function () {
      Java.compileMethod(
        "java.util.HashMap",
        "put",
        "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
        "auto"
      );
    
      Java._resetArtRouteStats();
    
      Java.managedHookDsl({
        className: "java.util.HashMap",
        methodName: "put",
        signature: "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
        dsl: "// Your DSL logic here"
      });
    });