Unidbg使用指南 - aeskkkey/reverse GitHub Wiki

个人笔记,转载请注明出处。


unidbg的github地址,基于unicorn来实现指令集的运算。

当前版本0.9.5-SNAPSHOT,其他版本一些[行数]可能对不上,自己查找。

一、基本使用

导入工程,下载maven依赖。使用IDEA编辑器的可以参考逆向工具之unidbg,这里不展开讲。

通用步骤如下:

//1. 构建一个模拟器实例
AndroidEmulator emulator = AndroidEmulatorBuilder
        .for32Bit() // 使用32位,相对来说,32位指令比64位更便于阅读,ida分析也一般分析32位
        .setProcessName("你的app进程名") // 设置进程名称
        // .addBackendFactory(new DynarmicFactory(true)) //添加后端,推荐使用Dynarmic,运行速度快,但并不支持某些新特性
        .setRootDir(new File("unidbg-android/src/test/java/tmp/root")) // 设置模拟器根目录,文件IO会在该目录下进行,否则在系统临时目录下进行
        .build(); // 生成模拟器

//2. 获取操作内存的接口
Memory memory = emulator.getMemory();

//3.设置Android SDK 版本,目前只支持19,23两个版本
memory.setLibraryResolver(new AndroidResolver(23));
 
//4.创建虚拟机
VM vm = emulator.createDalvikVM();
// 也可以用传入一个apk文件,来生成虚拟机;传入apk,unidbg会帮处理apk的签名、包名、版本号等Manifest信息
// VM vm = emulator.createDalvikVM(new Fil("apk文件路径名"))); 

//5.选择设置日志输出,vm的Verbose会输出JniEnv函数表的调用信息;其他类的信息通过apache的log4j日志模块查看
vm.setVerbose(true);
Logger.getLogger(ARM32SyscallHandler.class).setLevel(Level.DEBUG);

//6.添加IO处理
emulator.getSyscallHandler().addIOResolver(new MyIOResolver());

//7.添加对Java方法调用的处理
vm.setJni(new MyJni());

//8.加载ELF文件
DalvikModule dm = vm.loadLibrary(new File("so文件路径名"), false); 

//9.调用JNI_OnLoad
dm.callJNI_OnLoad(emulator);
 
//10.获取so模块
Module module = dm.getModule();
System.out.println("module base: 0x" + Long.toHexString(module.base) + ",size 0x" + Long.toHexString(module.size));

//11.调用native方法
DvmClass jniClazz = vm.resolveClass("com.example.temp.JNIUtils".replace(".", "/"));
DvmObject context = vm.resolveClass("android/content/Context").newObject(null);
Object ret = jniClazz.callStaticJniMethodObject(emulator, "getSign(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;", context, "data_str");

二、IO处理

第6步中添加了一个对IO的处理,当系统调用open, read, write, seek, mkdir, fstat等和文件相关指令时,都会调用到我们的IO处理里

//6.添加IO处理
emulator.getSyscallHandler().addIOResolver(new MyIOResolver());

查看open定义

unidbg是开源的,有使用不懂的地方,尽量多看源码。例如这里可以看一下open的实现,了解是如何调用到IOResolver的:

  1. 32位的cpu指令调用基本都在ARM32SyscallHandler这个类里,[62行]hook是对系统指令的hook;
  2. [137行]当NR=5时,为open调用;
  3. [1912行]从模拟器emulator里面读出文件路径pathname和操作码oflags
  4. 跳到UnixSyscallHandler[307行],先调用resolve找我们实现的Resolver,如果没有自己实现IO,再调用createDriverFileIO

实现一个IOResolver

模版如下:

public class MyIOResolver implements IOResolver {
    private static final Log log = LogFactory.getLog(MyIOResolver.class);
    private static final String ROOT = "unidbg-android/src/test/java/tmp/root";

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        if (log.isDebugEnabled()) {
            log.debug("openFile: " + pathname + ", " + Integer.toHexString(oflags));
        }
        if (pathname == null || pathname.isEmpty()) {
            return null;
        }
        if (("/proc/" + emulator.getPid() + "/status").equals(pathname)) {
            return FileResult.success(new ByteArrayFileIO(oflags, pathname, (
                    "Name:\tcom.example.temp\nState:\tr\nTgid:\t" + emulator.getPid() + "\nPid:\t" + emulator.getPid() + "\nTracerPid:\t0\n").getBytes()));
        }
        if ("/system/usr/share/zoneinfo/tzdata".equals(pathname)) {
          	return FileResult.success(new SimpleFileIO(oflags, new File(ROOT, pathname), pathname));
            // return FileResult.success(emulator.getFileSystem().createSimpleFileIO(new File(ROOT, pathname), oflags, pathname));
          	// 两个方法等效
        }
        if (("/data/user/0/com.example.temp/files").equals(pathname)){
          	return FileResult.success(new DirectoryFileIO(oflags, pathname, new File(ROOT, pathname)));
            // return FileResult.success(emulator.getFileSystem().createDirectoryFileIO(new File(pathname), oflags, pathname));
          	// 两个方法等效
        }
        return null;
    }
}

可供选择的文件类型大致分为几种:

  1. ByteArrayFileIO,只读文件:
  • 构造时传入文件内容bytes,供so读取;可以查看该类定义,当要进行write时,会抛出异常;

  • 通常像读/proc里面的信息,基本都可以用这个来构造,返回so想要的内容;

public class ByteArrayFileIO extends BaseAndroidFileIO {
    @Override
    public int write(byte[] data) {
        throw new UnsupportedOperationException();
    }
}
  1. SimpleFileIO,可读可写文件
  • 会在工程的实际路径里面构造一个文件,然后进行真实的读写IO;
  • 通常so会频繁对某个文件进行读写操作时,可以使用该类;
public class SimpleFileIO extends BaseAndroidFileIO implements NewFileIO {
    private synchronized RandomAccessFile checkOpenFile() { // 打开文件并进行读写
        try {
            if (_randomAccessFile == null) {
                FileUtils.forceMkdir(file.getParentFile());
                if (!file.exists() && !file.createNewFile()) {
                    throw new IOException("createNewFile failed: " + file);
                }
                _randomAccessFile = new RandomAccessFile(file, "rws");
                onFileOpened(_randomAccessFile);
            }
            return _randomAccessFile;
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }
}
  1. DirectoryFileIO,不可读不可写文件
  • 主要是文件夹、link等文件,so通常通过fstat查看这些文件信息,但是无法进行读写
public class DirectoryFileIO extends BaseAndroidFileIO {

    public enum DirentType {
        DT_FIFO(1), /* FIFO */
        DT_CHR(2), /* character device */
        DT_DIR(4), /* directory */
        DT_BLK(6), /* block device */
        DT_REG(8), /* regular file */
        DT_LNK(10), /* symbolic link */
        DT_SOCK(12), /* socket */
        DT_WHT(14); /* whiteout */
        private final byte type;
        DirentType(int type) {
            this.type = (byte) type;
        }
    }
}
  1. 其他文件类型,unidbg在package com.github.unidbg.linux.file下定义了很多文件类型模板,可以按需使用。
  • 例如如果so查看了/proc/${pid}/maps文件,可以使用MapsFileIO,里面定义了maps格式可以方便生成maps数据;
  • 例如如果so查看了系统文件,可以用DriverFileIO,里面有一些定义好的/dev下的文件;
  1. 自定义文件类型

通常可能会遇到要定制自定义文件处理类的情况。大部分文件处理都继承自BaseAndroidFileIO

public abstract class BaseAndroidFileIO extends BaseFileIO implements AndroidFileIO {}

再往上看,BaseFileIO继承自AbstractFileIO,实现了FileIO接口,接口里面抽象了几乎所有的文件IO操作,只需要按需实现其功能即可;

public interface FileIO {
    int SEEK_SET = 0;
    int SEEK_CUR = 1;
    int SEEK_END = 2;
    void close();
    int write(byte[] data);
    int read(Backend backend, Pointer buffer, int count);
    int pread(Backend backend, Pointer buffer, int count, long offset);
    int fcntl(Emulator<?> emulator, int cmd, long arg);
    int ioctl(Emulator<?> emulator, long request, long argp);
    FileIO dup2();
    int connect(Pointer addr, int addrlen);
    int bind(Pointer addr, int addrlen);
    int listen(int backlog);
    int setsockopt(int level, int optname, Pointer optval, int optlen);
    int sendto(byte[] data, int flags, Pointer dest_addr, int addrlen);
    int lseek(int offset, int whence);
    int ftruncate(int length);
    int getpeername(Pointer addr, Pointer addrlen);
    int shutdown(int how);
    int getsockopt(int level, int optname, Pointer optval, Pointer optlen);
    int getsockname(Pointer addr, Pointer addrlen);
    long mmap2(Emulator<?> emulator, long addr, int aligned, int prot, int offset, int length) throws IOException;
    int llseek(long offset, Pointer result, int whence);
    int recvfrom(Backend backend, Pointer buf, int len, int flags, Pointer src_addr, Pointer addrlen);
    String getPath();
}

最便捷的方式,就是继承已有文件模板,继承覆盖部分功能。

例如:如果希望实现对一个可读可写文件的处理,而且不希望在本地生成文件,所有IO都在内存里模拟处理,那么可以继承ByteArrayFileIO,把抛异常的write方法覆盖实现即可。

public class ReadWriteFile extends ByteArrayFileIO {
    @Override
    public int read(Backend backend, Pointer buffer, int count) {
        synchronized (lock) {
            return super.read(backend, buffer, count);
        }
    }

    @Override
    public int write(byte[] data) {
        if (data == null) {
            return 0;
        }
        if (log.isDebugEnabled()) {
            log.debug(path + " write: length " + data.length);
        }
        byte[] temp = new byte[bytes.length + data.length];
        synchronized (lock) {
            System.arraycopy(bytes, 0, temp, 0, bytes.length);
            System.arraycopy(data, 0, temp, bytes.length, data.length);
            bytes = temp;
            pos += data.length;
        }
        lastModified = System.currentTimeMillis();
        return data.length;
    }
}

三、JNI方法处理

第7步中添加了一个对JNI的处理,当调用到java方法时候,会到该类里面找实现。

//7.添加对Java方法调用的处理
vm.setJni(new MyJni());

DalvikVM处理JNI

vm.setVerbose(true);之后,可以看到JNIEnv的调用日志,直接工程里面全局搜索日志前缀字符串,可以发现是在com.github.unidbg.linux.android.dvm.DalvikVM里面处理了对JNIEnv通用函数的调用。

JNI函数table可以查阅oracle JNI Functions,对应的DalvikVM在[2430行]注册了这些函数。

final UnidbgPointer impl = svcMemory.allocate(0x3a4 + emulator.getPointerSize(), "JNIEnv.impl");
for (int i = 0; i < 0x3a4; i += 4) {
	impl.setInt(i, i);
}
impl.setPointer(0x10, _GetVersion);
impl.setPointer(0x18, _FindClass);
impl.setPointer(0x24, _ToReflectedMethod);
impl.setPointer(0x34, _Throw);
impl.setPointer(0x3c, _ExceptionOccurred);
impl.setPointer(0x44, _ExceptionClear);
impl.setPointer(0x4c, _PushLocalFrame);
impl.setPointer(0x50, _PopLocalFrame);
impl.setPointer(0x54, _NewGlobalRef);
...

例如CallStaticObjectMethod

Pointer _CallStaticObjectMethod = svcMemory.registerSvc(new ArmSvc() {
    @Override
    public long handle(Emulator<?> emulator) {
        RegisterContext context = emulator.getContext();
        UnidbgPointer clazz = context.getPointerArg(1);
        UnidbgPointer jmethodID = context.getPointerArg(2);
        if (log.isDebugEnabled()) {
            log.debug("CallStaticObjectMethod clazz=" + clazz + ", jmethodID=" + jmethodID);
        }
        DvmClass dvmClass = classMap.get(clazz.toIntPeer());
        DvmMethod dvmMethod = dvmClass == null ? null : dvmClass.getStaticMethod(jmethodID.toIntPeer());
        if (dvmMethod == null) {
            throw new BackendException();
        } else {
            VarArg varArg = ArmVarArg.create(emulator, DalvikVM.this, dvmMethod);
            DvmObject<?> obj = dvmMethod.callStaticObjectMethod(varArg); // 调用该方法
            if (verbose) {
                System.out.printf("JNIEnv->CallStaticObjectMethod(%s, %s(%s) => %s) was called from %s%n", dvmClass, dvmMethod.methodName, varArg.formatArgs(), obj, context.getLRPointer());
            }
            return addLocalObject(obj);
        }
    }
});

DvmMethod在调用方法前,会获取jni对象,最终返回的就是vm.setJni(new MyJni());里面传入的MyJni()对象。

    protected final Jni checkJni(BaseVM vm, DvmClass dvmClass) {
        Jni classJni = dvmClass.getJni();
        if (vm.jni == null && classJni == null) {
            throw new IllegalStateException("Please vm.setJni(jni)");
        }
        return classJni != null ? classJni : vm.jni;
    }

实现一个Jni处理类

unidbg的com.github.unidbg.linux.android.dvm.AbstractJni里处理了一部分常见的jni方法调用,我们只需继承这个类,按提示补充即可。

import com.github.unidbg.linux.android.dvm.AbstractJni;

public class MyJni extends AbstractJni {
}

可以直接运行工程,按照提示一步步来加上即可。例如如果报错

[13:13:09 900]  WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:467) - handleInterrupt intno=2, NR=-1073744353, svcNumber=0x129, PC=unidbg@0xfffe0324, LR=RX@0x40002d8d[libxx.so]0x2d8d, syscall=null
java.lang.UnsupportedOperationException: android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.getObjectField(AbstractJni.java:144)

那么就在MyJni里面补充

    @Override
    public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        switch (signature) {
            case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;":
                return new StringObject(vm, "/data/app/"+this.packageName+"-1/base.apk");
        }
      	// 调用父方法,父方法里如果没有就会抛异常
        return super.getObjectField(vm, dvmObject, signature);
    }

可以在AbstractJni类里面看到一些预定义的getPackageNamegetApplicationInfo等方法,如果构造vm时候,是使用apk来构造的,那么unidbg会帮你解析这些;

VM vm = emulator.createDalvikVM(new Fil("apk文件路径名")));

四、DvmClass和DvmObject

Dvm是DalvikVM的简写,对app里面出现的java类和对象,unidbg不可能会有其代码和实例,因此用了DvmClassDvmObject来包装指代,同时定义了StringObject, DvmInterge等基本包装类型。

使用JNI方法传入的对象,都必须是用Dvm来包装一次。例如如果app里面有函数

public class JNIUtils{
	public static native String getSign(Context context, String str);
}

那么调用时候,需要:

//1.声明一个DvmClass指代JNIUtils类
DvmClass jniClazz = vm.resolveClass("com.example.temp.JNIUtils".replace(".", "/"));
//2.声明一个DvmObject指代Context,StringObject指代String,DvmInteger指代int
// newObject给的参数是dvmObject里的value属性值,因为根本用不到value,所以直接给null就行
DvmObject context = vm.resolveClass("android/content/Context").newObject(null);
//3.调用函数
Object ret = jniClazz.callStaticJniMethodObject(emulator, "getSign(Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String;", context, "data_msg");

基本类型在调用时,会自动包装成Dvm的类型,因此上面例子字符串可以直接给。DvmObject类代码第[100行]定义了jni调用如下,可以看到,基本的String类型,会先自动转成StringObject,并且添加到vm的一个local引用里。调用结束之后会释放该引用。

    protected static Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object...args) {
        UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
        vm.addLocalObject(thisObj);
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(thisObj.hashCode());
        if (args != null) {
            for (Object arg : args) {
                if (arg instanceof Boolean) {
                    list.add((Boolean) arg ? VM.JNI_TRUE : VM.JNI_FALSE);
                    continue;
                } else if(arg instanceof Hashable) {
                    list.add(arg.hashCode()); // dvm object

                    if(arg instanceof DvmObject) {
                        vm.addLocalObject((DvmObject<?>) arg);
                    }
                    continue;
                } else if (arg instanceof String) {
                    StringObject str = new StringObject(vm, (String) arg);
                    list.add(str.hashCode());
                    vm.addLocalObject(str);
                    continue;
                } else if(arg instanceof byte[]) {
                    ByteArray array = new ByteArray(vm, (byte[]) arg);
                    list.add(array.hashCode());
                    vm.addLocalObject(array);
                    continue;
                }

                list.add(arg);
            }
        }
        return Module.emulateFunction(emulator, fnPtr.peer, list.toArray())[0];
    }

ArrayObject

如果函数参数是一个对象数组,例如如下,有两种方式。

public static native String getSign(Object... args);

第一种方式是用ArrayObject对象包装,但是每个元素都必须是DvmObject的子类:

// getSign("str_1", 1, context);
ArrayObject args = new ArrayObject(new StringObject(vm, "str_1"),
                DvmInteger.valueOf(vm, 1),
                vm.resolveClass("android/content/Context").newObject(new Object()));

第二种方式是使用ProxyDvmObject,两种方式等效。

ArrayObject args = ProxyDvmObject.createObject(
  vm, "str_1", 1, vm.resolveClass("android/content/Context").newObject(new Object()));

class和methodId自动创建

打开vm的日志之后,会发现so获取了很多Class和MethodID,但是这些都不需要在JniResolver里面定义,也不需要处理。

JNIEnv->FindClass(android/content/pm/PackageManager) was called from RX@0x40002b55[libxx.so]0x2b55
JNIEnv->FindClass(android/content/pm/PackageInfo) was called from RX@0x40002b65[libxx.so]0x2b65
JNIEnv->FindClass(android/content/pm/Signature) was called from RX@0x40002b75[libxx.so]0x2b75
JNIEnv->GetMethodID(android/app/Activity.getPackageManager()Landroid/content/pm/PackageManager;) => 0x6ef28d94 was called from RX@0x40002b8f[libxx.so]0x2b8f
JNIEnv->CallObjectMethod(android.app.Activity@4f47d241, getPackageManager() => android.content.pm.PackageManager@4c3e4790) was called from RX@0x40002b9f[libxx.so]0x2b9f
JNIEnv->GetMethodID(android/app/Activity.getPackageName()Ljava/lang/String;) => 0x1e74d6f4 was called from RX@0x40002bb9[libxx.so]0x2bb9
JNIEnv->CallObjectMethod(android.app.Activity@4f47d241, getPackageName() => com.jingdong.app.mall) was called from RX@0x40002bc9[libxx.so]0x2bc9

unidbg会在调用_FindClass(DalvikVM[49行])里自动创建该class的封装类DvmClass

    RegisterContext context = emulator.getContext();
    Pointer env = context.getPointerArg(0);
    Pointer className = context.getPointerArg(1);
    String name = className.getString(0);
    DvmClass dvmClass = resolveClass(name);//创建封装,添加到vm索引里
    long hash = dvmClass.hashCode() & 0xffffffffL;
    return hash;

DalvikVM的父类BaseVM里定义了classMap,用hash值索引。debug时候可以看到各个DvmClass的hash值

    final Map<Integer, DvmClass> classMap = new HashMap<>();
// debug查看   classMap = {HashMap@1677}  size = 19
// {Integer@1748} 2918 -> {DvmClass@1749} "class [B"
// {Integer@1750} 1187028615 -> {DvmClass@1751} "class java/security/cert/X509Certificate"
// {Integer@1752} -238452991 -> {DvmClass@1753} "class android/content/pm/ApplicationInfo"
// {Integer@1754} -578271390 -> {DvmClass@1755} "class android/content/pm/PackageManager"
// {Integer@1756} 608747430 -> {DvmClass@1649} "class com/jingdong/common/utils/BitmapkitUtils"
// {Integer@1757} 821642487 -> {DvmClass@1758} "class android/content/pm/PackageInfo"
// {Integer@1759} 495024495 -> {DvmClass@1760} "class java/security/cert/X509Extension"
// {Integer@1761} 1239130571 -> {DvmClass@1762} "class sun/security/util/DerEncoder"
// {Integer@1763} -615889765 -> {DvmClass@1764} "class android/content/pm/Signature"

同理methodId也是,通过方法名加变量名的signature字符串的hashcode来做索引值,生成一个DvmMethod;

可以在debug下,点开每个DvmClass可以看到methodMap的属性。

		private final Map<Integer, DvmMethod> methodMap = new HashMap<>();    
		private final Map<Integer, DvmMethod> staticMethodMap = new HashMap<>();

    int getMethodID(String methodName, String args) {
        String signature = getClassName() + "->" + methodName + args;
        int hash = signature.hashCode();
        if (log.isDebugEnabled()) {
            log.debug("getMethodID signature=" + signature + ", hash=0x" + Long.toHexString(hash));
        }
        if (vm.jni == null || vm.jni.acceptMethod(this, signature, false)) {
            if (!methodMap.containsKey(hash)) {
                methodMap.put(hash, new DvmMethod(this, methodName, args, false));
            }
            return hash;
        } else {
            return 0;
        }
    }

// debug查看 value = {DvmClass@1766} "class android/app/Activity"
//  vm = {DalvikVM@1654} 
//  superClass = null
//  interfaceClasses = {DvmClass[0]@1841} 
//  className = "android/app/Activity"
//  staticMethodMap = {HashMap@1843}  size = 0
//  methodMap = {HashMap@1844}  size = 3
//  -{Integer@1854} 510973684 -> {DvmMethod@1855} "android/app/Activity->getPackageName()Ljava/lang/String;"
//  -{Integer@1856} -2099175516 -> {DvmMethod@1857} "android/app/Activity->getApplicationInfo()Landroid/content/pm/ApplicationInfo;"
//  -{Integer@1858} 1861389716 -> {DvmMethod@1859} "android/app/Activity->getPackageManager()Landroid/content/pm/PackageManager;"
//  fieldMap = {HashMap@1845}  size = 0
//  staticFieldMap = {HashMap@1846}  size = 0
//  nativesMap = {HashMap@1847}  size = 0
//  jni = null
//  objectType = {DvmClass@1783} "class java/lang/Class"
//  value = null
//  DvmObject.vm = {DalvikVM@1654} 
//  memoryBlock = null

so调用java函数过程

总结一下上面so调用java函数的流程,例如调用类成员函数CallObjectMethod

  1. DalvikVM处理_FindClass,如果是没有定义的Class,那么生成一个DvmClass对象并添加到vm的classMap里面;
  2. DalvikVM处理_GetMethodID
    1. 根据classname的hashcode从vm的classMap里面拿到对应的dvmClass对象,如果没有则抛异常;
    2. 根据methodName和args,调用dvmClass对象的getMethodID(String methodName, String args)方法,生成一个方法hashcode和dvmMethod对象,并且存到dvmClass对象的methodMap里面;
  3. DalvikVM处理_CallObjectMethod
    1. 根据所给的class的hashCode,先从vm的classMap里面拿到对应的dvmClass对象,如果没有则抛异常;
    2. 根据所给的methodId,从dvmClass对象的methodMap里面去找对应的DvmMethod,如果没有则抛异常;
    3. 执行DvmMethod的callObjectMethod,先调用chekJni来获取对应的jni接口;
    4. 如果DvmMethod所对应的DvmClass有定义的Jni接口,那么使用该Jni接口来处理函数执行;
    5. 如果DvmClass没有实现Jni接口,那么使用vm的Jni接口来处理函数执行;
    6. 如果我们实现了vm的Jni接口,例如MyJni,那么就会调用相应函数执行。

五、ProxyClassFactory

上面流程3.5提到DvmClass自己的Jni接口,那什么时候会用到该接口呢?

对于一个app内的类,我们上面的做法是:用DvmClass来封装这个类,MethodID自动创建,我们只需要在JniResolver里面处理MethodID对应的方法即可。

另一种方法是,使用ProxyClassFactory,加载我们工程定义的同名类,并在该类里面定义app类的类方法。使用方法就是在通用步骤里,加上下面这行。

vm.setDvmClassFactory(new ProxyClassFactory());

ClassLoader

ProxyClassFactory的部分代码如下,显然我们在无参构造这个类的时候,使用了工程的ClassLoader(传递性)

public class ProxyClassFactory implements DvmClassFactory {

    protected final ProxyClassLoader classLoader;

    public ProxyClassFactory() {
        this(ProxyClassFactory.class.getClassLoader());
    }
  
    public ProxyClassFactory(ClassLoader classLoader) {
        this.classLoader = new ProxyClassLoader(classLoader);
    }

    @Override
    public DvmClass createClass(BaseVM vm, String className, DvmClass superClass, DvmClass[] interfaceClasses) {
        return new ProxyDvmClass(vm, className, superClass, interfaceClasses, classLoader, visitor);
    }
}

ProxyJni

创建DvmClass时候,创建的是ProxyDvmClass,主要完成两件事:

  1. DvmClass的value,由classLoader.loadClass(name)来完成,即加载了工程里的一个同名类class;
  2. 定义了DvmClass的jni,也就是上上面调用流程3.5提到DvmClass自己的Jni接口,用了ProxyJni来实现;ProxyJni通过反射调用的方式,来调用工程里面同名类的方法。
public class ProxyDvmClass extends DvmClass {

    protected ProxyDvmClass(BaseVM vm, String className, DvmClass superClass, DvmClass[] interfaceClasses, ProxyClassLoader classLoader, ProxyDvmObjectVisitor visitor) {
        super(vm, className, superClass, interfaceClasses, null);

        setJni(createJni(classLoader, visitor));

        try {
            this.value = classLoader.loadClass(getName()); // 使用classLoader来加载工程的同名类
        } catch (ClassNotFoundException ignored) {
        }
    }

    protected JniFunction createJni(ProxyClassLoader classLoader, ProxyDvmObjectVisitor visitor) {
        return new ProxyJni(classLoader, visitor); // 使用ProxyJni来处理jni函数调用
    }

}

定义了DvmClass的jni,调用该类的函数,就不会再走vm绑定的JniResolver了,而是会反射调用我们工程的同名类来实现。

对比

简单来说,

  • 使用默认的DvmClassFactory,unidbg会自动构建so里面FindClassGetMethodID使用到的DvmClassDvmMethod;当调用函数或者获取类属性时,调用vm绑定的JniResolver来实现。
  • 使用ProxyClassFactory,unidbg会通过反射调用,来构造和调用工程里同名类的对象和方法。

六、hook

没有详细去了解,这里列几个见到的用法:

HookZz

Module libc = null;
for (Module each : emulator.getMemory().getLoadedModules()) {
    if ("libc.so".equals(each.getPath())) {
        libc = each;
        break;
    }
}

HookZz zz = HookZz.getInstance(emulator);
zz.replace(libc.findSymbolByName("lrand48"), new ReplaceCallback() {
    @Override
    public HookStatus onCall(Emulator<?> emulator, long originFunction) {
        return HookStatus.LR(emulator, 0);
    }
});

zz.instrument(module.base + 0x18B8 | 1, new InstrumentCallback<Arm32RegisterContext>() {
    @Override
    public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) {
        int length = ctx.getR2Int();
        System.out.println("stack " + Arrays.toString(ctx.getStackPointer().getByteArray(0, 64)));
        System.out.println("hook: 0x18B8  length: " + new String(ctx.getR1Pointer().getByteArray(0, length)));
    }
});

zz.wrap(module.base + 0x10EA4 | 1, new WrapCallback<RegisterContext>() {
    @Override
    public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
        System.out.println("hook 10EA4 before key: " + Arrays.toString(ctx.getPointerArg(1).getByteArray(0, 40)));
        System.out.println("hook 10EA4 before type: " + ctx.getIntArg(2));
    }

    @Override
    public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
        System.out.println("hook 10EA4 after result: " + Arrays.toString(ret) + "\n");
    }
});

IxHook

IxHook xHook = XHookImpl.getInstance(emulator); // 加载xHook,支持Import hook,文档看https://github.com/iqiyi/xHook
xHook.register("libxx.so", "memcpy", new ReplaceCallback() {
    @Override
    public HookStatus onCall(Emulator<?> emulator, long originFunction) {
        RegisterContext ctx = emulator.getContext();
        System.out.println("stack " + getVector(ctx.getStackPointer().getByteArray(0, 64)));
        System.out.println("memcpy: " + Arrays.toString(ctx.getPointerArg(1).getByteArray(0, ctx.getIntArg(2))));
        return HookStatus.RET(emulator, originFunction);
    }
});
xHook.refresh(); // 使Import hook生效

七、debug

没有实际例子,这里简单介绍用法:

Emulator提供了获取debug的接口,默认attach是用DebuggerType.CONSOLE,开启调试方式如下:

Debugger MyDbg = emulator.attach();
MyDbg.addBreakPoint(module.base + 0xc6c9);

当运行到断点时,会在命令行窗口显示提示,此时可以输入指令来进行调试;help提供的命令如下:

c: continue
n: step over
bt: back trace

st hex: search stack
shw hex: search writable heap
shr hex: search readable heap
shx hex: search executable heap

nb: break at next block
s|si: step into
s[decimal]: execute specified amount instruction
s(blx): execute util BLX mnemonic, low performance

m(op) [size]: show memory, default size is 0x70, size may hex or decimal
mr0-mr7, mfp, mip, msp [size]: show memory of specified register
m(address) [size]: show memory of specified address, address must start with 0x

wr0-wr7, wfp, wip, wsp <value>: write specified register
wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x
wx(address) <hex>: write bytes to memory at specified address, address must start with 0x

b(address): add temporarily breakpoint, address must start with 0x, can be module offset
b: add breakpoint of register PC
r: remove breakpoint of register PC
blr: add temporarily breakpoint of register LR

p (assembly): patch assembly at PC address
where: show java stack trace

trace [begin end]: Set trace instructions
traceRead [begin end]: Set trace memory read
traceWrite [begin end]: Set trace memory write
vm: view loaded modules
vbs: view breakpoints
d|dis: show disassemble
d(0x): show disassemble at specify address
stop: stop emulation
run [arg]: run test
cc size: convert asm from 0x400018b8 - 0x400018b8 + size bytes to c function

八、一些常见坑

StringObject.toString()

unidbg源码里StringObject.toString()返回值会多一个双引号。

    @Override
    public String toString() {
        if (value == null) {
            return null;
        } else {
            return '"' + value + '"';
        }
    }

需要注意不要直接用toString就好

StringObject dvmStr = new StringObject(vm, "msg");
String value = "prefix," + dvmStr.getValue() + ",suffix";

当然为了方便,我直接把clone下来的工程源码里的双引号去掉了。

Find symbol failed

报错:

Find symbol open  failed: handle=0xffffffff...

错误发生在AndroidElfLoader类的dlsym函数[284行]

    @Override
    public Symbol dlsym(long handle, String symbolName) {
        for (LinuxModule module : modules.values()) {
            if (module.base == handle) { // virtual module may have same base address
                Symbol symbol = module.findSymbolByName(symbolName, false);
                if (symbol != null) {
                    return symbol;
                }
            }
        }
        if ("environ".equals(symbolName)) {
            return new VirtualSymbol(symbolName, null, environ.toUIntPeer());
        }
        return null;
    }

主要是传入的handle值为-1,导致无法找到对应的模块基址。至于为什么传入值为-1,目前没找到原因。解决办法是去掉module.base == handle的判断,直接默认不会有同名symbol导出;如果有同名symbol,自行判断处理。

Illegal JNI version

[21:26:44 798]  WARN [com.github.unidbg.AbstractEmulator] (AbstractEmulator:389) - emulate RX@0x4000374d[libxx.so]0x374d exception sp=unidbg@0xbffff5d8, msg=null, offset=40537ms
Exception in thread "main" java.lang.IllegalStateException: Illegal JNI version: 0xffffffff

在BaseVM里面抛出的异常,主要是callJNI_OnLoad失败了,如果没有什么问题,不影响后续调用(看app是否校验这个init成功),可以忽略。

		final void checkVersion(int version) {
        if (version != JNI_VERSION_1_1 &&
                version != JNI_VERSION_1_2 &&
                version != JNI_VERSION_1_4 &&
                version != JNI_VERSION_1_6 &&
                version != JNI_VERSION_1_8) {
            System.err.println("Illegal JNI version: 0x" + Integer.toHexString(version));
//            throw new IllegalStateException("Illegal JNI version: 0x" + Integer.toHexString(version));
        }
    }

继承问题

JNIEnv->NewGlobalRef(android.app.Activity@4f47d241) was called from RX@0x400037b7[libxx.so]0x37b7
JNIEnv->GetMethodID(android/content/Context.getPackageManager()Landroid/content/pm/PackageManager;) => 0x6ef28d94 was called from RX@0x40002b8f[libxx.so]0x2b8f
[17:00:22 852]  WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:467) - handleInterrupt intno=2, NR=-578271390, svcNumber=0x114, PC=unidbg@0xfffe01d4, LR=RX@0x40002b9f[libxx.so]0x2b9f, syscall=null
com.github.unidbg.arm.backend.BackendException
	at com.github.unidbg.linux.android.dvm.DalvikVM$21.handle(DalvikVM.java:405)

这个直接去看报错的DalvikVM.java:405,发现是获取DvmMethod为空导致的。

  1. so获取了一个Activity对象,解析其类名生成DvmClass activityClazz,并构造了一个DvmObject activity
  2. so获取了一个Context的getPackageManager()方法,methodID存在了DvmClass contextClazz的methodMap里面;
  3. so对DvmObject activity尝试调用这个methodID,但是找不到methodID。

在Android中,Activity继承Context,能调用Context函数很正常;但是在DvmClass封装这两个class时候,没有处理他们的继承关系。

解决办法一:不要传Activity dvm对象,直接给一个Context dvm对象

DvmObject activity = vm.resolveClass("android/content/Context").newObject(null);

方法二:处理他们的继承关系

DvmClass contextClz = vm.resolveClass("android/content/Context");
DvmObject activity = vm.resolveClass("android/app/Activity", contextClz).newObject(null);

方法三:使用ProxyClassFactory,定义Context和Activity并保持他们的继承关系。

参考issue-jmethodID投毒导致CallObjectMethod异常问题

long参数问题

long ->两个int

https://blog.csdn.net/qq_38851536/article/details/118122592

⚠️ **GitHub.com Fallback** ⚠️