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; private final VM vm; private final Module module ; private MainActivity () { emulator = AndroidEmulatorBuilder .for32Bit () .addBackendFactory (new Unicorn2Factory (true )) .setRootDir (new File ("unidbg-android/src/test/java/com/example/luodst/rootfs" )) .build (); Memory memory = emulator.getMemory (); LibraryResolver resolver = new AndroidResolver (23 ); memory.setLibraryResolver (resolver); vm = emulator.createDalvikVM (new File ("unidbg-android/src/test/java/com/example/luodst/files/DogPro.apk" )); vm.setVerbose (true ); vm.setJni (this ); new AndroidModule (emulator, vm).register (memory); DalvikModule dm = vm.loadLibrary ("dogpro" , true ); module = dm.getModule (); 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<?>>,用来表示类的元信息(如类名、方法、字段等)。
一般情况下,不需要补环境,是因为unidbg的虚拟机中内置了很多DvmClass(解析于SDK),也内置了很多自己的处理函数,操作这些DvmObject<?>,但遇到一些尚未解析的DvmClass或者无法解析的DvmClass的函数,就会抛出错误,这个时候就需要为它补环境了。
补环境的过程如下,简单来说,就是内置的类与函数不够用了,转而使用外部java虚拟机的类与函数,只将结果转换成DvmObject<?>返回给内部虚拟机。
举个例子,下面这个报错是无法处理ZipFile的构造函数。
一直定位追踪到下面这个函数,会发现没有针对于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并不是基本数据类型,在项目中也没有为它进行解析。
因此需要将这个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" : { StringObject pathObj = vaList.getObjectArg(0 ); assert pathObj != null ; String filePath = pathObj.getValue(); try { 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的认可,哈哈哈哈。
既然这样补不对,就需要看内置的Enumeration是如何创建的,可以看到,内置的Enumeration实现的俩函数,不过这里不是重点,重点是如何创建一个Enumeration对象。
通过构造函数,可以看到,需要输入一个外部java中的List对象,对象里的元素是DvmObject对象即可。
因此,我们需要创建一个List,然后把ZipFile对象里元素的类型(即ZipEntry)转换成DvmObject<ZipEntry>,然后再放入List中。
这样就算补好了。
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(); 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));