Exploring Native Modules in Android with Frida

Android applications increasingly push sensitive logic into native C and C++ libraries loaded through JNI, often specifically to make reverse engineering harder than working with decompiled Java or Kotlin bytecode. Encryption routines, license validation, anti-tampering checks, and even core business logic frequently live in these native libraries. Frida remains the most effective tool for exploring this layer at runtime, letting testers hook native functions, trace execution, and manipulate behavior without needing a fully static reverse engineering workflow. This guide walks through practical techniques for exploring native modules during an authorized Android security assessment. This kind of native-layer analysis is a regular part of mobile application assessments conducted by Sagefox Cybersecurity.

Why Native Modules Deserve Separate Attention

The Gap Between Java-Level and Native-Level Analysis

Standard Android static analysis tools decompile Java and Kotlin bytecode effectively, but native libraries compiled to ARM or x86 machine code require a fundamentally different analysis approach. An application can appear clean at the Java layer while all of its actual sensitive logic executes inside a native library that static Java tooling never touches.

Common Reasons Developers Push Logic into Native Code

  • Performance-sensitive operations like cryptographic routines or media processing
  • Deliberate obfuscation of license validation or anti-piracy checks
  • Reuse of existing C/C++ libraries across multiple platforms
  • An attempt to raise the difficulty of reverse engineering sensitive algorithms

Setting Up the Environment for Native Analysis

Preparing a Rooted Test Device or Emulator

Native module analysis with Frida requires the same rooted environment used for other dynamic instrumentation work.

adb root
adb remount
adb push frida-server-16.1.4-android-x86_64 /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server &"

Confirm the target application and its loaded native libraries are visible before proceeding.

frida-ps -U | grep com.target.app

Identifying Loaded Native Libraries

Before hooking any native function, it helps to know exactly which native libraries the application has loaded into memory. Frida's Process API can enumerate this directly through a short script.

javascript

Process.enumerateModules().forEach(function(module) {
   console.log(module.name + " - " + module.base);
});

Running this against the target application lists every loaded module, including system libraries, letting you identify which ones belong to the application itself rather than the Android framework.

Enumerating Exported Functions in a Native Library

Listing Exports from a Target Module

Once the relevant native library is identified, listing its exported functions reveals the entry points available for hooking.

javascript

var module = Process.findModuleByName("libtarget.so");
module.enumerateExports().forEach(function(exp) {
   console.log(exp.name + " @ " + exp.address);
});

This is often the first meaningful step in understanding what a native library actually does, since exported function names frequently hint at their purpose even before any deeper analysis.

Using Frida Trace for Quick Function Discovery

For a faster initial pass without writing a custom script, frida-trace automatically hooks functions matching a given pattern and logs every call.

frida-trace -U -i "encrypt*" com.target.app
frida-trace -U -i "verify*" com.target.app

Running these traces while interacting with the application's login or payment flow reveals exactly which native functions get called during sensitive operations, along with the order and frequency of those calls.

Hooking Native Functions to Inspect Arguments

Writing a Custom Interceptor for a Specific Function

Once a function of interest is identified, a custom Frida script provides full control over inspecting its arguments and return values.

javascript

Interceptor.attach(Module.findExportByName("libtarget.so", "validateLicense"), {
   onEnter: function(args) {
       console.log("License key argument: " + Memory.readUtf8String(args[0]));
   },
   onLeave: function(retval) {
       console.log("Return value: " + retval);
       retval.replace(1);
   }
});

This example logs the license key passed into a validation function and forcibly modifies the return value to always indicate success, a common technique for testing whether client-side license or entitlement checks can be bypassed entirely through native-layer manipulation.
[cta]

Reading Complex Argument Structures

Native functions often accept pointers to structs rather than simple primitive types. Reading these requires understanding the structure layout, which can often be inferred through experimentation or by cross-referencing with decompiled code from a tool like Ghidra.

javascript

Interceptor.attach(Module.findExportByName("libtarget.so", "processPayment"), {
   onEnter: function(args) {
       var structPtr = args[0];
       var amount = structPtr.readFloat();
       var currency = structPtr.add(4).readCString();
       console.log("Payment amount: " + amount + " " + currency);
   }
});

Finding Functions Without Exported Symbols

Locating Internal Functions by Offset

Not every function of interest is exported. Internal functions require locating their address by offset from the module's base address, typically determined through prior static analysis in a disassembler.

javascript

var module = Process.findModuleByName("libtarget.so");
var targetOffset = 0x4a2c;
var targetAddress = module.base.add(targetOffset);

Interceptor.attach(targetAddress, {
   onEnter: function(args) {
       console.log("Internal function hit at offset 0x4a2c");
   }
});

This approach depends on the offset remaining stable across the specific build being tested, which means it needs to be reconfirmed whenever the application updates.

Using Frida's Pattern Scanning to Locate Functions Dynamically

Rather than relying on a hardcoded offset that breaks with every app update, Frida supports scanning memory for a specific byte pattern associated with a function's prologue, making the hook more resilient to minor build changes.

javascript

var module = Process.findModuleByName("libtarget.so");
Memory.scan(module.base, module.size, "55 8B EC 83 EC ??", {
   onMatch: function(address, size) {
       console.log("Pattern match found at: " + address);
   },
   onComplete: function() {
       console.log("Scan complete");
   }
});

Tracing Cryptographic Operations in Native Code

Hooking Common Crypto Library Functions

Applications using OpenSSL or BoringSSL for native-layer cryptography expose predictable function names that are useful hooking targets for understanding what data gets encrypted and with what key.

javascript

Interceptor.attach(Module.findExportByName("libcrypto.so", "AES_encrypt"), {
   onEnter: function(args) {
       console.log("Plaintext block: " + hexdump(args[0], { length: 16 }));
   }
});

Capturing plaintext immediately before it enters a standard encryption routine is one of the most reliable ways to recover sensitive data that the application intends to protect through encryption at rest or in transit.
[cta]

Extracting Keys Passed to Native Crypto Functions

Where the key itself is passed as a function argument rather than derived internally, hooking the relevant key-scheduling function reveals it directly.

javascript

Interceptor.attach(Module.findExportByName("libcrypto.so", "AES_set_encrypt_key"), {
   onEnter: function(args) {
       var keyLength = args[1].toInt32();
       console.log("AES key: " + hexdump(args[0], { length: keyLength / 8 }));
   }
});

Bypassing Native Anti-Tampering and Root Detection

Identifying Native Root Detection Checks

Root detection implemented at the native layer often checks for the presence of specific files, binaries, or system properties associated with rooted devices. Tracing common file access functions reveals these checks in action.

frida-trace -U -i "fopen" -i "access" -i "stat" com.target.app

Reviewing the traced calls for paths like /system/bin/su or /system/xbin/busybox identifies exactly which checks the application performs and where they live in the native library.

Neutralizing a Detected Check

Once the specific check is identified, hooking it directly to force a benign return value neutralizes it for the remainder of the testing session.

javascript

Interceptor.attach(Module.findExportByName("libtarget.so", "isDeviceRooted"), {
   onLeave: function(retval) {
       retval.replace(0);
   }
});

Combining Native Analysis with Static Reverse Engineering

Using Ghidra Alongside Frida for a Complete Picture

Frida excels at dynamic analysis, showing exactly what happens at runtime, but pairing it with static analysis in Ghidra provides the broader context needed to understand unfamiliar functions, locate offsets for internal functions without exports, and confirm struct layouts before writing argument-reading scripts. A typical workflow moves between the two: identify a function of interest statically in Ghidra, confirm its actual runtime behavior and argument values dynamically with Frida, then iterate.

Exporting Frida Findings Back Into Static Analysis

Runtime values captured through Frida, such as confirmed struct offsets or decrypted constants, can be fed back into Ghidra's analysis to label data structures and constants accurately, making subsequent static analysis passes significantly faster.

Documenting Native-Layer Findings for a Report

Demonstrating Concrete Impact

As with any dynamic analysis finding, the value of native module exploration comes from clearly showing what was recovered and under what realistic conditions, whether that is a bypassed license check, an extracted cryptographic key, or plaintext payment data captured before encryption. A raw list of hooked functions without context does not communicate risk effectively to a development team.

Providing Actionable Remediation Guidance

Findings from native module analysis typically map to concrete recommendations:

  • Move sensitive validation logic server-side wherever feasible, since any check performed entirely on the client can eventually be bypassed
  • Use hardware-backed key storage rather than passing raw keys through native function arguments
  • Layer multiple independent anti-tampering checks rather than relying on a single detectable function, understanding this raises difficulty rather than eliminating risk entirely
  • Avoid relying on obfuscation or native code placement alone as a security control, since it primarily increases analysis time rather than preventing analysis

This is the kind of structured, prioritized reporting that assessments from Sagefox Cybersecurity are built around, ensuring native-layer findings translate into fixes development teams can realistically implement.

Common Mistakes During Native Module Analysis

Hardcoding Offsets Without Accounting for Build Variance

Offsets for internal, non-exported functions can shift between build versions, compiler optimizations, or architecture targets. Relying on a hardcoded offset without validating it against the specific build under test produces unreliable results.

Ignoring Architecture Differences

A native library compiled for ARM64 behaves differently at the binary level than one compiled for x86, and testing exclusively on an x86 emulator may miss checks or behavior specific to the ARM builds most real devices actually run.

Skipping Static Context Before Hooking Blindly

Attempting to hook and interpret unfamiliar native functions purely through trial and error wastes significant time compared to a brief static analysis pass first to understand the function's likely purpose and argument structure.

Key Takeaways

Native modules remain one of the most effective places for Android applications to hide sensitive logic from casual reverse engineering, but Frida's ability to hook, trace, and manipulate native functions at runtime closes that gap effectively during an authorized assessment. Combining dynamic instrumentation with static analysis in a tool like Ghidra produces the most complete picture, turning what looks like an opaque compiled library into a fully understood and testable component of the application.

If your organization needs a mobile application assessment that goes deep into native-layer behavior rather than stopping at the Java bytecode layer, the team at Sagefox Cybersecurity can help uncover what your app's native code actually does at runtime.