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");

Hooking Native Libraries with Frida Interceptor

Nov 25, 2025 • android,frida,reverse-engineering,native,pentest

Series: Android Pentesting (Part 4 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 four of my Android pentest series. We’ve covered the lab setup, SSL pinning bypass, and Java method hooking. Now let’s go deeper and hook native code.

A lot of apps implement security-sensitive logic in native libraries (.so files) to make reverse engineering harder. Crypto routines, license checks, anti-tampering — developers put this stuff in C/C++ thinking it’s safe. With Frida’s Interceptor API we can hook these functions just like we did with Java methods.

Finding Native Libraries

First we need to know what native libraries the app loads. This script lists them:

Java.perform(function() {
    var Runtime = Java.use("java.lang.Runtime");
    var loadLibrary = Runtime.loadLibrary0.overload('java.lang.Class', 'java.lang.String');
    
    loadLibrary.implementation = function(cls, lib) {
        console.log("[+] Loading library: " + lib);
        return this.loadLibrary0(cls, lib);
    };
});

Run this, use the app, and watch what gets loaded. You’ll see names like libnative-lib.so, libcrypto.so, or something app-specific.

You can also list already-loaded modules:

Process.enumerateModules({
    onMatch: function(module) {
        if (module.path.includes("/data/")) {
            console.log(module.name + " @ " + module.base);
        }
    },
    onComplete: function() {}
});

Finding Functions to Hook

Once you know the library name, you need to find the functions inside it. If you have the APK, extract the .so file and throw it into Ghidra or IDA. Look for:

  • Exported functions (easy to hook)
  • Functions with interesting names like verify, decrypt, check
  • Cross-references from Java native method declarations

You can also enumerate exports directly with Frida:

var module = Process.getModuleByName("libnative-lib.so");

module.enumerateExports().forEach(function(exp) {
    console.log(exp.type + " " + exp.name + " @ " + exp.address);
});

This gives you function names and addresses. Now we can hook them.

Basic Native Hooking

The Interceptor API lets us hook any native function by address. Here’s the basic pattern:

var module = Process.getModuleByName("libnative-lib.so");
var targetFunc = module.getExportByName("Java_com_target_NativeLib_verify");

Interceptor.attach(targetFunc, {
    onEnter: function(args) {
        console.log("[+] verify() called");
        console.log("    arg0: " + args[0]);
        console.log("    arg1: " + args[1]);
    },
    onLeave: function(retval) {
        console.log("    returns: " + retval);
    }
});

For JNI functions the first two arguments are always JNIEnv* and either jobject (instance method) or jclass (static method). The actual parameters start at args[2].

Reading String Arguments

Native functions often take C strings or Java strings. Here’s how to read them:

Interceptor.attach(targetFunc, {
    onEnter: function(args) {
        // For C strings (char*)
        var cString = args[2].readCString();
        console.log("C string: " + cString);
        
        // For Java strings (jstring) - need JNI env
        var env = Java.vm.getEnv();
        var jString = env.getStringUtfChars(args[2], null).readCString();
        console.log("Java string: " + jString);
    }
});

I’ve used this to pull API keys and tokens right out of native crypto functions.

Hooking Non-Exported Functions

Sometimes the interesting functions aren’t exported. You’ll need to find them by offset from static analysis. If Ghidra tells you a function is at offset 0x1234 from the library base:

var module = Process.getModuleByName("libnative-lib.so");
var funcAddr = module.base.add(0x1234);

Interceptor.attach(funcAddr, {
    onEnter: function(args) {
        console.log("[+] Hidden function called!");
    }
});

Just make sure you’re using the right offset for the architecture (ARM vs ARM64).

Modifying Return Values

Want to bypass a native check? Just change the return value:

var checkLicense = module.getExportByName("checkLicense");

Interceptor.attach(checkLicense, {
    onLeave: function(retval) {
        console.log("[+] Original return: " + retval);
        retval.replace(1);  // Force success
        console.log("[+] Modified to: 1");
    }
});

I’ve bypassed root detection, integrity checks, and license validation this way. If it returns a boolean or int, you can usually just flip it.

Dumping Encryption Keys

This is where native hooking really shines. A lot of apps do crypto in native code thinking it’s hidden. Hook the OpenSSL or custom crypto functions and grab everything:

// Hook OpenSSL's AES_set_encrypt_key
var aesSetKey = Module.findExportByName("libcrypto.so", "AES_set_encrypt_key");

if (aesSetKey) {
    Interceptor.attach(aesSetKey, {
        onEnter: function(args) {
            var keyLen = args[1].toInt32();
            console.log("[+] AES_set_encrypt_key called");
            console.log("    Key length: " + keyLen + " bits");
            console.log("    Key: " + hexdump(args[0], { length: keyLen / 8 }));
        }
    });
}

For custom crypto you’ll need to reverse engineer the library first to find the right functions, but the hooking part is the same.

Tracing Function Calls

Sometimes you want to see the call flow through native code. Frida’s Stalker API can trace every instruction but it’s heavy. For a lighter approach, hook multiple functions:

var funcsToTrace = [
    "decrypt",
    "verify",
    "processData",
    "sendRequest"
];

funcsToTrace.forEach(function(name) {
    var addr = module.getExportByName(name);
    if (addr) {
        Interceptor.attach(addr, {
            onEnter: function(args) {
                console.log("[TRACE] " + name + "() called");
            }
        });
    }
});

This gives you a high-level view of what’s happening without the overhead of full tracing.

Bypassing Anti-Frida Checks

Some apps try to detect Frida by looking for:

  • Frida’s default port (27042)
  • Frida libraries in memory
  • Frida’s named pipes
  • Specific strings in /proc/maps

You can hook the detection functions once you find them:

// Hook strstr to hide frida strings
var strstr = Module.findExportByName(null, "strstr");

Interceptor.attach(strstr, {
    onEnter: function(args) {
        this.haystack = args[0].readCString();
        this.needle = args[1].readCString();
    },
    onLeave: function(retval) {
        if (this.needle.includes("frida") || this.needle.includes("xposed")) {
            console.log("[+] Hiding: " + this.needle);
            retval.replace(ptr(0));  // Return NULL (not found)
        }
    }
});

You can also hook fopen to hide /proc/self/maps or return fake contents. It’s a cat and mouse game but usually you can win.

A Recon Script for Native Analysis

Here’s a script I use when starting native analysis on a new app:

// native-recon.js
console.log("[*] Native Recon Starting...\n");

// List app's native libraries
Process.enumerateModules({
    onMatch: function(m) {
        if (m.path.includes("/data/")) {
            console.log("[LIB] " + m.name);
            
            // List exports for each
            m.enumerateExports().forEach(function(e) {
                if (e.type === "function") {
                    console.log("  -> " + e.name);
                }
            });
        }
    },
    onComplete: function() {
        console.log("\n[*] Library enumeration complete\n");
    }
});

// Hook common crypto functions
var cryptoHooks = {
    "libcrypto.so": ["AES_encrypt", "AES_decrypt", "EVP_EncryptUpdate", "EVP_DecryptUpdate"],
    "libc.so": ["open", "read", "write"]
};

Object.keys(cryptoHooks).forEach(function(lib) {
    cryptoHooks[lib].forEach(function(func) {
        try {
            var addr = Module.findExportByName(lib, func);
            if (addr) {
                Interceptor.attach(addr, {
                    onEnter: function(args) {
                        console.log("[HOOK] " + lib + "!" + func + "()");
                    }
                });
            }
        } catch(e) {}
    });
});

console.log("[*] Hooks installed. Use the app...\n");

Tips for Native Hooking

A few things that have helped me:

  • Match architectures — ARM offsets are different from ARM64. Check what your target device uses.
  • Watch for ASLR — Always calculate addresses relative to the module base, never use hardcoded addresses.
  • Handle crashes gracefully — Native hooks can crash the app if you mess up. Save your work and use try/catch.
  • Combine with Java hooks — Often the interesting flow goes Java → Native → Java. Hook both sides.
  • Use hexdump() — Frida’s built-in hexdump is great for viewing binary data.

What’s Next

With native hooking you can get into parts of apps that developers thought were protected. I’ve found hardcoded keys, bypassed integrity checks, and reverse engineered proprietary protocols using these techniques.

In a future post I’ll cover automating app analysis with Frida scripts and objection. Building reusable tools saves a ton of time on engagements.

More to come!

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