unidbg初识

Frida和Unidbg最大的区别是,Unidbg是模拟程序执行,所以可以绕过检测,而Frida面临众多检测,需要学会反检测。

其实在理解了Unidbg的原理之后,它还蛮简单的。

模拟层级

Unicorn模拟了底层cpu指令模拟,相当于一台裸机。当native代码调用到系统调用的指令(如x86的syscall、arch64的svc等),会将这些触发转发给AndroidEmulator进行处理。

AndroidEmulator主要负责模拟安卓环境和系统环境JNI交互,除此之外,它还负责处理Unicorn转发而来的系统调用

补环境实例讲解

可以观察下面的一个需要补环境的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.example.luodst;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
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.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.sql.SQLOutput;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class MainActivity extends AbstractJni {

public static void main(String[] args) {
MainActivity mainActivity = new MainActivity();
mainActivity.getHash();
}

// 模拟器创建
private final AndroidEmulator emulator;
// 创建java虚拟机
private final VM vm;

private final Module module;

private MainActivity() {
//1.创建Android模拟器实例
emulator = AndroidEmulatorBuilder
.for32Bit() // 32位虚拟机
.addBackendFactory(new Unicorn2Factory(true)) // 选择指令集,相当于选择哪台裸机,默认是unicorn
.setRootDir(new File("unidbg-android/src/test/java/com/example/luodst/rootfs")) // 设置文件系统根目录
.build(); // 创建

//2.获取操作内存的接口
Memory memory = emulator.getMemory();
//3.设置Android SDK 版本
LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver); // 加载sdk里的java类(可能会添加hook)
//4.创建java虚拟机,导入apk文件
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/example/luodst/files/DogPro.apk"));
//5.是否打印日志
vm.setVerbose(true);
//6.将自定义的JNI处理逻辑绑定到java虚拟机上
vm.setJni(this);
// 为java环境模拟器和java虚拟机注册内存
new AndroidModule(emulator, vm).register(memory);
//7.加载目标so文件,true主动执行init init_array
DalvikModule dm = vm.loadLibrary("dogpro", true);
//8.将so文件对应的Module存入成员变量
module = dm.getModule();
//9.主动调用JNI_OnLoad
dm.callJNI_OnLoad(emulator);
}

private void getHash() {
DvmObject<?> dvmObject = vm.resolveClass("com/example/dogpro/MainActivity").newObject(null);
System.out.println("dvmObject = "+ dvmObject.toString());
String input = "unidbg-android/src/test/java/com/example/luodst/files/DogPro.apk";
DvmObject<?> ret = dvmObject.callJniMethodObject(emulator, "getHash(Ljava/lang/String;)Ljava/lang/String;", input);
System.out.println("result ==> "+ret.getValue());
}



}

可以这么理解Unidbg补环境的过程,so文件作为elf二进制代码,可以在unicorn上面跑,但一些JNI函数,他们的参数列表需要JNI类型的数据,有时候甚至要调用一些Java层的函数(java层给native层数据时,数据类型是JNI类型),而Unidbg就是负责处理JNI交互的。

Unidbg有自己的一套JNI类型,其内置的Java虚拟机中还内置了很多自定义的基本类型、引用类型的数据结果,如:外部虚拟机中的String类型,在Unidbg中是StringObject类型;外部的int、boolean类型,是Unidbg中的DvmInteger和DvmBoolean;除此之外,大部分引用类型在Unidbg都视作DvmClass类型(继承于DvmObject<?>类型),这些引用类型的对象都属于DvmObject<?>类型。

1
2
DvmObject<?>:这是 Unidbg 中所有 Java 对象的基类,类似于真实 JVM 中的 java.lang.Object。它是一个泛型类,DvmObject<T> 的 T 通常表示对应的真实 Java 类型。例如,DvmObject<String> 表示一个 String 类型的对象。
DvmClass:表示 Java 中的类对象(java.lang.Class 的模拟)。在 Unidbg 中,DvmClass 继承自 DvmObject<Class<?>>,用来表示类的元信息(如类名、方法、字段等)。

image-20250320093443037

一般情况下,不需要补环境,是因为unidbg的虚拟机中内置了很多DvmClass(解析于SDK),也内置了很多自己的处理函数,操作这些DvmObject<?>,但遇到一些尚未解析的DvmClass或者无法解析的DvmClass的函数,就会抛出错误,这个时候就需要为它补环境了。

补环境的过程如下,简单来说,就是内置的类与函数不够用了,转而使用外部java虚拟机的类与函数,只将结果转换成DvmObject<?>返回给内部虚拟机。

image-20250320094827973

举个例子,下面这个报错是无法处理ZipFile的构造函数。

image-20250320093822376

一直定位追踪到下面这个函数,会发现没有针对于ZipFile类型的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "java/io/ByteArrayInputStream-><init>([B)V": {
ByteArray array = vaList.getObjectArg(0);
assert array != null;
return vm.resolveClass("java/io/ByteArrayInputStream").newObject(new ByteArrayInputStream(array.value));
}
case "java/lang/String-><init>([B)V": {
ByteArray array = vaList.getObjectArg(0);
assert array != null;
return new StringObject(vm, new String(array.value));
}
case "java/lang/String-><init>([BLjava/lang/String;)V": {
ByteArray array = vaList.getObjectArg(0);
assert array != null;
StringObject charsetName = vaList.getObjectArg(1);
assert charsetName != null;
try {
return new StringObject(vm, new String(array.value, charsetName.value));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
case "javax/crypto/spec/SecretKeySpec-><init>([BLjava/lang/String;)V":{
byte[] key = (byte[]) vaList.getObjectArg(0).value;
StringObject algorithm = vaList.getObjectArg(1);
assert algorithm != null;
SecretKeySpec secretKeySpec = new SecretKeySpec(key, algorithm.value);
return dvmClass.newObject(secretKeySpec);
}
case "java/lang/Integer-><init>(I)V": {
int i = vaList.getIntArg(0);
return DvmInteger.valueOf(vm, i);
}
case "java/lang/Boolean-><init>(Z)V":{
boolean b;
b = vaList.getIntArg(0) != 0;
return DvmBoolean.valueOf(vm, b);
}
}

throw new UnsupportedOperationException(signature);
}

这个时候就需要为它补环境了,如何补呢?这里的ZipFile并不是基本数据类型,在项目中也没有为它进行解析。

image-20250320095402681

因此需要将这个ZipFile类型解析到Unidbg的虚拟机中,再为它创建一个对象进行返回。

下面的代码中,vm.resolveClass负责解析ZipFile类,此时ZipFile类光荣的加入了Unidbg的虚拟机中,并重新定义为DvmObject<ZipFile>类,newObject返回了一个Unidbg内置的DvmObject<ZipFile>类对象。

这样便解解决了一个环境问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "java/util/zip/ZipFile-><init>(Ljava/lang/String;)V": {
// 正确提取 String 参数
StringObject pathObj = vaList.getObjectArg(0); // 内部虚拟机的String
assert pathObj != null;
String filePath = pathObj.getValue(); // 获取实际java类型的String

try {
// 创建虚拟的 ZipFile 对象(需提前在虚拟文件系统中补文件)
ZipFile zipFile = new ZipFile(filePath);
return vm.resolveClass("java/util/zip/ZipFile").newObject(zipFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}

还有一个补环境的例子,下面是报错,报错理由是缺少ZipFile的entries操作。

1
2
3
4
5
java.lang.UnsupportedOperationException: java/util/zip/ZipFile->entries()Ljava/util/Enumeration;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)
at com.github.unidbg.linux.android.dvm.DalvikVM$32.handle(DalvikVM.java:553)

一开始的补法是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/util/zip/ZipFile->entries()Ljava/util/Enumeration;": {
//拿操作的对象
ZipFile zipFile = (ZipFile) dvmObject.getValue();
//通过对象来调用方法
Enumeration<? extends ZipEntry> entries = zipFile.entries();
return vm.resolveClass("java/util/Enumeration").newObject(entries);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

但之后仍会报错,提示无法将DvmObject转换为Enumeration,为什么会出现这个问题?其实DvmObject<Enumeration>是我们补的一个类,但Unidbg中内置了一个Enumeration类,这个类不应该补的,因为负责与与native层交互的内置虚拟机,它默认是转换内置的Enumeration => JNI类型,而我们导入的这个类型会被忽视。

虽然如此,但return的这个对象很重要,这是我们通过外部虚拟机,要返回给内部虚拟机的一个Enumeration对象,如果我们返回的是它内置的com.github.unidbg.linux.android.dvm.Enumeration就没任何事,因为这是它缺少的对象,但我们返回的是com.github.unidbg.linux.android.dvm.DvmObject<Enumeration>,对象不一致,就要强转了,然后失败了。

1
2
3
4
5
java.lang.ClassCastException: class com.github.unidbg.linux.android.dvm.DvmObject cannot be cast to class com.github.unidbg.linux.android.dvm.Enumeration (com.github.unidbg.linux.android.dvm.DvmObject and com.github.unidbg.linux.android.dvm.Enumeration are in unnamed module of loader 'app')
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:610)
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:603)
at com.github.unidbg.linux.android.dvm.DvmMethod.callBooleanMethodV(DvmMethod.java:119)
at com.github.unidbg.linux.android.dvm.DalvikVM$35.handle(DalvikVM.java:630)

我的分析得到了grok的认可,哈哈哈哈。

image-20250320101751847

既然这样补不对,就需要看内置的Enumeration是如何创建的,可以看到,内置的Enumeration实现的俩函数,不过这里不是重点,重点是如何创建一个Enumeration对象。

通过构造函数,可以看到,需要输入一个外部java中的List对象,对象里的元素是DvmObject对象即可。

因此,我们需要创建一个List,然后把ZipFile对象里元素的类型(即ZipEntry)转换成DvmObject<ZipEntry>,然后再放入List中。

image-20250320101923116

这样就算补好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/util/zip/ZipFile->entries()Ljava/util/Enumeration;": {
ZipFile zipFile = (ZipFile) dvmObject.getValue();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
//return vm.resolveClass("java/util/Enumeration").newObject(entries);
List<DvmObject<?>> objs = new ArrayList<>();
while (entries.hasMoreElements()){
ZipEntry zipEntry = entries.nextElement();
objs.add(vm.resolveClass("java/util/zip/ZipEntry").newObject(zipEntry));
}
return new com.github.unidbg.linux.android.dvm.Enumeration(vm, objs);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

追踪读写

1
2
3
/* 监控读写 */
emulator.traceRead(module.base, module.base + module.size);
emulator.traceWrite(module.base, module.base + module.size);

读取内存

1
2
3
4
5
6
/* 读取内存 */
long targetAddr = module.base + 0xE0320;
UnidbgPointer ptr = UnidbgPointer.pointer(emulator, targetAddr);
byte[] data = ptr.getByteArray(0, 0xC0);

Inspector.inspect(data, "Dumped Memory at 0x" + Long.toHexString(targetAddr));