Android逆向学习笔记——Unidbg学习与实战

  1. 1、unidbg介绍
    1. 1.1、unicorn介绍
    2. 1.2、基于unicorn开发的框架
    3. 1.3、unidbg
  2. 2、unidbg安装
    1. 2.1、项目地址
    2. 2.2、导入工程
    3. 2.3、工程结构
  3. 3、unidbg入门案例
    1. 3.1、代码基本框架
    2. 3.2、callStaticJniMethodObject
    3. 3.3、通过符号寻找函数地址的过程
    4. 3.4、对传入的参数进行包装的过程
    5. 3.5、unidbg入门案例
    6. 3.6、unidbg入门案例—md5调用
    7. 3.7、处理so调用系统Java类
    8. 3.8、处理so调用其他的so
    9. 3.9、将Unidbg的日志全开
    10. 3.10、通过符号调用函数
    11. 3.11、module.callFunction
  4. 4、unidbg中的hook
    1. 4.1、简介
    2. 4.2、hookzz
    3. 4.3、hookzz.instrument
    4. 4.4、参数的获取
    5. 4.5、hookZz.replace
    6. 4.6、原生UnicornHook
    7. 4.6、打印函数调用栈
    8. 4.7、unidbg动态调试
    9. 4.8、监控内存读写
    10. 4.9、unidbg—trace
    11. 4.10、处理so调用自写Java类
  5. 5、unidbg实践案例
    1. a、直接调用
    2. b、补环境
    3. c、mfw
    4. d、绕过is_initialised
  6. 6、unidbg中的VirtualModule
  7. 7、处理so与系统的交互
    1. 1、代码方式
    2. 2、使用rootfs虚拟文件系统
    3. 3、进程pid的获取与设置
    4. 4、unidbg中全局变量的设置与获取
    5. 5、arm32下的unidbg多线程
    6. 6、获取环境变量
      1. 6.1、unidbg提供了对环境变量的初始化
      2. 6.2、通过libc里的setenv设置环境变量
      3. 6.3、Hook getenv
    7. 7、HookListener
    8. 8、hook popen
  8. 8、Linux内核的Syscall
    1. 1、系统调用号与对应的函数
    2. 2、unidbg中对syscall的实现
    3. 3、unidbg的检测点
    4. 4、接管未处理的调用号

1、unidbg介绍

1.1、unicorn介绍

好比是一个CPU,可以模拟执行各种指令。提供了很多编程语言接口,可以操作内存、寄存器等,但它不是一个系统,内存管理、文件管理、系统调用等都需要自己来实现。

1.2、基于unicorn开发的框架

cemu 用来学习汇编的好工具

AndroidNativeEmu Python开发

unidbg Java开发

1.3、unidbg

支持模拟JNI调用

支持模拟系统调用指令

支持ARM32和ARM64

支持Hookzz、Dobby、xHook、原生unicorn Hook等Hook方式

支持Android、iOS

好比是在CPU上搭建了一个系统,因此可以很方便地在PC端模拟运行so

学习成本较低,不需要复现so算法,补环境后直接运行即可

2、unidbg安装

2.1、项目地址

安装地址:https://github.com/zhkl0228/unidbg

2.2、导入工程

从GitHub下载项目,解压后,使用IDEA打开

2.3、工程结构

unidbg-master -> unidbg-android -> src

​ main 工程源码

​ test 测试案例

​ java 测试案例的源码

​ resources 测试案例的资源

3、unidbg入门案例

3.1、代码基本框架

    TTEncrypt(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for32Bit()
                .setProcessName("com.qidian.dldl.official")
                .addBackendFactory(new Unicorn2Factory(true))
                .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 有19和23

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libttEncrypt.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数,看后面的操作是否依赖JNI_OnLoad
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块,可以调用下面的方法
			
        TTEncryptUtils = vm.resolveClass("com/bytedance/frameworks/core/encrypt/TTEncryptUtils");//加载类
    }


...
...
  
  
        if (logging) {
            emulator.attach(DebuggerType.ANDROID_SERVER_V7); // 附加IDA android_server,可输入c命令取消附加继续运行
        }
        byte[] data = new byte[16];
        ByteArray array = TTEncryptUtils.callStaticJniMethodObject(emulator, "ttEncrypt([BI)[B", new ByteArray(vm, data), data.length); // 执行Jni方法
        return array.getValue();
    }

TTEncryptUtils是前面加载进来的类,现在调用类中的方法,因为返回值是ByteArray数组,所以用callStaticJniMethodObject,第一个参数是模拟器,第二个参数是方法名A+签名,第三个参数是方法A的参数,data已经是ByteArray数组,但是需要通过unidbg库的转换才行,第四个参数也是方法A的参数。

3.2、callStaticJniMethodObject

该函数比callFunction多封装了一些代码

不需要自己寻找函数地址

不需要自己包装参数

底层都是调用callJniMethod

3.3、通过符号寻找函数地址的过程

我们得探究为什么这里callStaticJniMethodObject传入的是java层的函数名,而不是native层的函数名

我们在这里下断点进行调试

再往里面跟

调用了findNativeFunction,继续跟进

这里通过nativesMap.get获得了函数指针,跟过去看看nativesMap是什么

这里是JNI函数动态注册的部分,把注册过的函数相关信息放到了nativesMap中

在这个Map中,我们的函数名+签名组成了键,而so中的函数地址为值,所以可以直接通过get取出

不过有些函数不是动态注册的,这样找不到的函数,则按静态注册规则拼接符号,然后寻找函数地址

这里也可以看出来我们之前为什么要传入TTEncrypt完整的类路径,这样才能找得到,图中就是静态注册时的函数名

3.4、对传入的参数进行包装的过程

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 DvmAwareObject ||
                        arg instanceof String ||
                        arg instanceof byte[] ||
                        arg instanceof short[] ||
                        arg instanceof int[] ||
                        arg instanceof float[] ||
                        arg instanceof double[] ||
                        arg instanceof Enum) {
                    DvmObject<?> obj = ProxyDvmObject.createObject(vm, arg);
                    list.add(obj.hashCode());
                    vm.addLocalObject(obj);
                    continue;
                }

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

这里面主要是对参数进行封装,创建了一个object的list,加入了vm.getJNIEnv(),thisObj的传入需要自身和hashcode作为索引

这部分是对传入参数的遍历和分析

3.5、unidbg入门案例

package com.xiaojianbang.ndk;
import com.alibaba.fastjson.util.IOUtils;
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.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
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.utils.Inspector;

import java.io.File;
public class NativeHelper {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass NativeHelper;

    private final boolean logging;

    NativeHelper(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for64Bit()
                .setProcessName("com.xiaojianbang.app")
                .addBackendFactory(new Unicorn2Factory(true))
                .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg/unidbg-android/src/test/java/com/xiaojianbang/ndk/libxiaojianbang.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
       // dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块

        NativeHelper = vm.resolveClass("com/xiaojianbang/ndk/NativeHelper");//加载类
    }

    void destroy() {
        IOUtils.close(emulator);
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        NativeHelper test = new NativeHelper(true);
        int revtal = test.callFunc();
        System.out.println("revtal:0x"+Integer.toHexString(revtal));
        test.destroy();
    }

    int callFunc() {
        int retval = NativeHelper.callStaticJniMethodInt(emulator,"add(III)I",0x100,0x200,0x300); // 执行Jni方法
        return retval;
    }

}

对so中的add方法进行调用

静态注册的字符串拼接,遍历模拟器中加载的so文件,找到xiaojianbang.so

3.6、unidbg入门案例—md5调用

md5也是静态注册,所以我们按照上面的代码,只需要改callFunc就行了

void  callFunc() {
     StringObject tt =  NativeHelper.callStaticJniMethodObject(emulator,"md5(Ljava/lang/String;)Ljava/lang/String;","whitebird"); // 执行Jni方法
     System.out.println("md5encode:"+tt.getValue());
}

运行后发现报错,分析错误日志

找到了md5,但是函数内部执行时,发生了错误,我们用ida看看

在callObjectMethodV发生了错误,提示让我们执行Please vm.setJni(jni)

这样就可以了

底层实现

3.7、处理so调用系统Java类

1、unidbg实现了大部分的JNI函数,对于没有实现的,需要自己来实现

2、已经实现的,类似callObjectMethodV,需要自己分析so,做有针对性的覆写

3、通过vm.setjni(this)覆写父类AbstractJni方法

4、如果某so通过JNI访问比较多的Java类,或者IDA反编译的so,逻辑不是很清楚,比如存在混淆,这时可以借助jnitrace来补

vm.setJni(new AbstractJni(){
         @Override
         public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
             System.out.println("signature:"+signature);
             System.out.println("dvmObject:"+dvmObject.getValue());
             System.out.println("vaList:"+vaList);
             if (signature.equals("java/lang/String->getBytes(Ljava/lang/String;)[B")){
                 String args=(String)  dvmObject.getValue();
                 byte [] bytes=args.getBytes();
                 return new ByteArray(vm,bytes);

             }
             return super.callObjectMethodV(vm, dvmObject, signature, vaList);
         }
     });

其实就是自己去处理一些unidbg还没实现的JNI函数

3.8、处理so调用其他的so

encode函数是动态注册的,如果我们不调用JNI_OnLoad,就会找不到encode

void  callFunc() {
     StringObject tt =  NativeHelper.callStaticJniMethodObject(emulator,"encode()Ljava/lang/String;"); // 执行Jni方法
     System.out.println("encode:"+tt.getValue());
}

如果我们此时先调用JNI_Load再看看执行结果

此时又报了一个错误

[main]D/xiaojianbang: myInit是正常执行打印的

注意这个导入函数

所以这也是导致报错的原因,所以我们要手动加载bssFunc所在的so

现在再运行一下试试

1、如果被调用的函数,需要先执行JNI_OnLoad或者其他函数,那就按顺序调用

2、 如果so中调用了其他的so,只需按顺序加载所需的so即可

3 、dlopen会自己处理,因为unidbg加载了libdl.so

4 、C/C++标准库也会自己处理,因为unidbg加载了libc.so、libc++.so

3.9、将Unidbg的日志全开

src/test/resources/log4j.properties中INFO全改成DEBUG

3.10、通过符号调用函数

在findNativeFunction中先通过 nativesMap.get(method);获取函数指针,如果没得到就拼装JNI类型的函数名通过module.findSymbolByName(symbolName, false)获得symbol,然后symbol.createPointer(emulator)获取函数指针

但是symbol中有个call方法也可以直接调用函数

先以add为例

我们由于直接从Module开始,而module.findSymbolByName是传入JNI类型的函数名了,所以我们要在so中找

void  callFunc() {
    Symbol symbol = module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add");
    Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper),100,200,300);
    int ret =numbers.intValue();
    System.out.println("findSymbolByName:"+ret);
}

先通过函数名找到symbol,然后调用symbol.call,第一个填入emulator,后面就是对应Java_com_xiaojianbang_ndk_NativeHelper_add的参数,第一个是JNIEnv,第二个是Jclass,但是要用addLocalObject将dvmclass转成jclass,后面三个就是真正要想加的值。返回值放到了numbers里,通过intValue拿到

我们再试一下_Z7_strcatP7_JNIEnvP7_jclass

void  callFunc() {
    Symbol symbol = module.findSymbolByName("_Z7_strcatP7_JNIEnvP7_jclass");
    Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper));
    int ret =numbers.intValue();
    System.out.println("_Z7_strcatP7_JNIEnvP7_jclass:"+    vm.getObject(ret).getValue());//相当于vm.addLocalObject的反过程
}

这次numbers.intValue()的返回值不是直接得到结果了,而是一个哈希值,然后通过vm.getObject,传入哈希值,得到对象,最后调用getValue获得结果。因此直接获得结果只适用于基本数据类型。

3.11、module.callFunction

可以看到module.callFunction其实就是帮我们完成了上面的操作,通过findSymbolByName找符号,然后利用

symbol.call调用

void  callFunc() {
    Symbol symbol = module.findSymbolByName("_Z7_strcatP7_JNIEnvP7_jclass");
    Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper));
    int ret1 =numbers.intValue();
    System.out.println("_Z7_strcatP7_JNIEnvP7_jclass1:"+    vm.getObject(ret1).getValue());
    Number number = module.callFunction(emulator, "_Z7_strcatP7_JNIEnvP7_jclass", vm.getJNIEnv(), vm.addLocalObject(NativeHelper));
    int ret2 =number.intValue();
    System.out.println("_Z7_strcatP7_JNIEnvP7_jclass2:"+    vm.getObject(ret2).getValue());

}

callFunction还有个通过偏移调用的方法,只需传入偏移地址,unidbg会自动加上so的基址

Number number1 = module.callFunction(emulator, 0x1B4C, vm.getJNIEnv(), vm.addLocalObject(NativeHelper));
  int ret3 =number1.intValue();
  System.out.println("_Z7_strcatP7_JNIEnvP7_jclass3:"+    vm.getObject(ret3).getValue());

//MD5Init(MD5_CTX *)
UnidbgPointer MD5Ctxpointer = emulator.getMemory().malloc(200, false).getPointer();
module.callFunction(emulator,0x2230,MD5Ctxpointer);

//MD5Update(MD5_CTX *, unsigned char *, unsigned int)
UnidbgPointer plainText = emulator.getMemory().malloc(200,false).getPointer();
byte[] buffer="whitebird_unidbg".getBytes();
plainText.write(buffer);
module.callFunction(emulator,0x22A0,MD5Ctxpointer,plainText,buffer.length);

//MD5Final(MD5_CTX *, unsigned char *)
UnidbgPointer cipherText = emulator.getMemory().malloc(200,false).getPointer();
module.callFunction(emulator,0x3A78,MD5Ctxpointer,cipherText);
byte[] byteArray=cipherText.getByteArray(0,16);
Inspector.inspect(byteArray,"MD5Result");

上面代码是调用MD5算法

C语言类型的传递,通过操作内存处理

emulator.getMemory().malloc(200, false).getPointer()
memory下属有很多操作内存的方法
pointer下属有很多操作内存的方法
MD5_CTX不需要特别定义,只需给它足够的内存即可,结构体实际上就是一段连续的内存,结构体中存在内存对齐

4、unidbg中的hook

4.1、简介

  1. unidbg支持dobby、hookzz、whale、xhook

  1. hookzz是dobby的前身,hookzz对32位支持较好,dobby对64位支持较好

  2. unidbg支持Unicorn自带的Hook(指令级Hook,块级Hook,内存读写Hook,异常Hook)以及unidbg封装后的console debugger

  3. 原生unicorn hook不容易被检测,console debugger可下多个断点,用于快速验证

  4. unidbg没法处理子线程中的操作,Hook相应位置,把子线程计算结果赋值给寄存器

4.2、hookzz

可以通过符号Hook,也可以通过地址Hook

可以发现其实两种wrap的hook最后走的都是同一个流程

void  callFunc() {
  
    IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
    hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
    hookZz.wrap(module.findSymbolByName("_Z9MD5UpdateP7MD5_CTXPhj"), new WrapCallback<RegisterContext>() {
        @Override
        public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
            md5_ctx = ctx.getPointerArg(0);
            Pointer plainText = ctx.getPointerArg(1);
            int length = ctx.getIntArg(2);
            Inspector.inspect(md5_ctx.getByteArray(0,64),"preCall md5_ctx");
            Inspector.inspect(plainText.getByteArray(0,length),"plainText");
        }

        @Override
        public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
            Inspector.inspect(md5_ctx.getByteArray(0,64),"postCall md5_ctx");
        }
    });
   hookZz.disable_arm_arm64_b_branch();

    //MD5Init(MD5_CTX *)
    UnidbgPointer MD5Ctxpointer = emulator.getMemory().malloc(200, false).getPointer();
    module.callFunction(emulator,0x2230,MD5Ctxpointer);

    //MD5Update(MD5_CTX *, unsigned char *, unsigned int)
    UnidbgPointer plainText = emulator.getMemory().malloc(200,false).getPointer();
    byte[] buffer="whitebird_unidbg".getBytes();
    plainText.write(buffer);
    module.callFunction(emulator,0x22A0,MD5Ctxpointer,plainText,buffer.length);

    //MD5Final(MD5_CTX *, unsigned char *)
    UnidbgPointer cipherText = emulator.getMemory().malloc(200,false).getPointer();
    module.callFunction(emulator,0x3A78,MD5Ctxpointer,cipherText);
    byte[] byteArray=cipherText.getByteArray(0,16);
    Inspector.inspect(byteArray,"MD5Result");
}

4.3、hookzz.instrument

也是有两种方式,可以通过符号名或者函数地址进行hook

我们以add为例,去hook寄存器中的值

 IHookZz hookZz = HookZz.getInstance(emulator);
    hookZz.instrument(module.base + 0x1AEC, new InstrumentCallback<Arm64RegisterContext>() {
        @Override
        public void dbiCall(Emulator<?> emulator, Arm64RegisterContext ctx, HookEntryInfo info) {

            System.out.println("w8=0x"+Integer.toHexString(ctx.getXInt(8))+"   w9=0x"+Integer.toHexString(ctx.getXInt(9)));
        }
    });
    Symbol symbol = module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add");
    Number numbers = symbol.call(emulator, vm.getJNIEnv(),vm.addLocalObject(NativeHelper),0x100,0x200,0x300);
    int ret =numbers.intValue();
    System.out.println("findSymbolByName:"+Integer.toHexString(ret));

}

注意只有Arm64RegisterContext里有getXInt,里面的序号就是寄存器序号

4.4、参数的获取

Symbol symbol = module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring");
     Number number = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(new StringObject(vm, "whitebird 123")));
     long cStringAddr = number.intValue();
     byte[] bytes = emulator.getMemory().pointer(cStringAddr).getByteArray(0, 16);
     Inspector.inspect(bytes, "cStringAddr:");

通过hook获取参数

IHookZz hookZz = HookZz.getInstance(emulator);
      hookZz.wrap(module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring"), new WrapCallback<RegisterContext>() {
          @Override
          public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
              int intArg=ctx.getIntArg(1);//得到第二个参数,是hash值,按照之前的getObject取到对象
              StringObject stringObject=vm.getObject(intArg);
              System.out.println("preCall:"+stringObject.getValue());
          }
          @Override
      public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
         byte[] bytes = ctx.getPointerArg(0).getByteArray(0,16);//其实这里getPointerArg就是访问0号寄存器,猜测可能0号寄存器类似x86中的eax寄存器,用于存放函数调用完的返回结果
         Inspector.inspect(bytes,"postCall:");
      }
      });

IHookZz hookZz = HookZz.getInstance(emulator);
    hookZz.wrap(module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring"), new WrapCallback<HookZzArm64RegisterContext>() {
        @Override
        public void preCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
            int intArg=ctx.getIntArg(1);//得到第二个参数
            StringObject stringObject=vm.getObject(intArg);
            System.out.println("preCall:"+stringObject.getValue());
        }
        @Override
    public void postCall(Emulator<?> emulator, HookZzArm64RegisterContext ctx, HookEntryInfo info) {
       String str = ctx.getPointerArg(0).getString(0);
            System.out.println("getString:"+str);
            int hashcode=vm.addGlobalObject(new StringObject(vm,"this is set_Ret_Result"));
            ctx.setXLong(0,hashcode);
    }
    });

    Symbol symbol = module.findSymbolByName("_Z12jstring2cstrP7_JNIEnvP8_jstring");
    Number number = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(new StringObject(vm, "whitebird 123")));
    int hashcode=number.intValue();
    StringObject stringObject=vm.getObject(hashcode);
    System.out.println("this is set_Ret_Result:"+stringObject.getValue());

}

通过HookZzArm64RegisterContext下面的setXLong可以设置寄存器的值,这里对返回值设置成了jString的hashcode,最后通过getObject得到StringObject,调用getValue进行打印

4.5、hookZz.replace

可以用来直接替换要hook的函数

  IHookZz hookZz = HookZz.getInstance(emulator);
      hookZz.replace(module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add"), new ReplaceCallback() {
          @Override
          public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
              System.out.println("whitebird hook this function");
           	 //return HookStatus.RET(emulator,originFunction);
              return super.onCall(emulator, context, originFunction);
          }
});
   Symbol symbol = module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_add");
  Number number = symbol.call(emulator,vm.getJNIEnv(),vm.addLocalObject(NativeHelper),0x100,0x200,0x300);
  int result =number.intValue();
    System.out.println("add result :"+Integer.toHexString(result));

还可以通过return HookStatus.LR(emulator,0x100);修改返回值

4.6、原生UnicornHook

emulator.getBackend().hook_add_new(new CodeHook() {
    @Override
    public void hook(Backend backend, long address, int size, Object user) {
        RegisterContext context=emulator.getContext();
        if (address==module.base+0x1FF4){
            Pointer MD5_CTX=context.getPointerArg(0);
            Inspector.inspect(MD5_CTX.getByteArray(0,32),"MD5_CTX");
            Pointer plainText=context.getPointerArg(1);
            int length=context.getIntArg(2);
            Inspector.inspect(plainText.getByteArray(0,length),"plainText");
        }else if (address==module.base+0x2004){
            Pointer ciperText=context.getPointerArg(1);
            Inspector.inspect(ciperText.getByteArray(0,16),"ciperText");
        }
    }

    @Override
    public void onAttach(UnHook unHook) {

    }

    @Override
    public void detach() {

    }
},module.base+0x01FF4,module.base+0x2004,null);
Symbol symbol=module.findSymbolByName("Java_com_xiaojianbang_ndk_NativeHelper_md5");
Number number=symbol.call(emulator,vm.getJNIEnv(),vm.addLocalObject(NativeHelper),vm.addLocalObject(new StringObject(vm,"whitebird_getBackend")));

通过emulator.getBackend().hook_add_new进行hook

  1. 基于原生Unicorn API进行Hook时,不需要考虑是否 + 1,会自己转换
  2. Unicorn原生的Hook功能强大,而且不容易被检测
  3. Unicorn原生API进行inline hook

4.6、打印函数调用栈

emulator.getUnwinder().unwind()

4.7、unidbg动态调试

1、基于unicorn的console debugger同样不用管地址是否 + 1,会自己转换

2、附加下断点

Debugger debugger = emulator.attach();
debugger.addBreakPoint(module.base + 0x1AF4);
debugger.addBreakPoint(module.base + 0x1AEC);

3、emulator.attach支持几种调试方式,默认是console debugger

4、类似IDA动态调试,断点触发后,会显示寄存器信息,汇编指令

5、可以通过输入命令进行打印内存、写寄存器、跳到下一个断点、打印函数栈等操作

6、回车两下或者随便输错一个指令,就会打印出命令的帮助信息

b用于下断点
blr、b0x40228000
m用于读内存
	mr0、mx0、m0x40228000、msp
bt用于查看函数栈
c 跳到下一个断点
s 单步调试

当断下来的时候,会打印当前寄存器的值和指令

回车两次后弹出指令介绍

后面都有注释,这里就不展开说了

查看函数栈

4.8、监控内存读写

1、通过监控内存读写,可以从算法计算的结果,一步步定位到操作内存的关键函数

2、将信息输出到文件

String traceFile = "yourpath";

PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);

3、 监控内存读写

emulator.traceRead(module.base, module.base + module.size).setRedirect(traceStream);
emulator.traceWrite(module.base, module.base + module.size).setRedirect(traceStream);

4、每条读写,unidbg提供这些信息

写的位置、写了几个字节、写入什么值、发生写操作的地址、lr的值(程序的返回地址)

5、traceRead/traceWrite,也可以添加监听回调,打印调用栈

emulator.traceWrite(0x402e4009, 0x402e4052, new TraceWriteListener() {
    @Override
	public boolean onWrite(Emulator<?> emulator, long address, int size, long value) {
        emulator.getUnwinder().unwind();
        return false;
    }
});

代码测试:

String traceFile = "outputlog.txt";
PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);
emulator.traceRead(module.base, module.base + module.size).setRedirect(traceStream);
emulator.traceWrite(module.base, module.base + module.size).setRedirect(traceStream);
//地址是从module.base到module.base+module.size,说明监控了整个so文件

测试结果:

以第一条为例,读的内存地址是0x40005f60,去ida看一下偏移0x5F60的地方

执行读的地址为0x1570,其实就是调用0x5F60函数的地址

返回值是0x1E34,也就是_Z6myInitv调用完的下一行指令地址

4.9、unidbg—trace

1、代码写法

String traceFile = "yourpath";
PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true);
emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);

我们以add方法为例

再看一下ida里面的汇编指令

2、源码修改

由于未修改的trace打印的东西比较少,如果我们想要打印每一条指令执行时的寄存器值,就需要对源码进行修改。

在项目中找到类AbstractARM64Emulator

这里就是控制打印的地方,我们在这修改

通过ARM下面的方法进行打印寄存器值,showRegs64和showRegs都需要两个参数,第一个参数很简单,但是第二个我们暂时还不清楚怎么传,先看看showThumbRegs

showThumbRegs其实是已经被封装好了,我们跟进去看看ARM.THUMB_REGS,上下翻找可以发现

我们模仿去实现一下

builder.append(ARM.showRegs64(),ARM64_REGS);

由于ARM64_REGS是私有的,所以会报错,因此我们再模仿showThumbRegs实现下

public static String showARM64ExportRegs(Emulator<?> emulator){
      return showRegs64(emulator,ARM.ARM64_REGS);
  }

由于我们直接在append中调用的showARM64ExportRegs,所以需要返回String类型,而且showRegs64返回的是void,所以也要进行修改。

并且在最后返回String类型

现在执行一下看看

已经成功打印了,但是很乱,而且有很多寄存器的值都没有变化,所以我们还需要进一步修改。

首先复制一份新的ARM64_REGS,防止修改源码后,别的方法再调用时会有影响,由于很多寄存器用不到,我们可以直接注释掉

同理复制一份showRegs64

要修改的地方如下:

创建一个HashMap用来存储寄存器的状态

魔改的部分如下,现在同理要把x0-x10都加入判断语句,其他的寄存器由于被注释了,因此不会被调用,所以我们就不用修改了。

现在已经成功实现了

这里的结果都是上一条指令执行完后产生的。

4.10、处理so调用自写Java类

  1. 将自写的Java类,放到unidbg工程中

  2. 包名最好与原包名一致,避免歧义

  3. 类中有用到android相关类,需要用Java去实现

  4. 代码不需要完全一致,只需函数处理结果符合预期即可

  5. 访问修饰符的问题

JNI调用Java函数不需要理会访问修饰符

unidbg用Java开发,在调用函数时需要注意访问修饰符

解决方法可以用反射,或者直接将private改成public

5、unidbg实践案例

a、直接调用

主动调用一下sign

package com.xiaojianbang.ndk;
import com.alibaba.fastjson.util.IOUtils;
import com.github.unidbg.*;
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.memory.Memory;



import java.io.File;
import java.io.FileNotFoundException;

public class tre extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass TreUtil;
    private final boolean logging;

    tre(boolean logging) throws FileNotFoundException {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for32Bit()
                .setProcessName("com.maihan.tredian")
                .addBackendFactory(new Unicorn2Factory(true))
                .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setJni( this);

        vm.setVerbose(logging); // 设置是否打印Jni调用细节

        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg/unidbg-android/src/test/java/com/xiaojianbang/ndk/libtre.so"), false);
        module = dm.getModule();
        TreUtil  =vm.resolveClass("com/maihan/tredian/util/TreUtil");
    }

    void destroy() {
        IOUtils.close(emulator);
        if (logging) {
            System.out.println("destroy");
        }
    }
    public static void main(String[] args) throws Exception {
        tre test = new tre(true);
        test.callFunc();
        test.destroy();

    }

    void callFunc() {
        String data ="app_ver=100&nonce=wi3nwo1632749058036&timestamp=1632749058&tzrd=BwzXzSGFyiPstMIVuzTZb7LzTZzbXRJOFzpbQiIaT7tLD0cc1eFFficAAUFoQPlbwk1GAlvWKaP4ipqRnbsFguJ0fALWPNMT6vcqr2uBwiETYt29YHmXhn+VadBnDGFnvzttJTttfKExb/bBJFSuEHKUKh+upPAGYjMNZN9hK1OdN9HxyH8Nx5BM2BnciDsQzjZLg8JAmGuHYiIUedIZaiRuUL/1np58iIU3duQ3B4KPTXvpPYb97T0ARzf8TjQp//LpfYv+QBaqNu6CBaKolV77fC1pqaR7v6eai7CIAABbWAKj6xanD+GEEDzvSTz7ZxTiZrCnAf9+0dNq7QfzbI/uf9LhD+24MshM4pcKh6FpA1PwcHH9wFWofI8dw8KCcrMdLfQ4303sA4Bp/ghN9IzBBtMIj21UAcFFgkch1iKcVTbJ8dlNWk679L7bHDutTmKSZ556wvrhtklUg9yDi0KKS/3QiNVcTXfXj+N/3T4=";
        StringObject stringObject = TreUtil.callStaticJniMethodObject(emulator,"sign(Ljava/lang/String;)Ljava/lang/String;",data);
        System.out.println(stringObject);
    }

}

b、补环境

我们知道unidbg的作用是模拟执行so中的函数,也就是使用C/C++编写的函数,它处于Native层。而Native的函数是Java层的函数通过JNI调用起来的, 那么Native也可以通过JNI这座桥梁去调用Java层的函数。在Native层调用Java层的函数的时候,unidbg中并没有这些函数的实现,那么这些so就无法正常的通过unidbg加载起来。

这边调用Java层的函数,所以我们需要补环境,直接返回一个字符串就行了。

@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    if ("com/sichuanol/cbgc/util/LogShutDown->getAppSign()Ljava/lang/String;".equals(signature)) {
        return new StringObject(vm, "0093CB6721DAF15D31CFBC9BBE3A2B79");
    }
    return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

这个字符串也就是App的签名可以通过frida的hook获得,或者直接看logcat

代码中已经打印出来app sign。

现在依然有问题,我们需要定位,通过下断点看堆栈的方式

Debugger debugger =emulator.attach();
debugger.addBreakPoint(0x40061b12);

Java_com_sichuanol_cbgc_util_SignManager_getSign + 0x18f输入到ida,跳到了_android_log_print这行

分析了一下代码,发现v10已经被release了,但是再下面又被调用了,我们现在需要绕过这个打印。

emulator.getBackend().hook_add_new(new CodeHook() {
           @Override
           public void hook(Backend backend, long address, int size, Object user) {
               emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
           }

           @Override
           public void onAttach(UnHook unHook) {

           }
           @Override
           public void detach() {

           }
       },module.base+0xABE, module.base + 0xABE, null);

利用hook的方式,当程序执行到0xABE的时候,让pc寄存器跳过打印,直接往下执行。

需要跳过4个字节,又由于是thumb指令,还需要+1

成功得到结果

emulator.attach().addBreakPoint(module.base + 0xABE, new BreakPointCallback() {
     @Override
     public boolean onHit(Emulator<?> emulator, long address) {
         emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
         return true;
     }
 });

通过addBreakPoint可以添加一个回调函数,当断点触发时,直接让指令+5就行了,和上面的思路一样。

package com.whitebird;

import com.github.unidbg.AndroidEmulator;

import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
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.memory.Memory;
import unicorn.ArmConst;


import java.io.File;
import java.io.IOException;

public class Wtf extends AbstractJni {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private final DvmClass SignManager;

    private final boolean logging;

    Wtf(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sichuanol.cbgc").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setJni(this);
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libwtf.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        //dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
        SignManager = vm.resolveClass("com/sichuanol/cbgc/util/SignManager");
    }

    void destroy() throws IOException {
        emulator.close();
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        Wtf test = new Wtf(true);
        test.callFunc();
        test.destroy();
    }

    void callFunc() {

//        Debugger debugger =emulator.attach();
//        debugger.addBreakPoint(0x40061b12);
//        emulator.getBackend().hook_add_new(new CodeHook() {
//            @Override
//            public void hook(Backend backend, long address, int size, Object user) {
//                emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
//            }
//
//            @Override
//            public void onAttach(UnHook unHook) {
//
//            }
//            @Override
//            public void detach() {
//
//            }
//        },module.base+0xABE, module.base + 0xABE, null);
//
        emulator.attach().addBreakPoint(module.base + 0xABE, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
                return true;
            }
        });

        String data = "1636221462621";
        StringObject strResult = SignManager.callStaticJniMethodObject(emulator, "getSign(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", new StringObject(vm, ""), new StringObject(vm, ""), new StringObject(vm, data)); // 执行Jni方法
        System.out.println(strResult);
    }

    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        if ("com/sichuanol/cbgc/util/LogShutDown->getAppSign()Ljava/lang/String;".equals(signature)) {
            return new StringObject(vm, "0093CB6721DAF15D31CFBC9BBE3A2B79");
        }
        return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }
}

c、mfw

package com.whitebird;


import com.github.unidbg.AndroidEmulator;

import com.github.unidbg.Emulator;
import com.github.unidbg.Module;

import com.github.unidbg.debugger.BreakPointCallback;
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.memory.Memory;
import unicorn.ArmConst;


import java.io.File;
import java.io.IOException;

public class mfw extends AbstractJni {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private final DvmClass xAuthencode;

    private final boolean logging;

    mfw(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setJni(this);
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libmfw.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        //dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
        xAuthencode = vm.resolveClass("com/mfw/tnative/AuthorizeHelper");
    }

    void destroy() throws IOException {
        emulator.close();
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        mfw test = new mfw(true);
        test.callFunc();
        test.destroy();
    }

    void callFunc() {
        emulator.attach().addBreakPoint(module.base + 0x914C, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+4+1);
                emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0,1);
                return true;
            }
        });
        String data = "PUT&https%3A%2F%2Fmapi.mafengwo.cn%2Frest%2Fapp%2Fuser%2Flogin%2F&after_style%3Ddefault%26app_code%3Dcom.mfw.roadbook%26app_ver%3D8.1.6%26app_version_code%3D535%26brand%3Dgoogle%26channel_id%3DGROWTH-WAP-LC-3%26device_id%3DAC%253A37%253A43%253AA9%253A3F%253A34%26device_type%3Dandroid%26hardware_model%3DPixel%26mfwsdk_ver%3D20140507%26oauth_consumer_key%3D5%26oauth_nonce%3Dcfa857ff-8f4c-4268-8c75-37f07c7aaccf%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1632795895%26oauth_version%3D1.0%26open_udid%3DAC%253A37%253A43%253AA9%253A3F%253A34%26put_style%3Ddefault%26screen_height%3D1794%26screen_scale%3D2.625%26screen_width%3D1080%26sys_ver%3D10%26time_offset%3D480%26x_auth_mode%3Dclient_auth%26x_auth_password%3Da12345678%26x_auth_username%3D15968079477";

        StringObject strResult = xAuthencode.callStaticJniMethodObject(emulator,
                "xAuthencode(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;",
                vm.resolveClass("android/content/Context"),
                new StringObject(vm,data),
                new StringObject(vm,""),
                new StringObject(vm,"com.mfw.roadbook"),
                true
                );
        System.out.println(strResult);
    }

}

利用emulator.getBackend().reg_write绕过调用JNI的地方,同时修改该函数的返回值即R0

d、绕过is_initialised

这边有个值如果没被初始化就会exit(1)

package com.whitebird;

import com.github.unidbg.AndroidEmulator;

import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
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.memory.Memory;
import unicorn.ArmConst;


import java.io.File;
import java.io.IOException;

public class hoge extends AbstractJni {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    private final DvmClass signature;

    private final boolean logging;

    hoge(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.hoge.android.app.fujian").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setJni(this);
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libm2o_jni.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        //dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
        signature = vm.resolveClass("com/hoge/android/jni/Utils");
    }

    void destroy() throws IOException {
        emulator.close();
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        hoge test = new hoge(true);
        test.callFunc();
        test.destroy();
    }

    void callFunc() {
        emulator.attach().addBreakPoint(module.base + 0xA92C, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+8+1);
                return true;
            }
        });
       StringObject stringObject =  signature.callStaticJniMethodObject(emulator,"signature(Ljava/lang/String;Ljava/lang/String)Ljava/lang/String",
                new StringObject(vm,"4.0.0"),
                new StringObject(vm,"1632814143009d0rkV9"));
        System.out.println(stringObject);
    }
}

直接利用 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC,address+8+1);跳过检测

6、unidbg中的VirtualModule

  1. 某些so可能整个加载不起来,或者处理起来很麻烦

  2. unidbg的虚拟模块功能,可以用来注册虚拟的so,自己实现so中相应的方法

  3. libandroid.so用于读取app的assests资源

  4. 如果so中需要依赖该libandroid.so,可以注册libandroid.so到内存中:new AndroidModule(emulator, vm).register(memory);

告诉我们libxiaojianbang.so的加载需要libxiaojianbangA.so

package com.whitebird;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.memory.Memory;

import java.io.*;

public class NativeHelper extends AbstractJni {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass NativeHelper;
    private final boolean logging;

    NativeHelper(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xiaojianbang.app").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析

        vm = emulator.createDalvikVM(); // 创建Android虚拟机

        vm.setJni(this);
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        new XiaojianbangAModule(emulator, vm).register(memory);

        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libxiaojianbang.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        module = dm.getModule(); // 加载好的 libxiaojianbang.so 对应为一个模块

        dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
        NativeHelper = vm.resolveClass("com/xiaojianbang/ndk/NativeHelper");

    }

    void destroy() throws IOException {
        emulator.close();
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        NativeHelper test = new NativeHelper(true);
        test.callFunc();
        test.destroy();
    }

    void callFunc() {
        int retval = NativeHelper.callStaticJniMethodInt(emulator, "add(III)I", 0x100, 0x200, 0x300); // 执行Jni方法
        System.out.println("retval: 0x" + Integer.toHexString(retval));
    }
}

XiaojianbangAModule.java

package com.whitebird;

import com.github.unidbg.Emulator;
import com.github.unidbg.arm.Arm64Svc;
import com.github.unidbg.arm.ArmSvc;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.VirtualModule;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.Map;

public class XiaojianbangAModule extends VirtualModule<VM> {

    private static final Log log = LogFactory.getLog(AndroidModule.class);

    public XiaojianbangAModule(Emulator<?> emulator, VM vm) {
        super(emulator, vm, "libxiaojianbangA.so");
    }

    @Override
    protected void onInitialize(Emulator<?> emulator, final VM vm, Map<String, UnidbgPointer> symbols) {
        boolean is64Bit = emulator.is64Bit();
        SvcMemory svcMemory = emulator.getSvcMemory();
        //要用到的函数在IDA中的符号名
        symbols.put("_Z7bssFuncv", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
            @Override
            public long handle(Emulator<?> emulator) {
              fromJava(emulator, vm);
              return 0;
            }
        } : new ArmSvc() {
            @Override
            public long handle(Emulator<?> emulator) {
                 fromJava(emulator, vm);
                return 0;
            }
        }));
    }

    private static long fromJava(Emulator<?> emulator, VM vm) {
        System.out.println("libxiaojianbangA.so is virtual module");
        return 0;
    }
}

其实有点类似于hook

7、处理so与系统的交互

1、代码方式

之前如果我们想要unidbg在native层调用java层的函数需要补环境,现在如果我们想要访问一些文件也是要补环境的。

我们用unidbg先调用一下这个函数看看,这个函数没有参数和返回值

这里用的unidbg自带的maps,我们现在把程序运行时真正的maps提取出来

package com.whitebird;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
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.file.SimpleFileIO;
import com.github.unidbg.memory.Memory;

import java.io.*;

public class NativeHelper extends AbstractJni implements IOResolver {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass NativeHelper;
    private final boolean logging;

    NativeHelper(boolean logging) {
        this.logging = logging;

        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xiaojianbang.app").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(); // 创建Android虚拟机
        vm.setJni(this);
        vm.setVerbose(logging); // 设置是否打印Jni调用细节
        emulator.getSyscallHandler().addIOResolver(this);
        new XiaojianbangAModule(emulator, vm).register(memory);
        DalvikModule dm = vm.loadLibrary(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/libxiaojianbang.so"), false);
        module = dm.getModule(); // 加载好的 libxiaojianbang.so 对应为一个模块
        NativeHelper = vm.resolveClass("com/xiaojianbang/ndk/NativeHelper");
    }

    void destroy() throws IOException {
        emulator.close();
        if (logging) {
            System.out.println("destroy");
        }
    }

    public static void main(String[] args) throws Exception {
        NativeHelper test = new NativeHelper(true);
        test.callFunc();
        test.destroy();
    }

    void callFunc() {
        emulator.attach().addBreakPoint(module.base+0x2138+1);
        NativeHelper.callStaticJniMethod(emulator, "readSomething()"); // 执行Jni方法

    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        if (("/proc/self/maps").equals(pathname)) {
            return FileResult.success(new SimpleFileIO(oflags, new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/maps"), pathname));
            //return FileResult.success(new ByteArrayFileIO(oflags, pathname, "xiaojianbangmaps".getBytes()));
        }
        //emulator.attach().debug();
        System.out.println("whitebird: " + pathname);
        return null;
    }
}

通过实现IOResolver接口的resolve方法,可以进行文件的重定向,现在unidbg运行起来就是打开的我们自己指定的maps了

记得需要通过emulator.getSyscallHandler().addIOResolver(this);进行注册

2、使用rootfs虚拟文件系统

此时这个目录相当于我们手机上的根目录了,我们可以把代码中要读取的文件放进去,不过在这里maps没有用,因为unidbg内置了一个maps,这个是用代码实现的,代码的方式优先级大于使用rootfs虚拟文件系统

emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xiaojianbang.app").setRootDir(new File("/Users/whitebird/Android/unidbg/unidbg-android/src/test/java/com/whitebird/rootfs")).build();

3、进程pid的获取与设置

emulator.getPid()
this.pid = 6666

4、unidbg中全局变量的设置与获取

emulator.set(..., ...)
emulator.get(...)

5、arm32下的unidbg多线程

unidbg本身不支持多线程,不过有大佬开发了32位下的多线程unidbg

https://github.com/asmjmp0/unidbgMutilThread

6、获取环境变量

上图是真正的环境变量,有的时候我们的App也会去获取环境变量,所以这里也需要我们补环境。

6.1、unidbg提供了对环境变量的初始化

定义在src/main/java/com/github/unidbg/linux/AndroidElfLoader.java中

这个优先级比较高,直接改这里就行了

改完后

6.2、通过libc里的setenv设置环境变量

Symbol setenv = module.findSymbolByName("setenv", true); 
setenv.call(emulator, "PATH", "xxx", 0);

不过好像改了没反应,可能优先级比较低

6.3、Hook getenv

getenv是libc里的函数,Hook这个函数,产生调用就打印日志

emulator.attach().addBreakPoint(module.findSymbolByName("getenv").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            String key = context.getPointerArg(0).getString(0);
            System.out.println("whitebird getenv: " + key);
            return true;
        }
    });

7、HookListener

package com.whitebird;

import com.github.unidbg.Emulator;
import com.github.unidbg.arm.Arm64Hook;
import com.github.unidbg.arm.ArmHook;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.BackendException;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.hook.HookListener;
import com.github.unidbg.linux.android.SystemPropertyHook;
import com.github.unidbg.linux.android.SystemPropertyProvider;
import com.github.unidbg.memory.SvcMemory;
import com.sun.jna.Pointer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class GetEnvHook implements HookListener {


    private final Emulator<?> emulator;

    public GetEnvHook(Emulator<?> emulator) {
        this.emulator = emulator;
    }

    @Override
    public long hook(SvcMemory svcMemory, String libraryName, String symbolName, final long old) {
        if ("libc.so".equals(libraryName)) {
            if ("getenv".equals(symbolName)) {
                if (emulator.is64Bit()) {
                    return svcMemory.registerSvc(new Arm64Hook() {
                        @Override
                        protected HookStatus hook(Emulator<?> emulator) {
                            RegisterContext context = emulator.getContext();
                            int index = 0;
                            Pointer pointer = context.getPointerArg(index);
                            String key = pointer.getString(0);
                            System.out.println("whitebird hooklisten:" + key);
                            return HookStatus.RET(emulator, old);
                        }
                    }).peer;
                } else {
                    return svcMemory.registerSvc(new ArmHook() {
                        @Override
                        protected HookStatus hook(Emulator<?> emulator) {
                            RegisterContext context = emulator.getContext();
                            int index = 0;
                            Pointer pointer = context.getPointerArg(index);
                            String key = pointer.getString(0);
                            System.out.println("whitebird hooklisten:" + key);
                            return HookStatus.RET(emulator, old);
                        }
                    }).peer;
                }
            }
        }
   return 0; }

}

然后在NativeHelper中增加一个监听器

GetEnvHook getEnvHook =new GetEnvHook(emulator);
      memory.addHookListener(getEnvHook);

建议与系统交互的函数都先Hook一下,防止出现错误而unidbg没有打印错误信息而导致无法确认错误所在

8、hook popen

这里读取了两次PATH

我们hook了getenv,确实打印了两次,但是中间的报了两个INFO,应该就是popen的问题

把调试信息全部打开

在popen下个断点

emulator.attach().addBreakPoint(module.findSymbolByName("popen").getAddress(), new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        RegisterContext context = emulator.getContext();
        String command = context.getPointerArg(0).getString(0);
        System.out.println("popen command: " + command);
        return true;
    }
});

尝试直接跳过popen

emulator.attach().addBreakPoint(module.base + 0x26E4, new BreakPointCallback() {
         @Override
         public boolean onHit(Emulator<?> emulator, long address) {
             emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, address + 4);
             return true;
         }
     });

进入so分析一下error

我们没得到v3,但是v3在下面用到了,所以还需要跳过__fgets_chk(&v5, 512LL, v3, 512LL),不过这个是一个循环,还需要跳出循环

emulator.attach().addBreakPoint(module.base + 0x2744, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, address + 4);
                return true;
            }
        });
        emulator.attach().addBreakPoint(module.base + 0x276C, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                if (count == 0) {
                    count = 1;
                    System.out.println("while is run 0");
                } else if (count == 1) {
                    emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X8, 0);
                    System.out.println("while is run 1");
                }
                return true;
            }
        });

先跳过函数调用,然后在while循环判断的地方修改寄存器

现在就可以正常打印了

8、Linux内核的Syscall

1、系统调用号与对应的函数

https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md

比如0调用号,对应linux里面的read

2、unidbg中对syscall的实现

unidbg也补了一些syscall的实现

对一些调用号进行了处理,不过不是所有的调用号,因此可能需要我们自己补环境。

3、unidbg的检测点

uname -a指令可以得到设备的一些信息

特征还是比较明显的,我们可以改掉。

4、接管未处理的调用号

比如之前的popen虽然是libc.so的函数,但是底层调用了系统调用号且unidbg没有处理

480 pipe和549 fork

可以发现对于找不到的NR,会有专门的处理函数,我们的目的就是重写这个函数,自己实现调用号的处理

package com.whitebird;

import com.github.unidbg.Emulator;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.linux.ARM64SyscallHandler;
import com.github.unidbg.memory.SvcMemory;
import com.sun.jna.Pointer;

public class MyARMSyscallHandler extends ARM64SyscallHandler {
    public MyARMSyscallHandler(SvcMemory svcMemory) {
        super(svcMemory);
    }

    @Override
    protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) {
        if (NR == 114) {
            Arm32RegisterContext context = null;
            int pid = context.getR0Int();
            Pointer wstatus = context.getR1Pointer();
            int options = context.getR2Int();
            Pointer rusage = context.getR3Pointer();
            System.out.println("wait4 pid=" + pid + ", wstatus=" + wstatus + ", options=0x" + Integer.toHexString(options) + ", rusage=" + rusage);
            return true;
        }
        return super.handleUnknownSyscall(emulator, NR);
    }
}

现在就需要去替换掉原来的ARM64SyscallHandler

将emulator = AndroidEmulatorBuilder.for64Bit().setProcessName(“com.xiaojianbang.app”).build();改成了上面的样子

其实for64Bit就是下图的函数,只不过展开了

还会调用build

再跟进AndroidARM64Emulator,就可以发现引用调用号的地方,所以重写了UnixSyscallHandler


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com