Exploring Native Functions on Android And Runtime Analyses Using Jadx, Ghidra, and Frida
Preface
Exploring native functions on Android tutorial. Mobile penetration testing often involves running dynamic analyses on the target application when bypassing root detection and SSL pinning are the most common scenarios, and It’s easy to find pre-made scripts and tools for the job.
But as we evolve and gain more experience, we stumble across cases and goals where using pre-made scripts and tools is not enough. The time comes to write custom scripts, dig inside the (decompiled and obfuscated) source code, start hooking functions, and examine them at runtime. At this point, we refer to the Frida API (overwhelming at first but excellent) documentation and search the Internet for Frida tutorials and examples.
Sometimes inspecting and hooking the decompiled Java code is not enough.
Looking at and performing dynamic analyses on the decompiled Java code is sometimes not enough. Developers often use native code written in C and C++, compiled into ELF format binaries, and embedded in the application. They may use native code for tasks that require high performance, such as image processing and rendering, or share functionality across multiple platforms, such as the Flatter Framework.
Let’s take a look at a specific use case using JADX, Ghidra, and Frida to extract an encrypted library from the application’s memory at runtime.
Introduction
In one of our recent assessments, a client told us that their most valuable IP (Intellectual Property) is a library encrypted in a file inside the APK, which only gets decrypted in memory. So, we told him the first thing that came to mind:
If it is used on the client side, it can be extracted.
Obviously, after that, we had to dig and pull this encrypted file. So, let’s get things started.
Environment setup
To unpack this issue, we will be using the following tools. All of them are free, open-source projects.
- JADX – Dex to Java decompiler.
- GHIDRA – Software Reverse Engineering Framework.
- FRIDA – Dynamic Instruction toolkit.
First things first: We install these tools on our machine.
Next, we set up Frida to run on the Android device. We used a rooted Android device, dropped the “frida-server” binary on the device, and ran it with the root user. If you use an unrooted device, you will have to use the “frida-gadget” instead and inject it into the application. You can read about it here and here.
Goal reminder: Get the encrypted library file from the application in an unencrypted form.
First step:
After the client sends us their APK file, the first step in any mobile testing project is to put the APK in Jadx. Normally, we would do a quick Google search on how Android applications read files from the file system and search the code in Jadx. Or, even simpler, search for the encrypted file name in the application code and work our way through the functions until we understand the logic of the specific part of the application we are interested in abusing. However, in this case, we already knew that all the application’s heavy lifting was in its native libraries, including decrypting and loading the encrypted libraries.
Locating Native functions in Jadx
In short, to use native code inside Android applications, the developer needs to load the library using the function System.loadLibrary(String libname) or System.load(String filename), and declare the function they want to use with the following syntax:
public final native {return type} funcName(args)
Using Jadx text search to locate all the code that loads a library in the application
The Java class that loads the native library and defines the exported functions of the native library
Now we can see that the loaded native library exports a function that the Java code refers to as loadNet. So let’s move on to Ghidra and analyze the native code. You don’t need to be a reverse engineering expert; even as a beginner, you can understand enough to get what you need.
Examining The Native Code In Ghidra
Open Ghidra, create a new project, then go to File à Batch import…, and import all the application’s native libraries (the default location is lib/{cpuType}/).
Now we need to find the exported function we saw earlier in the Java code. Luckily, the exported functions have a strict naming policy to work with the Android JDK. They must start with Java_, followed by the package name, and then the Java class file.
Open the desired native library in Ghidra, and on the left side, locate the “Symbol Tree” windows, in which we can find the library’s functions and the exported functions.
The native library’s exported functions in Ghidra
Clicking each function opens them in the assembly window and the decompiled code on the right window. If the window is missing, go to Window à Decompiler, or press CTRL + E to open it.
When looking at the exported loadNet function, we saw that the function calls another function in the library, also called loadNet. So we ignored all the other code in that function and jumped to the next loadNet function. There, we found a call to a method called decryptModel in a different library called base.
The exported loadNet function, the inner loadNet function, and the call to decryptModel function
Using Frida
Here things started to look promising, so we used Frida to watch arguments sent to each function. Writing a script whenever we want to test something with Frida is time-consuming, so we prefer to use the command line interpreter.
Spawning the application with Frida
First, we find the loadNet function and then the decryptModel function, using Process.enumerateModules() to get the library we want and Module.enumerateExports() to find the function inside it that we need.
We find two functions, the wrapper loadNet, and loadNet. Looking at the loadNet function signature, we see four arguments, where the first three are of type “int”, and the last one is a “char” pointer.
Now we intercept the loadNet function and use the onEnter event to print the function’s arguments.
The last argument is a pointer, so we read the memory pointed to by this pointer.
We can see that the fourth argument is a string containing the absolute path to the encrypted file we’re interested in. This is also the argument passed to the decryptModel function.
Now let’s go to the decryptModel function.
The function gets one argument — the absolute path to the encrypted file
Then, we see a call to the stat function with the file path argument, which returns information about the file, such as file size, last access, etc., and pushes that information to a buffer called auStack264.
Next, the function reads the file to the memory pointed by *in_x8.
Finally, at the end of the function, we see three calls to functions from a PRNG class.
The first function receives the auStack264 buffer with the file stat (with an offset of 0x80, probably to access a specific property) and seems to init the decryption. The function chooses an algorithm, prepares the key, and we skip it. The second function Prng::Process receives three arguments, the file stat, the file itself, and the file size. So, we intercept this function and look at the function’s arguments.
We find two Process functions, but we can see that the first function is from the Hash class, and the second is the one we are looking for from the Prng class.
We can also compare the function address at runtime to the function address at Ghidra. Of course, these values won’t be the same because Ghidra sets a default offset of 00100000, but they should be the same at the lowest bits. (You can locate the base address of the library using Frida’s Process.enumeratemodules() function and set this value as the base value in Gidra).
The Process function receives three arguments
We intercept this function and print the values of its arguments and the first 50 bytes of the memory pointed to by the first two arguments.
If we look at the original encrypted file in a hex editor, we can see that the second pointer references the memory address in which the encrypted file content is written.
Now the Process function does not return a value, so without digging too much inside the function, we can guess that the function writes the decrypted Module back to the same location in memory as the encrypted Module.
We construct a native pointer from the second argument at the onEnter event and use this object to pass it to the onLeave event. There we print the first 100 bytes from the pointer.
Seeing actual strings in the result, we can assume that we found what we were looking for. Now, we need to get the full file outside the Android device.
(We arbitrarily assumed that the file size would be less than the encrypted file size and pulled the full encrypted file size from memory. This assumption worked for our case, but this is not good practice and most likely does not work in all cases)
We use ADB to connect to the Android device, switch to root, and copy the file to /sdcard, where we can pull it out.
Conclusion
We’ve demonstrated a specific use case for extracting an encrypted library from an Android application at runtime. The process involved the use of JADX, GHIDRA, and Frida tools, all of which are free and open source. The first step was to locate the Java class that loaded the native library and defined the exported functions. This information was then used in GHIDRA to analyze the native code and find the exported function. We highlighted how even beginners could understand enough to get what they need by using these tools. The techniques we’ve demonstrated can be helpful to others who are interested in exploring native functions on Android, especially in cases where inspecting and hooking the decompiled Java code is not enough. Having some troubles? feel free to contact us
Reference
https://medium.com/swlh/exploring-native-functions-with-frida-on-android-part-1-bf93f0bfa1d3
https://frida.re/docs/home/ – Frida API