跳到主要内容Unidbg 环境下解决 Native 调用 Java 方法报错问题 | 极客日志Javajava
Unidbg 环境下解决 Native 调用 Java 方法报错问题
Unidbg 模拟 Android 环境执行 .so 文件时,Native 层调用 Java 方法常因缺少实现抛出异常。解决方案包括自定义 AbstractJni 子类拦截回调并返回模拟对象,加载真实 APK 补充缺失类,或构建轻量 Dex 文件。此外,利用 RegisterNatives 钩子可监控动态注册的方法信息,辅助逆向分析。重点在于匹配类名、方法签名并返回正确的 DvmObject 类型以维持执行流程。
未来可期1 浏览 Unidbg 环境下解决 Native 调用 Java 方法报错问题
背景与目标
在使用 Unidbg 对 Android .so 进行离线执行时,常会遇到 native 函数内部调用 Java 方法(JNIEnv 回调)的情况。若未在 Unidbg 中提供对应的 Java 实现或模拟,通常会报出 "not implemented"、"FindClass 失败"、"GetMethodID 返回 null"、"类型不匹配" 等错误。
常见报错与原因
- UnsupportedOperationException:
callObjectMethod 等方法未实现,类/方法/签名未匹配。
- FindClass 失败: Jni 未正确处理 FindClass,或 vm.resolveClass 对应的类没有可用(未加载 APK/Dex 或未 stub)。
- GetMethodID 返回空: 签名不匹配尤为常见(注意 JNI 签名格式)。
- 崩溃: 返回类型不对,例如 native 期望 String,却返回了非 StringObject;或者签名是 ()I(返回 int),却返回了 DvmObject。
原理
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 就能得到期望值,继续执行。
解决方案
方案一:用 AbstractJni 自定义方法返回值(最常用)
- 优点:简单直接,适合没有完整 APK 的场景
- 做法:匹配类名/方法名/签名,返回合理的 DvmObject 或基础类型,让 native 满足条件分支即可
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.AndroidEmulatorBuilder;
com.github.unidbg.linux.android.AndroidResolver;
com.github.unidbg.memory.Memory;
com.github.unidbg.Module;
com.github.unidbg.linux.android.dvm.*;
com.github.unidbg.utils.Inspector;
{
AndroidEmulator emulator;
VM vm;
Module ;
{
emulator = AndroidEmulatorBuilder.for32Bit().build();
emulator.getMemory();
memory.setLibraryResolver( ());
vm = emulator.createDalvikVM();
vm.setVerbose();
vm.setJni( (vm));
vm.loadLibrary(, );
= dm.getModule();
dm.callJNI_OnLoad(emulator);
}
{
vm.resolveClass();
DvmObject<?> context = vm.resolveClass().newObject();
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator, , context
);
System.out.println( + result.getValue());
}
{
();
demo.callDoWork();
}
{
VM vm;
{
.vm = vm;
}
DvmClass {
vm.resolveClass(className);
}
DvmObject<?> callStaticObjectMethod(VM vm, DvmClass dvmClass, String methodName, String signature, VarArgs varArgs) {
dvmClass.getName();
(.equals(className) && .equals(methodName) && .equals(signature)) {
DvmObject<?> context = varArgs.getObjectArg();
(vm, );
}
( + className + + methodName + signature);
}
DvmObject<?> callObjectMethod(VM vm, DvmObject<?> dvmObject, String methodName, String signature, VarArgs varArgs) {
dvmObject.getObjectType().getName();
(.equals(className) && .equals(methodName) && .equals(signature)) {
(vm, );
}
( + className + + methodName + signature);
}
DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
dvmClass.getName();
(.equals(className) && .equals(fieldName) && .equals(signature)) {
(vm, );
}
(.equals(className) && .equals(fieldName) && .equals(signature)) {
(vm, );
}
.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
{
dvmClass.getName();
(.equals(className) && .equals(fieldName)) {
;
}
.getStaticIntField(vm, dvmClass, fieldName);
}
{
System.out.println( + className + + methods.length);
.registerNativeMethods(vm, dvmClass, className, methods);
}
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 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
import
import
import
import
import
public
class
UnidbgJniDemo
private
private
private
module
public
UnidbgJniDemo
()
Memory
memory
=
new
AndroidResolver
23
null
true
new
MyJni
DalvikModule
dm
=
"demo"
true
module
public
void
callDoWork
()
DvmClass
bridge
=
"com/example/NativeBridge"
"android/content/Context"
null
"doWork(Landroid/content/Context;)Ljava/lang/String;"
"doWork result = "
public
static
void
main
(String[] args)
UnidbgJniDemo
demo
=
new
UnidbgJniDemo
static
class
MyJni
extends
AbstractJni
private
final
public
MyJni
(VM vm)
this
@Override
public
findClass
(VM vm, String className)
return
@Override
public
String
className
=
if
"com/example/Device"
"getDeviceId"
"(Landroid/content/Context;)Ljava/lang/String;"
0
return
new
StringObject
"FAKE-DEVICE-ID-123456"
throw
new
UnsupportedOperationException
"callStaticObjectMethod not implemented: "
"."
@Override
public
String
className
=
if
"android/content/Context"
"getPackageName"
"()Ljava/lang/String;"
return
new
StringObject
"com.example.app"
throw
new
UnsupportedOperationException
"callObjectMethod not implemented: "
"."
@Override
public
String
className
=
if
"android/os/Build"
"MODEL"
"Ljava/lang/String;"
return
new
StringObject
"Pixel-FAKE-Model"
if
"android/os/Build"
"MANUFACTURER"
"Ljava/lang/String;"
return
new
StringObject
"Google"
return
super
@Override
public
int
getStaticIntField
(VM vm, DvmClass dvmClass, String fieldName)
String
className
=
if
"android/os/Build$VERSION"
"SDK_INT"
return
23
return
super
@Override
public
void
registerNativeMethods
(VM vm, DvmClass dvmClass, String className, DvmNativeMethod[] methods)
"RegisterNatives for "
", methods count="
super
方案二:加载 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=\"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);
}
}
}
Dex 生成参考
如需自行生成 Dex 文件,可使用 ASM 等工具编译 Class 字节码并打包为 Dex。