跳到主要内容Android 逆向:在 Unidbg 中解决 native 函数内调用 Java 方法的报错 | 极客日志Javajava
Android 逆向:在 Unidbg 中解决 native 函数内调用 Java 方法的报错
Android 逆向中使用 Unidbg 进行离线执行时,native 层调用 Java 方法常因缺少实现而报错。文章分析了常见错误类型如 FindClass 失败、GetMethodID 返回 null 及类型不匹配等问题,并提供了四种解决方案:通过 AbstractJni 子类自定义返回值、加载真实 APK 文件、使用轻量级 Dex 模拟类以及监控 RegisterNatives 动态注册。重点展示了如何通过拦截 JNI 回调返回伪造数据以绕过检测,确保 native 函数正常执行,适用于安全审计与逆向分析场景。
背景与目标
在使用 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 {
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);
}
}
}
方案二、加载 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 {
vm.resolveClass();
DvmObject<?> context = vm.resolveClass().newObject();
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
,
context
);
System.out.println( + result.getValue());
}
{
();
demo.executeNativeMethod();
}
{
VM vm;
{
.vm = vm;
}
DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
(.equals(dvmClass.getName()) && .equals(fieldName) && .equals(signature)) {
(vm, );
}
.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
{
(.equals(dvmClass.getName()) && .equals(fieldName)) {
;
}
.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(, );
= dm.getModule();
dm.callJNI_OnLoad(emulator);
}
[] generateLightDex() {
();
dexBuilder.addClass(, +
+
);
dexBuilder.addClass(, +
+
+
+
);
dexBuilder.build();
}
Exception {
();
(baos);
jar.putNextEntry( ());
jar.write(generateLightDex());
jar.closeEntry();
jar.putNextEntry( ());
jar.write(( +
+
+
+
+
).getBytes());
jar.closeEntry();
jar.close();
( ()) {
fos.write(baos.toByteArray());
}
}
{
vm.resolveClass();
DvmObject<?> context = vm.resolveClass().newObject();
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
,
context
);
System.out.println( + result.getValue());
}
{
();
demo.executeDoWork();
}
{
VM vm;
{
.vm = vm;
}
DvmObject<?> callObjectMethod(VM vm, DvmObject<?> dvmObject, String methodName, String signature, VarArgs varArgs) {
(.equals(dvmObject.getObjectType().getName()) && .equals(methodName) && .equals(signature)) {
(vm, );
}
.callObjectMethod(vm, dvmObject, methodName, signature, varArgs);
}
DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
(.equals(dvmClass.getName()) && .equals(fieldName) && .equals(signature)) {
(vm, );
}
.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() {
.findSymbol();
(registerNativesAddr == ) {
System.out.println();
;
}
emulator.getBackend().addHook( (), registerNativesAddr);
}
{
vm.resolveClass();
DvmObject<?> context = vm.resolveClass().newObject();
DvmObject<?> result = bridge.callStaticJniMethodObject(
emulator,
,
context
);
System.out.println( + result.getValue());
}
{
();
demo.executeDoWork();
}
{
{
backend.getRegisters()[];
backend.getRegisters()[];
backend.getRegisters()[];
backend.getRegisters()[];
vm.findClassByAddress(clazz).getName();
System.out.println( + className);
System.out.println( + methodCount);
methodsPtr;
( ; i < methodCount; i++) {
backend.readPointer(currentPtr);
backend.readCString(namePtr);
backend.readPointer(currentPtr + emulator.getPointerSize());
backend.readCString(sigPtr);
backend.readPointer(currentPtr + emulator.getPointerSize() * );
System.out.printf(, i + , name, signature, fnPtr);
currentPtr += emulator.getPointerSize() * ;
}
}
}
{
VM vm;
{
.vm = vm;
}
DvmObject<?> callStaticObjectMethod(VM vm, DvmClass dvmClass, String methodName, String signature, VarArgs varArgs) {
(.equals(dvmClass.getName()) && .equals(methodName) && .equals(signature)) {
(vm, );
}
.callStaticObjectMethod(vm, dvmClass, methodName, signature, varArgs);
}
DvmObject<?> callObjectMethod(VM vm, DvmObject<?> dvmObject, String methodName, String signature, VarArgs varArgs) {
(.equals(dvmObject.getObjectType().getName()) && .equals(methodName) && .equals(signature)) {
(vm, );
}
.callObjectMethod(vm, dvmObject, methodName, signature, varArgs);
}
DvmObject<?> getStaticObjectField(VM vm, DvmClass dvmClass, String fieldName, String signature) {
(.equals(dvmClass.getName()) && .equals(fieldName) && .equals(signature)) {
(vm, );
}
.getStaticObjectField(vm, dvmClass, fieldName, signature);
}
}
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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
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
void
executeNativeMethod
()
DvmClass
bridge
=
"com/example/NativeBridge"
"android/content/Context"
null
"doWork(Landroid/content/Context;)Ljava/lang/String;"
"Native method result: "
public
static
void
main
(String[] args)
ApkLoadedDemo
demo
=
new
ApkLoadedDemo
static
class
ApkJni
extends
AbstractJni
private
final
public
ApkJni
(VM vm)
this
@Override
public
if
"android/os/Build"
"MODEL"
"Ljava/lang/String;"
return
new
StringObject
"Pixel-5"
return
super
@Override
public
int
getStaticIntField
(VM vm, DvmClass dvmClass, String fieldName)
if
"android/os/Build$VERSION"
"SDK_INT"
return
30
return
super
"demo"
true
module
private
byte
DexBuilder
dexBuilder
=
new
DexBuilder
"com/example/NativeBridge"
"public class NativeBridge {\n"
" public static native String doWork(android.content.Context ctx);\n"
"}"
"com/example/Device"
"public class Device {\n"
" public static String getDeviceId(android.content.Context ctx) {\n"
" return \"LIGHT-DEX-DEVICE-ID\";\n"
" }\n"
"}"
return
private
void
generateLightApk
()
throws
ByteArrayOutputStream
baos
=
new
ByteArrayOutputStream
JarOutputStream
jar
=
new
JarOutputStream
new
JarEntry
"classes.dex"
new
JarEntry
"AndroidManifest.xml"
"<?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>"
try
FileOutputStream
fos
=
new
FileOutputStream
"light-app.apk"
public
void
executeDoWork
()
DvmClass
bridge
=
"com/example/NativeBridge"
"android/content/Context"
null
"doWork(Landroid/content/Context;)Ljava/lang/String;"
"doWork result: "
public
static
void
main
(String[] args)
LightDexImplementation
demo
=
new
LightDexImplementation
static
class
LightJni
extends
AbstractJni
private
final
public
LightJni
(VM vm)
this
@Override
public
if
"android/content/Context"
"getPackageName"
"()Ljava/lang/String;"
return
new
StringObject
"com.example.lightapp"
return
super
@Override
public
if
"android/os/Build"
"MODEL"
"Ljava/lang/String;"
return
new
StringObject
"LightDex-Device"
return
super
long
registerNativesAddr
=
module
"RegisterNatives"
if
0
"RegisterNatives symbol not found"
return
new
RegisterNativesHook
public
void
executeDoWork
()
DvmClass
bridge
=
"com/example/NativeBridge"
"android/content/Context"
null
"doWork(Landroid/content/Context;)Ljava/lang/String;"
"doWork result: "
public
static
void
main
(String[] args)
RegisterNativesMonitor
demo
=
new
RegisterNativesMonitor
class
RegisterNativesHook
extends
BlockHook
@Override
public
void
hook
(Backend backend, long address, int size, Object user)
long
envPtr
=
0
long
clazz
=
1
long
methodsPtr
=
2
int
methodCount
=
3
String
className
=
"RegisterNatives called for class: "
"Method count: "
long
currentPtr
=
for
int
i
=
0
long
namePtr
=
String
name
=
long
sigPtr
=
String
signature
=
long
fnPtr
=
2
"|-- Method %d: %s%s @ 0x%x%n"
1
3
static
class
MonitorJni
extends
AbstractJni
private
final
public
MonitorJni
(VM vm)
this
@Override
public
if
"com/example/Device"
"getDeviceId"
"(Landroid/content/Context;)Ljava/lang/String;"
return
new
StringObject
"MONITOR-DEVICE-ID"
return
super
@Override
public
if
"android/content/Context"
"getPackageName"
"()Ljava/lang/String;"
return
new
StringObject
"com.example.monitor"
return
super
@Override
public
if
"android/os/Build"
"MODEL"
"Ljava/lang/String;"
return
new
StringObject
"Monitor-Device"
return
super