Unidbg 简介
Unidbg 是一个基于 Unicorn 引擎的 Android 模拟执行框架,能够让我们在 无需真实 Android 设备 或 不启动 APK 的情况下执行 SO 库中的 JNI 方法。它主要用于:
- 逆向分析:解密、协议分析、破解加密算法。
- 自动化测试:在 PC 端模拟 JNI 调用,提高效率。
- 安全研究:分析恶意 APK 代码,检测安全漏洞。
核心概念
AndroidEmulator
AndroidEmulator
是 Unidbg 提供的 Android 进程模拟器,它负责创建 ARM 架构的运行环境,支持 32/64 位。
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.example.test")
.build();
Memory(虚拟内存管理)
Unidbg 通过 Memory
模块管理 so 库的加载和 Android 进程的内存映射。
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // 23 代表 Android 版本
DalvikVM(虚拟机)
DalvikVM
是 Unidbg 提供的 Java 层虚拟机,用于 JNI 交互。
VM vm = emulator.createDalvikVM();
vm.setJni(new AbstractJni() {});
vm.setVerbose(true); // 打开日志输出
DalvikModule Module(SO模块)
DalvikModule
负责加载 Java 层 DEX 代码,同时支持解析 JNI 调用。Module
代表一个已加载的so
文件,支持解析导出函数及调用。
DalvikModule dm = vm.loadLibrary(new File("path\\libxyz.so"), false);
Module module = dm.getModule();
##
如何编译
使用Maven编译,下载解压到本地目录后,进入到unidbg的源码目录,执行编译命令:
D:\maven\apache-maven-3.9.9\bin\mvn.cmd clean install -DskipTests
编译出错:
--- gpg:1.5:sign (default) @ unidbg ---
'gpg.exe' 不是内部或外部命令,也不是可运行的程序或批处理文件。
修改 unidbg根目录下的 pom.xml
文件,删除如下内容后重新编译即可。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
首次使用
AndroidStudio先随便创建一个安卓应用的工程,然后为工程添加模块,选择 Java or Kotlin Library
,根据向导填写基本信息,即可创建一个Java应用程序。
添加依赖库:
dependencies {
implementation 'com.github.zhkl0228:unidbg:0.9.8'
}
模拟调用
例如有一个so文件libxyz.so,它有一些注册函数,其中函数targetFunc是我们想要模拟调用的。
首先使用IDA查看该函数的偏移地址,如下,该偏移地址是:0x6A60。
.data:0001A6B8 DCD atargetFunc ; "targetFunc"
.data:0001A6BC DCD aLandroidConten_6 ; "(Landroid/content/Context;I[B[B)I"
.data:0001A6C0 DCD sub_6A60
编写代码:
package com.bigsing.unidbgdemo;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
public class MyClass extends AbstractJni {
final AndroidEmulator emulator;
final VM vm;
final Module module;
final DalvikModule dalvikModule;
MyClass() {
// 创建 Android ARM 32位模拟器
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.example.test")
.addBackendFactory(new Unicorn2Factory(true))
.build();
// 获取模拟内存并设置 Android 版本解析器
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // 23 代表 Android 版本
// 创建 Android 虚拟机
vm = emulator.createDalvikVM();
vm.setJni(this);
vm.setVerbose(true);
// 注册 Android 模块,确保 Unidbg 能正确解析系统库
new AndroidModule(emulator, vm).register(memory);
// 加载目标 so 文件
dalvikModule = vm.loadLibrary(new File("E:\\APPs\\libxyz.so"), false);
module = dalvikModule.getModule(); // 获取加载后的模块对象
}
public static void main(String[] args) {
String input = "这里是加密字符串...";
MyClass test = new MyClass();
test.callFuncByOffset(input);
test.callFuncByRegister(input);
}
// 通过 JNI 注册的方法调用
public void callFuncByRegister(String input) {
byte[] inData = input.getBytes(StandardCharsets.UTF_8);
final byte[] outData = new byte[4096];
ByteArray inByteArray = new ByteArray(vm, inData);
ByteArray outByteArray = new ByteArray(vm, outData);
// 执行 JNI_OnLoad 以初始化 JNI 相关的符号
dalvikModule.callJNI_OnLoad(emulator);
DvmClass nativeClass = vm.resolveClass("com/xyz/app/abcd");
try {
// 调用 targetFunc 方法
int len = nativeClass.callStaticJniMethodInt(emulator,
"targetFunc(Landroid/content/Context;I[B[B)I",
null,
1,
vm.addLocalObject(inByteArray),
vm.addLocalObject(outByteArray));
if (len > 0) {
byte[] out = outByteArray.getValue();
System.out.println("[Register Call] Decoded Text: " + new String(out, StandardCharsets.UTF_8));
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 通过 native 层函数偏移调用
public void callFuncByOffset(String input) {
Pointer env = vm.getJNIEnv(); // 获取 JNI 环境指针
byte[] inData = input.getBytes(StandardCharsets.UTF_8);
final byte[] outData = new byte[4096];
ByteArray inByteArray = new ByteArray(vm, inData);
ByteArray outByteArray = new ByteArray(vm, outData);
// 获取目标 Java 类对象的引用
DvmClass nativeClass = vm.resolveClass("com/xyz/app/abcd");
DvmObject<?> nativeClassObject = nativeClass.newObject(null);
int jclassRef = vm.addLocalObject(nativeClassObject); // 获取 Java 层的 jclass 句柄,也可以填 0
// 构造参数列表
ArrayList<Object> list_arg = new ArrayList<>();
list_arg.add(env); // JNIEnv*
list_arg.add(jclassRef); // jclass (静态方法调用)
list_arg.add(null); // Context (可选参数)
list_arg.add(1); // int 参数
list_arg.add(vm.addLocalObject(inByteArray)); // byte[] 输入数据
list_arg.add(vm.addLocalObject(outByteArray)); // byte[] 输出数据
// 调用目标 native 层函数,偏移地址 0x6A60
Number result = module.callFunction(emulator, 0x6A60, list_arg.toArray());
int len = result.intValue();
if (len > 0) {
byte[] out = outByteArray.getValue();
System.out.println("[Offset Call] Decoded Text: " + new String(out, StandardCharsets.UTF_8));
}
}
}
代码使用了两种方法来调用目标函数:按照函数偏移地址调用(callFuncByOffset
)、按照注册的jni函数调用(callFuncByRegister
)。
因为我们的目标函数恰巧被它的JNI_OnLoad
进行了注册,所以可以按照调用jni函数的方式进行调用,但是这个方法不够通用,特别是在目标函数没有被注册的情况下,这个时候就要通过函数偏移进行调用了。
函数的偏移地址已经要分析正确,否则调用肯定是不会成功的,这需要在IDA里进行分析。因为这个函数是被注册成jni函数了,通过运行日志也可以看出它的偏移是0x6a60
,也可以佐证在IDA里获取的地址是否正确。
JNIEnv->FindClass(com/xyz/app/abcd) was called from RX@0x40004e60[libxyz.so]0x4e60
JNIEnv->NewGlobalRef(class com/xyz/app/abcd) was called from RX@0x40004e80[libxyz.so]0x4e80
JNIEnv->FindClass(java/lang/String) was called from RX@0x40004ec0[libxyz.so]0x4ec0
JNIEnv->NewGlobalRef(class java/lang/String) was called from RX@0x40004ee0[libxyz.so]0x4ee0
JNIEnv->RegisterNatives(com/xyz/app/abcd, RW@0x4001a6a0[libxyz.so]0x1a6a0, 3) was called from RX@0x40004f2c[libxyz.so]0x4f2c
RegisterNative(com/xyz/app/abcd, targetFunc1(Landroid/content/Context;II)I, RX@0x40005cbc[libxyz.so]0x5cbc)
RegisterNative(com/xyz/app/abcd, targetFunc2(Landroid/content/Context;ILjava/lang/String;Ljava/lang/String;[BII)J, RX@0x4000646c[libxyz.so]0x646c)
RegisterNative(com/xyz/app/abcd, targetFunc(Landroid/content/Context;I[B[B)I, RX@0x40006a60[libxyz.so]0x6a60)
Find native function Java_com_xyz_app_abcd_targetFunc => RX@0x40006a60[libxyz.so]0x6a60
JNIEnv->GetArrayLength([B@496bc455 => 231) was called from RX@0x40006aa0[libxyz.so]0x6aa0
文档信息
- 本文作者:zhupite
- 本文链接:https://zhupite.com/sec/unidbg.html
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)