The Native Side of Android
Most Android apps are written in Java/Kotlin. But when they want more speed, security, or just to be extra sneaky 👀, they use native code — written in C or C++ and compiled into .so
shared libraries.
These .so
files:
-
Are compiled for your phone’s CPU (ARM, x86, etc.)
-
Run natively — not in the JVM
-
Usually hide sensitive logic like license checks, encryption, or flag validation in C/C++
Meet JNI: Java’s Plug to the Native Matrix
Java Native Interface (JNI) = a bridge between Java/Kotlin and C/C++
It lets Java code call native code and vice versa. Think of it as:
Android Java/Kotlin ↔ C/C++ (.so file)
You write something like this in Java:
public native int add(int a, int b); // We declare it, but code lives in C/C++
static {
System.loadLibrary("native-lib"); // This loads native-lib.so
}
And the native part (in C++) looks like:
JNIEXPORT jint JNICALL
Java_com_example_MyClass_add(JNIEnv* env, jobject obj, jint a, jint b) {
return a + b; // Finally, C does some work!
}
Yes, that method name is ridiculously long. But it’s how JNI dynamically links your Java method to the correct C function.
So, What the Heck Is JNIEnv*
?
Glad you asked.
JNIEnv*
is like your remote control to Java — from inside native code. It lets C/C++ do things like:
-
Create Java objects
-
Call Java methods
-
Throw exceptions
-
Manipulate strings, arrays, etc.
The Call Flow
MainActivity.java
↓
Calls native method `add()`
↓
JNI jumps into .so file
↓
Runs C++ function `Java_com_example_MyClass_add(...)`
↓
You can now "talk back" to Java via JNIEnv*
In short: you’re in C, but still whispering sweet Java nothings. 💬
Two Ways to Link Java <—> C/C++
There are two flavors of JNI linking:
1. Dynamic Linking
You follow the naming pattern:
Java_<package>_<Class>_<method>
Java:
public native void sayHello();
C/C++:
JNIEXPORT void JNICALL
Java_com_example_myapp_MyClass_sayHello(JNIEnv* env, jobject obj) {
printf("Hi!\n");
}
2. Static Linking (a.k.a. I’m too cool for long names)
Here, you register native functions manually using RegisterNatives
.
Java:
public native String doWork(int num);
C++:
jstring myFunction(JNIEnv* env, jobject obj, jint num) {
return env->NewStringUTF("Work done!");
}
Then, you tell Java:
“Hey! When you call doWork()
, run myFunction()
instead.”
static JNINativeMethod methods[] = {
{"doWork", "(I)Ljava/lang/String;", (void*)myFunction}
};
Let’s break down that strange-looking method signature:
Part | Meaning |
---|---|
I | Parameter is an int |
Ljava/lang/String; | Return type is String |
() | Wraps the arguments |
Finally, we register it when the library is loaded:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
vm->GetEnv((void**)&env, JNI_VERSION_1_6);
jclass clazz = env->FindClass("com/example/MyClass");
env->RegisterNatives(clazz, methods, 1);
return JNI_VERSION_1_6;
}
Boom. Static linkage.
Bonus: This can make reverse engineering harder because names don’t follow that Java-style pattern. 😏
Reversing .so Files Like a Pro
When you’re reversing an APK with .so
files:
-
Use JADX to check out the Java code
-
Find
native
method declarations -
Open the
.so
in Ghidra (or IDA, or Hopper) -
If it’s static linkage, look for
RegisterNatives
-
If it’s dynamic, look for those long, ugly
Java_com_example_X_Y
names -
Match the signatures and figure out the logic
Bonus: Ghidra + JNI
To make life easier, load JNI definitions:
-
Grab a
.gdt
file likejni_all.gdt
-
Import it in Ghidra
-
Go to your function
-
Right-click the first parameter (usually
undefined *param_1
) -
Change it to
JNIEnv *
Now your disassembly starts to make sense and looks a bit less like alien script.
TL;DR
-
Native code (
.so
files) runs directly on the CPU, often hiding secrets. -
JNI is the bridge between Java and native.
-
Use
JNIEnv*
to manipulate Java stuff from C. -
Linking can be dynamic (name-based) or static (
RegisterNatives
). -
Reverse engineering involves jadx + Ghidra (with JNI definitions).
Fun Fact: If you ever find yourself naming a function like
Java_com_super_secure_bankapp_MoneyManager_validateUser
,
just know some reverse engineer is gonna find it in 5 seconds. Use RegisterNatives
if you’re feeling spicy. 🌶️
Want help reversing a real .so
file or decoding a flag? Hit me up! 😎