关于 JNI 函数逆向(从 Java 到 native)
一、JNI基础概念
JNI(Java Native Interface) 是 Java 调用 native 层 C/C++ 函数的桥梁。
在 Android 中,Java 使用 System.loadLibrary("xxx")
加载 so 文件,然后通过 native
方法声明调用底层函数。
public class Test {static {System.loadLibrary("native-lib"); // 加载 native-lib.so}public native String getToken(String userId); // Java 调用 native 函数
}
native 层实现(C/C++):
JNIEXPORT jstring JNICALL Java_com_example_Test_getToken(JNIEnv *env, jobject thiz, jstring userId) {const char* id = (*env)->GetStringUTFChars(env, userId, 0);// 加密处理逻辑……return (*env)->NewStringUTF(env, "encrypt_result");
}
二、逆向场景目标:追到 so
里具体函数、算法
目标是:
-
找出 Java 中调用的
native
函数对应哪个.so
文件; -
分析
.so
中的函数逻辑(加密/校验); -
使用 Frida 或手动还原算法。
三、逆向流程分解(Java → JNI → native)
步骤 1:找到 native 函数调用点
使用 jadx 打开 APK,搜索 native
关键字或 System.loadLibrary
,例如:
public native String encrypt(String input);
这会生成一个 JNI 函数名:
Java_包名_类名_方法名(JNIEnv *env, jobject obj, jstring input)
如:
Java_com_example_Test_encrypt
也可能是动态注册(没有 Java_ 开头),用 RegisterNatives
注册的。
步骤 2:定位对应的 .so
文件
-
System.loadLibrary("xxx")
表示会加载libxxx.so
-
找出
libxxx.so
,用IDA
或Ghidra
打开分析
步骤 3:分析 JNI
函数(导出函数 / 动态注册)
>静态注册
在 so 中能看到类似函数名:
Java_com_example_Test_encrypt
可以直接反汇编分析。
>动态注册(更常见)
在 IDA 中搜索 RegisterNatives
,找到注册表:
(*env)->RegisterNatives(env, clazz, methods, methodCount);
其中 methods
是一个结构体数组,包含 Java 方法名、签名、函数指针。
static JNINativeMethod methods[] = {{"encrypt", "(Ljava/lang/String;)Ljava/lang/String;", (void *)encrypt_impl},
};
就能定位 encrypt_impl
函数地址。
步骤 4:分析 native 函数核心逻辑
打开 IDA,进入函数逻辑:
JNIEXPORT jstring JNICALL encrypt_impl(JNIEnv *env, jobject thiz, jstring input) {const char *str = (*env)->GetStringUTFChars(env, input, 0);// --> 加密、混淆、MD5/SHA/AES/RSA/自定义算法分析return (*env)->NewStringUTF(env, result);
}
此时可以使用 Frida 或静态还原分析算法。
步骤 5:恢复 Java ↔ native 参数结构
常见类型映射如下:
Java 类型 | JNI C/C++ 类型 |
---|---|
int | jint (int32) |
long | jlong (int64) |
String | jstring |
byte[] | jbyteArray |
Object | jobject |
对应使用:
const char *nativeStr = (*env)->GetStringUTFChars(env, jstringObj, NULL);
四、结合 Frida Hook 的动态分析
可以使用 Frida 动态 Hook JNI 函数,更直观分析逻辑:
Interceptor.attach(Module.findExportByName("libnative-lib.so", "Java_com_example_Test_encrypt"), {onEnter: function(args) {console.log("Hook encrypt input: " + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString());},onLeave: function(retval) {console.log("encrypt result: " + Java.vm.getEnv().getStringUtfChars(retval, null).readCString());}
});
或在 Java 层用 Frida 的 Java.perform
Hook:
Java.perform(function() {var cls = Java.use("com.example.Test");cls.encrypt.implementation = function(arg) {console.log("encrypt arg = " + arg);var ret = this.encrypt(arg);console.log("encrypt ret = " + ret);return ret;}
});
五、JNI 函数逆向常见技巧
技巧 | 说明 |
---|---|
搜索 RegisterNatives | 确定动态注册函数表位置 |
搜索 GetStringUTFChars | 找到参数获取代码 |
用 xrefs 查找调用链 | IDA 中右键 Xrefs to |
使用 Frida trace so | 快速获得运行时行为 |
使用 frida-trace -n 自动生成 hook 模板 | 减少手工工作 |
用 strings 命令提取字符串线索 | 看是否有 base64、json、AES等加密关键词 |
六、示例实战流程
-
用 jadx 找到 native 方法如
getToken(String uid)
-
找到
System.loadLibrary("encrypt")
→ 分析libencrypt.so
-
用 IDA 找到
Java_..._getToken
或通过RegisterNatives
找getToken
-
分析内部处理逻辑,比如:
-
先 base64 解码
-
再 SHA1 加密
-
拼接签名参数返回
-
-
用 Frida Hook 输出中间值验证你的推测是否正确
-
成功提取 native 加密算法,Python 重写绕过
七、总结
JNI 函数逆向的核心是从 Java native 方法 → 找到 so 文件函数实现 → 分析参数/返回值结构 → 动静结合还原算法。