跳到主要内容Android 逆向:在 Unidbg 中解决 native 函数内调用 Java 方法的报错 | 极客日志Javajava
Android 逆向:在 Unidbg 中解决 native 函数内调用 Java 方法的报错
综述由AI生成Android 逆向中使用 Unidbg 进行离线执行时,native 层调用 Java 方法常因缺少实现而报错。文章分析了常见错误类型如 FindClass 失败、GetMethodID 返回 null 及类型不匹配等问题,并提供了四种解决方案:通过 AbstractJni 子类自定义返回值、加载真实 APK 文件、使用轻量级 Dex 模拟类以及监控 RegisterNatives 动态注册。重点展示了如何通过拦截 JNI 回调返回伪造数据以绕过检测,确保 native 函数正常执行,适用于安全审计与逆向分析场景。
人间失格25 浏览 背景与目标
在使用 Unidbg 对 Android .so 进行离线执行时,常会遇到 native 函数内部调用 Java 方法(JNIEnv 回调)的情况。若未在 Unidbg 中提供对应的 Java 实现或模拟,通常会报出 "not implemented"、"FindClass 失败"、"GetMethodID 返回 null"、"类型不匹配"等错误。
| 错误类型 | 原因 | 解决方案 |
|---|
| UnsupportedOperationException | AbstractJni 未实现 | 实现对应方法 |
| FindClass 失败 | 类名不准或未加载 | 确认类名,加载 APK/Dex |
| GetMethodID 为 null | 签名不匹配 | 校验 JNI 签名 |
| 类型不匹配 | 返回 DvmObject 类型错误 | 返回正确类型 |
| native 崩溃 | 寄存器/内存问题 | 开启 verbose,Inspect |
原理
native 如何调用 Java?
在真实的 Android 环境中,native 获取到 JNIEnv* 后,会使用如下调用:
FindClass("com/example/Device")
GetStaticMethodID(...) / GetMethodID(...)
CallStaticObjectMethod(...) / CallObjectMethod(...)
- 读取字段:
GetStaticFieldID(...) / GetObjectField(...)
这些都属于"native 回调 Java"的过程。只要 native 里有这些调用,Unidbg 默认是"不知道如何返回"的,除非你提供"Java 世界"的实现或 stub。
Unidbg 如何承接 JNI 回调?
- Unidbg 在创建 VM(Dalvik/ART)后,会通过
vm.setJni(...) 绑定一个 AbstractJni 的子类。
- 当 native 通过 JNIEnv 调用 Java 方法时,Unidbg 会把这次调用"回调"到你的
AbstractJni 子类中对应的函数,例如:
callObjectMethod(...)
callStaticObjectMethod(...)
getStaticIntField(...) / getStaticObjectField(...) 等
- 你需要在这些方法里,根据"类名 + 方法名 + 签名"判断当前调用是谁,并返回合适的
DvmObject<?>(如 StringObject、DvmInteger)。这样 native 就能得到期望值,继续执行。
执行流程:
- 启动:配置 Emulator + VM
- 加载 libdemo.so
- 执行 JNI_OnLoad
调用 JNI 导出函数Native 是否通过 JNIEnv 调用 Java?路由到 AbstractJni 对应方法返回模拟的 DvmObject 或基础类型Native 继续执行业务逻辑将执行结果返回给 Unidbg常见报错与原因
- java.lang.UnsupportedOperationException: callObjectMethod … not implemented
你的 AbstractJni 子类没有处理该 Java 方法的调用(类/方法/签名没匹配)。
- FindClass("xxx") 返回空或抛异常
你的 Jni 没有正确处理 FindClass,或 vm.resolveClass("xxx") 对应的类没有可用(未加载 APK/Dex 或未 stub)。
- GetMethodID/GetStaticMethodID 返回空
与上类似;签名不匹配尤为常见(注意 JNI 签名格式)。
- 返回类型不对导致崩溃
例如 native 期望 String,你却返回了一个非 StringObject;或者签名是 ()I(返回 int),你却返回了 DvmObject。
场景
- native 函数
doWork(Context ctx) 需要调用 Java 层获取设备信息(如 packageName、deviceId、Build.MODEL 等),再拼接返回一个签名字符串。
- 在 Unidbg 中,我们用自定义 Jni 拦截这些调用并返回"伪造数据",让函数跑通。
native 中的伪代码示例:
jstring Java_com_example_NativeBridge_doWork(JNIEnv* env, jclass clazz, jobject ctx){
jclass devCls = env->FindClass("com/example/Device");
jmethodID getId = env->GetStaticMethodID(devCls,"getDeviceId","(Landroid/content/Context;)Ljava/lang/String;");
jstring deviceId =(jstring) env->CallStaticObjectMethod(devCls, getId, ctx);
jclass ctxCls = env->FindClass("android/content/Context");
jmethodID pkg = env->GetMethodID(ctxCls,"getPackageName","()Ljava/lang/String;");
jstring pkgName =(jstring) env->CallObjectMethod(ctx, pkg);
jclass buildCls = env->FindClass("android/os/Build");
jfieldID modelField = env->GetStaticFieldID(buildCls,"MODEL","Ljava/lang/String;");
jstring model =(jstring) env->GetStaticObjectField(buildCls, modelField);
}
解决方案
方案一、用 AbstractJni 自定义方法返回值(最常用)
- 优点:简单直接,适合没有完整 APK 的场景
- 做法:匹配类名/方法名/签名,返回合理的 DvmObject 或基础类型,让 native 满足条件分支即可
unidbg 调用流程
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.utils.Inspector;
public class UnidbgJniDemo {
private AndroidEmulator emulator;
private VM vm;
private Module module;
public UnidbgJniDemo() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(null);
vm.setVerbose(true);
vm.setJni(new MyJni(vm));
DalvikModule dm = vm.loadLibrary("demo", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public void callDoWork() {
DvmClass bridge = vm.resolveClass("com/example/NativeBridge");
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
"doWork(Landroid/content/Context;)Ljava/lang/String;",
context
);
System.out.println("doWork result = " + result.getValue());
}
public static void main(String[] args) {
UnidbgJniDemo demo = new UnidbgJniDemo();
demo.callDoWork();
}
static class MyJni extends AbstractJni {
private final VM vm;
public MyJni(VM vm) {
this.vm = vm;
}
@Override
public DvmClass findClass(VM vm, String className) {
return vm.resolveClass(className);
}
@Override
public DvmObject<?> callStaticObjectMethod(VM vm, DvmClass dvmClass, String methodName, String signature, VarArgs varArgs) {
String className = dvmClass.getName();
if ("com/example/Device".equals(className) && "getDeviceId".equals(methodName) && "(Landroid/content/Context;)Ljava/lang/String;".equals(signature)) {
DvmObject<?> context = varArgs.getObjectArg(0);
return new StringObject(vm, "FAKE-DEVICE-ID-123456");
}
throw new UnsupportedOperationException("callStaticObjectMethod not implemented: " + className + "." + methodName + signature);
}
@Override
public DvmObject<?> callObjectMethod(VM vm, DvmObject<?> dvmObject, String methodName, String signature, VarArgs varArgs) {
String className = dvmObject.getObjectType().getName();
if ("android/content/Context".equals(className) && "getPackageName".equals(methodName) && "()Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "com.example.app");
}
throw new UnsupportedOperationException("callObjectMethod not implemented: " + className + "." + methodName + signature);
}
@Override
public DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
String className = dvmClass.getName();
if ("android/os/Build".equals(className) && "MODEL".equals(fieldName) && "Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "Pixel-FAKE-Model");
}
if ("android/os/Build".equals(className) && "MANUFACTURER".equals(fieldName) && "Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "Google");
}
return super.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
@Override
public int getStaticIntField(VM vm, DvmClass dvmClass, String fieldName) {
String className = dvmClass.getName();
if ("android/os/Build$VERSION".equals(className) && "SDK_INT".equals(fieldName)) {
return 23;
}
return super.getStaticIntField(vm, dvmClass, fieldName);
}
@Override
public void registerNativeMethods(VM vm, DvmClass dvmClass, String className, DvmNativeMethod[] methods) {
System.out.println("RegisterNatives for " + className + ", methods count=" + methods.length);
super.registerNativeMethods(vm, dvmClass, className, methods);
}
}
}
方案二、加载 APK
配置说明:
将真实 APK 文件放在指定路径(修改 new File("path/to/your/app.apk"))
确保 APK 中包含 com.example.NativeBridge 类
将目标 SO 库(如 libnative-lib.so)放在工作目录或指定路径
只需在 JNI 中补充 APK 中缺少的 Android 系统方法
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.Module;
import java.io.File;
public class ApkLoadedDemo {
private AndroidEmulator emulator;
private VM vm;
private Module module;
public ApkLoadedDemo() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
File apkFile = new File("path/to/your/app.apk");
vm = emulator.createDalvikVM(apkFile);
vm.setVerbose(true);
vm.setJni(new ApkJni(vm));
DalvikModule dm = vm.loadLibrary("native-lib", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public void executeNativeMethod() {
DvmClass bridge = vm.resolveClass("com/example/NativeBridge");
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
"doWork(Landroid/content/Context;)Ljava/lang/String;",
context
);
System.out.println("Native method result: " + result.getValue());
}
public static void main(String[] args) {
ApkLoadedDemo demo = new ApkLoadedDemo();
demo.executeNativeMethod();
}
static class ApkJni extends AbstractJni {
private final VM vm;
public ApkJni(VM vm) {
this.vm = vm;
}
@Override
public DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
if ("android/os/Build".equals(dvmClass.getName()) && "MODEL".equals(fieldName) && "Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "Pixel-5");
}
return super.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
@Override
public int getStaticIntField(VM vm, DvmClass dvmClass, String fieldName) {
if ("android/os/Build$VERSION".equals(dvmClass.getName()) && "SDK_INT".equals(fieldName)) {
return 30;
}
return super.getStaticIntField(vm, dvmClass, fieldName);
}
}
}
方案三、轻量 Dex 实现
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.Module;
import com.github.unidbg.utils.Inspector;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
public class LightDexImplementation {
private AndroidEmulator emulator;
private VM vm;
private Module module;
public LightDexImplementation() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(null);
vm.setVerbose(true);
byte[] dexBytes = generateLightDex();
DalvikModule dm = vm.load(dexBytes);
dm.callJNI_OnLoad(emulator);
vm.setJni(new LightJni(vm));
dm = vm.loadLibrary("demo", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
private byte[] generateLightDex() {
DexBuilder dexBuilder = new DexBuilder();
dexBuilder.addClass("com/example/NativeBridge", "public class NativeBridge {\n" +
" public static native String doWork(android.content.Context ctx);\n" +
"}");
dexBuilder.addClass("com/example/Device", "public class Device {\n" +
" public static String getDeviceId(android.content.Context ctx) {\n" +
" return \"LIGHT-DEX-DEVICE-ID\";\n" +
" }\n" +
"}");
return dexBuilder.build();
}
private void generateLightApk() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
JarOutputStream jar = new JarOutputStream(baos);
jar.putNextEntry(new JarEntry("classes.dex"));
jar.write(generateLightDex());
jar.closeEntry();
jar.putNextEntry(new JarEntry("AndroidManifest.xml"));
jar.write(("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
" package=\"com.example.lightapp\">\n" +
" <application android:label=\"LightApp\">\n" +
" </application>\n" +
"</manifest>").getBytes());
jar.closeEntry();
jar.close();
try (FileOutputStream fos = new FileOutputStream("light-app.apk")) {
fos.write(baos.toByteArray());
}
}
public void executeDoWork() {
DvmClass bridge = vm.resolveClass("com/example/NativeBridge");
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
"doWork(Landroid/content/Context;)Ljava/lang/String;",
context
);
System.out.println("doWork result: " + result.getValue());
}
public static void main(String[] args) {
LightDexImplementation demo = new LightDexImplementation();
demo.executeDoWork();
}
static class LightJni extends AbstractJni {
private final VM vm;
public LightJni(VM vm) {
this.vm = vm;
}
@Override
public DvmObject<?> callObjectMethod(VM vm, DvmObject<?> dvmObject, String methodName, String signature, VarArgs varArgs) {
if ("android/content/Context".equals(dvmObject.getObjectType().getName()) && "getPackageName".equals(methodName) && "()Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "com.example.lightapp");
}
return super.callObjectMethod(vm, dvmObject, methodName, signature, varArgs);
}
@Override
public DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
if ("android/os/Build".equals(dvmClass.getName()) && "MODEL".equals(fieldName) && "Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "LightDex-Device");
}
return super.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
}
}
方案四、RegisterNatives 监控与分析
一些 .so 通过 RegisterNatives 动态注册 JNI 方法,Unidbg 会回调到 registerNativeMethods,便于记录与分析。
- 添加 RegisterNatives 钩子监控
- 捕获并解析所有动态注册的 JNI 方法
- 输出方法名、签名和函数指针
- 适用于逆向分析和安全审计
- 不影响正常功能执行
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
public class RegisterNativesMonitor {
private AndroidEmulator emulator;
private VM vm;
private Module module;
public RegisterNativesMonitor() {
emulator = AndroidEmulatorBuilder.for32Bit().build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(null);
vm.setVerbose(true);
vm.setJni(new MonitorJni(vm));
DalvikModule dm = vm.loadLibrary("demo", true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
setupRegisterNativesHook();
}
private void setupRegisterNativesHook() {
long registerNativesAddr = module.findSymbol("RegisterNatives");
if (registerNativesAddr == 0) {
System.out.println("RegisterNatives symbol not found");
return;
}
emulator.getBackend().addHook(new RegisterNativesHook(), registerNativesAddr);
}
public void executeDoWork() {
DvmClass bridge = vm.resolveClass("com/example/NativeBridge");
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
"doWork(Landroid/content/Context;)Ljava/lang/String;",
context
);
System.out.println("doWork result: " + result.getValue());
}
public static void main(String[] args) {
RegisterNativesMonitor demo = new RegisterNativesMonitor();
demo.executeDoWork();
}
class RegisterNativesHook extends BlockHook {
@Override
public void hook(Backend backend, long address, int size, Object user) {
long envPtr = backend.getRegisters()[0];
long clazz = backend.getRegisters()[1];
long methodsPtr = backend.getRegisters()[2];
int methodCount = backend.getRegisters()[3];
String className = vm.findClassByAddress(clazz).getName();
System.out.println("RegisterNatives called for class: " + className);
System.out.println("Method count: " + methodCount);
long currentPtr = methodsPtr;
for (int i = 0; i < methodCount; i++) {
long namePtr = backend.readPointer(currentPtr);
String name = backend.readCString(namePtr);
long sigPtr = backend.readPointer(currentPtr + emulator.getPointerSize());
String signature = backend.readCString(sigPtr);
long fnPtr = backend.readPointer(currentPtr + emulator.getPointerSize() * 2);
System.out.printf("|-- Method %d: %s%s @ 0x%x%n", i + 1, name, signature, fnPtr);
currentPtr += emulator.getPointerSize() * 3;
}
}
}
static class MonitorJni extends AbstractJni {
private final VM vm;
public MonitorJni(VM vm) {
this.vm = vm;
}
@Override
public DvmObject<?> callStaticObjectMethod(VM vm, DvmClass dvmClass, String methodName, String signature, VarArgs varArgs) {
if ("com/example/Device".equals(dvmClass.getName()) && "getDeviceId".equals(methodName) && "(Landroid/content/Context;)Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "MONITOR-DEVICE-ID");
}
return super.callStaticObjectMethod(vm, dvmClass, methodName, signature, varArgs);
}
@Override
public DvmObject<?> callObjectMethod(VM vm, DvmObject<?> dvmObject, String methodName, String signature, VarArgs varArgs) {
if ("android/content/Context".equals(dvmObject.getObjectType().getName()) && "getPackageName".equals(methodName) && "()Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "com.example.monitor");
}
return super.callObjectMethod(vm, dvmObject, methodName, signature, varArgs);
}
@Override
public DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
if ("android/os/Build".equals(dvmClass.getName()) && "MODEL".equals(fieldName) && "Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "Monitor-Device");
}
return super.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online