Android Native Library Analysis

๐Ÿ“ŒWhat are Native Libraries?

  • Native libraries are compiled code written in low-level languages like C or C++ that run directly on the hardware.

  • They offer better performance and lower overhead compared to Java or Kotlin, which run on the Java Virtual Machine (JVM).

  • In Android, developers use native libraries to optimize performance-critical code, such as image processing, encryption, or game engines.

๐Ÿ”งHow Are Native Libraries Used in Android?

  • Writing Native Code

    • Native code is written in C or C++.

    • This is typically done using the NDK (Native Development Kit).

  • JNI (Java Native Interface)

    • JNI allows Java/Kotlin to call functions written in C/C++.

    • You write native methods in Java/Kotlin, then link them to implementations in C/C++.

      JNIEXPORT jstring JNICALL
      Java_com_example_myapp_NativeExample_stringFromJNI(JNIEnv *env, jobject obj) {
          return (*env)->NewStringUTF(env, "Hello from C!");
      }
    • Code

      • This code defines a native C function that can be called from Java/Kotlin using JNI.

      • It takes three parameters: env for the JNI environment, thiz for the Java object, and str for a Java string (jstring).

      • So if Java calls stringFromJNI(), it gets "Hello from C!" from native C code.

  • Compiling Native Code

    • Native code is compiled into .so files (shared objects).

    • These .so files are placed inside the APK under the /lib folder for each supported architecture (e.g., armeabi-v7a, arm64-v8a).

๐Ÿ”„ How Java Calls Native Code

  • System.loadLibrary("native-lib") is used to load libnative-lib.so.

๐Ÿ“How to Reverse .so Files

  • Decompile the APK

    • Use tools like apktool or JADX to extract the APK contents.

    • Navigate to the /lib/ folder where youโ€™ll find .so files for each architecture.

  • Use Ghidra to Analyze .so Files

    • How to use Ghidra

      • Click file โ‡’ new project โ‡’ OK

      • Now, double-click on the file again.

        Click on Yes and wait until the analysis is complete.

      • Click on Analyze again. After the analysis is complete, navigate to the left dropdown list Functions.

  • Focus on JNI Functions

    • Look for function names like Java_com_example_myapp_ClassName_functionName.

    • These are native methods called from Java/Kotlin.

  • Understand the Code

    • Ghidra shows the decompiled C-like code on the right and assembly code on the left.

    • You can rename variables, add comments, and trace function calls.

  • Use AI

    • Use AI tools to understand what the functions are doing.


๐Ÿ“ŒHooking the Native Functions

  • To hook native functions, we can use the Interceptor API. Here is the template for this:

    var targetAddress = Module.getExportByName("libName.so","Java_func")
    Interceptor.attach(targetAddress, {
        onEnter: function (args) {
            console.log('Entering ' + functionName);
            // Modify or log arguments if needed
        },
        onLeave: function (retval) {
            console.log('Leaving ' + functionName);
            // Modify or log return value if needed
        }
    });
  • Interceptor.attach: Attaches a callback to the specified function address. The targetAddress should be the address of the native function we want to hook.

  • onEnter: This callback is called when the hooked function is entered. It provides access to the function arguments (args).

  • onLeave: This callback is called when the hooked function is about to exit. It provides access to the return value (retval).

Always restart frida when you are hooking native functions or it will get messy.

๐Ÿ“When to use onEnter and when to use onLeave

  • onEnter: When the function starts executing

    • Use it when you want to:

      • Read or modify function arguments

      • Log or tamper with input values

      • Track call stacks, thread info, etc.

  • onLeave: When the function is about to return

    • Use it when you want to:

      • Read the return value

      • Modify (or "patch") the return value using .replace(...)

    • Example use case:

      onLeave: function(retval) {
          console.log("Original return value:", retval);
          retval.replace(623);  // Replace it with 623
      }

๐Ÿ“Finding Function Addresses

  • There are several ways to do this. Let me show you some APIs to do this.

    • Using the Frida API: Module.enumerateExports("sharedObjectName.so")

      • This API lists all the exported symbols from a specified module.

      • It requires one argument: the name of the module (shared library or executable) from which you want to enumerate the exports.

    • Using the Frida API: Module.getExportByName()

      • The Module.getExportByName(modulename, exportName) function fetches the address of the exported symbol with the specified name from the module (shared library). If youโ€™re unsure which library contains your exported symbol, you can pass null.

    • Using the Frida API: Module.findExportByName()

    • Calculate the offset and add it to the Module.getBaseAddress() (Base address)

    • Using the Frida API: Module.enumerateImports()

      • It provides the imports of a module.

  • Itโ€™s important not to hardcode this address since it changes every time the application starts due to ASLR being enabled by default in Android.

๐Ÿ“Exports and Imports

Exports refer to the functions or variables a library provides for external use, such as the functions we use daily in programming languages like Python and C. Imports are functions or variables imported by our application.

๐Ÿ“Native Hooking Methodology (with Frida)

๐Ÿ” 1. List All Exports of the Native Library

  • Goal: Identify the available functions that can be hooked in the native library (libFridaEight.so in this case).

    Module.enumerateExports("libFridaEight.so")

๐ŸŽฏ 2. Identify the Target Function to Hook

  • Use the name from the enumeration step to get the functionโ€™s address:

    var targetAddress = Module.getExportByName("libFridaEight.so", "Java_com_mobilehackinglab_FridaEight_MainActivity_funcName");

๐Ÿช 3. Hook the Function with Interceptor

  • Start with a basic Interceptor.attach:

    var targetAddress = Module.getExportByName("libFridaEight.so", "Java_com_mobilehackinglab_FridaEight_MainActivity_funcName");
    Interceptor.attach(targetAddress, {
        onEnter: function (args) {
            console.log('๐Ÿ“ Entered funcName');
        },
        onLeave: function (retval) {
            console.log('โœ… Returned from funcName with: ' + retval);
        }
    });
    

๐Ÿง  4. Understand Arguments and Return Value

  • If itโ€™s a JNI function:

    • args[0]: JNIEnv pointer

    • args[1]: jobject (usually this)

    • args[2+]: Function parameters

  • Explaintion

    When you hook a JNI function (a native function from Java that calls C/C++ code), Frida gives you the arguments in args like this:

    jboolean Java_com_example_app_MainActivity_strCmp(JNIEnv *env, jobject thiz, jstring input)
    • That means the function receives:

      1. JNIEnv *env โ†’ helps work with Java stuff (like strings)

      2. jobject thiz โ†’ reference to the Java object that called it (like this)

      3. jstring input โ†’ the actual Java string sent by the app

    • Now in Frida:

      Interceptor.attach(targetAddress, {
          onEnter: function (args) {
              console.log(args[0]); // ๐Ÿ‘ˆ This is `JNIEnv *env`
              console.log(args[1]); // ๐Ÿ‘ˆ This is `jobject thiz`
              console.log(args[2]); // ๐Ÿ‘ˆ This is the first real argument: `jstring input`
          }
      });

๐Ÿ”“ 5. Reconstruct native logic to extract secrets


Last updated