Everything we learned was related to hooking methods of Dalvik Virtual Machine in Java. The Android NDK (Native Development Kit) is a toolset provided by Google that allows developers to write native code in C/C++ for Android applications.
Why Developers Use NDK
Some applications require high performance or low-level access to system resources, which may be difficult to achieve using Java alone. In such cases, the NDK can be used to write native code that can be compiled into machine code and run directly on the device's CPU
Using the NDK can be beneficial for applications that require high performance, such as games or media applications, or for applications that need to access low-level system resources. However, it's important to note that using the NDK can also introduce additional complexity and may require more time and effort to develop and maintain compared to using Java alone.
How To Identify Native Functions
In android it’s really easy to find them. The first sign you find is
System.loadLibrary("native-lib")
. This load the library in memory. Then you seen function like this:public native String encryptString(String secretMessage)
Example native code:
#include<jni.h>
#include<string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_apphacking_ndkfrida_MainActivity_encryptString(
JNIEnv* env, jobject, jstring secretMessage){
return env->NewStringUTF("hello".c_str());
}
env → This is an pointer to all important functions like
NewStringUTF
which developers require. Usually used for type casting.jobject → This is an pointer to java object instance.
Enumerate Exported Function of Native Library
Module.enumerateExports("libnativesecret.so")
Example:
let exportedFunctions = Module.enumerateExports("libnativesecret.so")
exportedFunctions.forEach(func => {
if(func.name.indexOf("Java_") != -1){
send(func)
}
});
Enumerate Imported Function of Native Library
Module.enumerateImports("libnativesecret.so")
How To Hook NDK Functions
To hook NDK functions we use
Interceptor.attach
which has to callback. onEnter
and onLeave
. The onEnter
is before the function codes execute and onLeave
callback is when the function wants to return.Most of the times you didn’t need to hook native functions. Just hook Java function prototype. In upper example you can hook
encryptString
method in Java instead.setTimeout(() => {
let targetFunctionAddress = null
let exportedFunctions = Module.enumerateExports("libnativesecret.so")
exportedFunctions.forEach(func => {
if(func.name.indexOf("Java_lab_seczone64_nativesecret_MainActivity_encryptDecrypt") != -1){
send(func)
targetFunctionAddress = func.address
}
}); // this code find the address target function which here is encryptDecrypt
Interceptor.attach( targetFunctionAddress, {
onEnter: (args) => {
let inputString = Java.cast(ptr(args[2]), Java.use("java.lang.String")) // We need to cast jstring to string
console.log("[+] Arguments parameters: " + inputString)
},
onLeave: (ret) => {
var returnString = Java.cast(ptr(ret), Java.use("java.lang.String"))
console.log("[+] The return is: " + returnString)
}
})
}, 100);
Hooking System.loadLibrary
It’s tricky:
Java.perform(function() {
const System = Java.use('java.lang.System');
const Runtime = Java.use('java.lang.Runtime');
const VMStack = Java.use('dalvik.system.VMStack');
System.loadLibrary.implementation = function(library) {
try {
console.log('System.loadLibrary("' + library + '")');
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
if(library == "nativesecret"){
Interceptor.attach( Module.findExportByName("libnativesecret.so", "Java_lab_seczone64_nativesecret_MainActivity_encryptDecrypt"), {
onEnter: (args) => {
let inputString = Java.cast(ptr(args[2]), Java.use("java.lang.String"))
console.log("[+] Arguments parameters: " + inputString)
},
onLeave: (ret) => {
var returnString = Java.cast(ptr(ret), Java.use("java.lang.String"))
console.log("[+] The return is: " + returnString)
}
})
}
} catch(ex) {
console.log(ex);
}
};
})
VMStack.getCallingClassLoader
: This method returns the ClassLoader
associated with the caller of the method. It can be useful for obtaining information about the class loading context during runtime.The
java.lang.Runtime
class is a class in the Java SE API that provides access to the Java runtime environment. It serves as an interface between the Java application and the underlying operating system environment.Hooking strcmp Function
Interceptor.attach(
Module.findExportByName("libc.so", "strcmp"), {
onEnter: (args) => {
let firstArgStr = Memory.readUtf8String(args[0])
let secondArgStr = Memory.readUtf8String(args[1])
console.log("[+] Arguments are: " + firstArgStr + "\t:\t" + secondArgStr)
},
onLeave: (ret) => {
console.log("[+] Return value is: " + ret.toInt32())
}
}
)
Please consider sometimes
readUtf8String
function didn’t work. Because the string on the memory may not be an utf8
. Therefore we use readCString
function.Interceptor.attach(
Module.findExportByName("libc.so", "strcmp"), {
onEnter: (args) => {
let firstArgStr = Memory.readUtf8String(args[0])
let secondArgStr = Memory.readCString(args[1])
console.log("[+] Arguments are: " + firstArgStr + "\t:\t" + secondArgStr)
},
onLeave: (ret) => {
console.log("[+] Return value is: " + ret.toInt32())
}
}
)
Also you can change this args. But their not Java. To change them simply use this code:
Memory.writeUtf8String(args[0],"Hello")