marginaldeer

char nick[5] = "marg";

char full_nick[13] = "marginaldeer";

printf("https://github.com/%s\n",full_nick);

printf("%s\x40marginaldeer\x2ecom\n",nick);

puts("63DD 76E6 3428 1285 CD8B E3F5 7509 1985 DF70 E945");

Frida Method Hooking for Android App Analysis

Nov 15, 2025 • android,frida,reverse-engineering,pentest,mobile

Series: Android Pentesting (Part 3 of 4)
  1. Android Pentest Lab Build
  2. Android SSL Pinning Bypass with Burp Suite
  3. Frida Method Hooking for Android App Analysis
  4. Hooking Native Libraries with Frida Interceptor

This is part three of my Android pentest series. We’ve covered setting up the lab and bypassing SSL pinning. Now let’s go deeper with Frida and start hooking methods, tracing function calls, and pulling secrets out of running apps.

I’ve relied on Frida heavily on recent engagements and it’s become one of my favorite tools. It lets you inject JavaScript into running processes which means we can:

  • Intercept method calls and log arguments
  • Modify arguments before they reach the original method
  • Steal return values (encryption keys, tokens, etc.)
  • Change return values to bypass security controls
  • Trace execution flow through the app

All at runtime without touching the APK.

Getting Started

Make sure you have Frida installed (pip install frida-tools) and the Frida server running on your emulator or rooted device. I covered the setup in my SSL pinning post so check that out if you haven’t already.

Verify everything is working:

frida-ps -U

You should see a list of processes on the device.

Finding What to Hook

Before we can hook anything we need to know what’s in the app. I usually start by listing all the loaded classes and filtering for the app’s package name.

Save this as list-classes.js:

Java.perform(function() {
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            if (className.includes("com.target")) {
                console.log(className);
            }
        },
        onComplete: function() {
            console.log("[*] Class enumeration complete");
        }
    });
});

Run it with:

frida -U -f com.target.app -l list-classes.js --no-pause

Once you find an interesting class you can list all its methods:

Java.perform(function() {
    var targetClass = Java.use("com.target.app.crypto.AESHelper");
    var methods = targetClass.class.getDeclaredMethods();
    
    methods.forEach(function(method) {
        console.log(method.toString());
    });
});

This gives you a roadmap of what the app is doing under the hood.

Basic Method Hooking

Let’s say we found a login method and want to see what credentials are being passed. Here’s how to hook it and log the arguments:

Java.perform(function() {
    var LoginActivity = Java.use("com.target.app.LoginActivity");
    
    LoginActivity.authenticate.implementation = function(username, password) {
        console.log("[+] authenticate() called");
        console.log("    Username: " + username);
        console.log("    Password: " + password);
        
        return this.authenticate(username, password);
    };
});

This intercepts the method, logs whatever gets passed to it, then lets the original method run. Simple but incredibly useful.

Dealing with Overloaded Methods

Java methods can have the same name but different parameters. If you try to hook one and get an error about ambiguous overloads, you need to specify which version you want:

Java.perform(function() {
    var Crypto = Java.use("com.target.app.utils.Crypto");
    
    Crypto.encrypt.overload('java.lang.String').implementation = function(data) {
        console.log("[+] encrypt(String) called with: " + data);
        var result = this.encrypt(data);
        console.log("[+] Returns: " + result);
        return result;
    };
    
    Crypto.encrypt.overload('[B').implementation = function(data) {
        console.log("[+] encrypt(byte[]) called");
        console.log("[+] Data: " + bytesToHex(data));
        return this.encrypt(data);
    };
});

function bytesToHex(bytes) {
    var hex = [];
    for (var i = 0; i < bytes.length; i++) {
        hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
    }
    return hex.join('');
}

The [B notation means byte array. It’s weird Java internal syntax but you get used to it.

Stealing Encryption Keys

This is where things get fun. A lot of apps use hardcoded keys or generate them at runtime. Either way we can grab them by hooking the crypto classes.

Java.perform(function() {
    var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
    
    SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(key, algorithm) {
        console.log("[+] SecretKeySpec created");
        console.log("    Algorithm: " + algorithm);
        console.log("    Key (hex): " + bytesToHex(key));
        
        return this.$init(key, algorithm);
    };
});

I’ve pulled AES keys, API secrets, and all sorts of goodies with this technique. You can also hook Cipher.doFinal() to see the actual encryption and decryption happening in real time.

Bypassing Root Detection

A lot of apps refuse to run on rooted devices. Rather than mess with hiding root, I just hook the detection methods and make them return false:

Java.perform(function() {
    var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
    
    RootBeer.isRooted.implementation = function() {
        console.log("[+] isRooted() called, returning false");
        return false;
    };
    
    var File = Java.use("java.io.File");
    File.exists.implementation = function() {
        var path = this.getAbsolutePath();
        if (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1) {
            console.log("[+] Hiding root path: " + path);
            return false;
        }
        return this.exists();
    };
});

RootBeer is a common detection library so this covers a lot of apps. For custom detection you’ll need to reverse engineer the app to find their specific checks.

Changing Return Values

Need to bypass a license check or make the app think you’re a premium user? Just change what the method returns:

Java.perform(function() {
    var LicenseCheck = Java.use("com.target.app.LicenseValidator");
    
    LicenseCheck.isLicensed.implementation = function() {
        console.log("[+] isLicensed() bypassed!");
        return true;
    };
    
    LicenseCheck.getDaysRemaining.implementation = function() {
        console.log("[+] getDaysRemaining() spoofed!");
        return 9999;
    };
});

I’ve used this on engagements to bypass trial restrictions and test what functionality is available in the full version.

Dumping SharedPreferences

Apps love to store things in SharedPreferences, sometimes including things they shouldn’t. Hook it and see everything the app reads:

Java.perform(function() {
    var SharedPreferences = Java.use("android.app.SharedPreferencesImpl");
    
    SharedPreferences.getString.implementation = function(key, defValue) {
        var value = this.getString(key, defValue);
        console.log("[SharedPrefs] getString(" + key + ") = " + value);
        return value;
    };
});

I’ve found API tokens, session IDs, and even passwords stored in plain text this way.

A Recon Script to Get You Started

Here’s a script I use for initial app analysis. It hooks crypto, network calls, and logging all at once:

Java.perform(function() {
    console.log("[*] Starting recon...\n");
    
    try {
        var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
        SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(key, algo) {
            console.log("[CRYPTO] Key created - Algorithm: " + algo);
            console.log("[CRYPTO] Key bytes: " + bytesToHex(key));
            return this.$init(key, algo);
        };
    } catch(e) {}
    
    try {
        var URL = Java.use("java.net.URL");
        URL.$init.overload('java.lang.String').implementation = function(url) {
            console.log("[NET] URL: " + url);
            return this.$init(url);
        };
    } catch(e) {}
    
    try {
        var Log = Java.use("android.util.Log");
        Log.d.overload('java.lang.String', 'java.lang.String').implementation = function(tag, msg) {
            console.log("[LOG] " + tag + ": " + msg);
            return this.d(tag, msg);
        };
    } catch(e) {}
    
    console.log("[*] Hooks installed. Use the app...\n");
});

function bytesToHex(bytes) {
    var hex = [];
    for (var i = 0; i < bytes.length; i++) {
        hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
    }
    return hex.join('');
}

Run it, then interact with the app and watch the secrets flow by.

Tips I’ve Learned

A few things that have saved me time:

  • Start with static analysis — Use jadx or apktool to decompile the APK first. Find interesting classes before hooking blindly.
  • Wrap hooks in try/catch — If one hook fails it won’t crash your whole script.
  • Watch for obfuscation — Proguard renames classes to a.b.c. Look for string references to identify what’s what.
  • Hook constructors with $init — Useful for seeing when objects get created and with what parameters.
  • Use Objection for quick winsobjection -g com.app explore is faster for initial recon.

What’s Next

With Frida in your toolkit you can reverse engineer pretty much any Android app. Some things I use it for regularly:

  • Reverse engineering proprietary API protocols
  • Extracting hardcoded secrets and API keys
  • Bypassing authentication and licensing checks
  • Understanding obfuscated code through runtime analysis
  • Dumping decrypted data before it hits the network

In a future post I’ll cover hooking native libraries with Frida’s Interceptor API. That’s where things get really interesting.

More to come!

If you enjoyed this post please consider subscribing to the feed!