Unidbg

开源地址

初始配置

用idea打开github源码即可,初始配置会由maven自动配齐,下面是项目的部分结构解析。

├── README.md                     # 项目介绍和使用指南
├── LICENSE                       # 开源许可证文件
├── .gitignore                    # Git 忽略文件配置
├── pom.xml                       # Maven 配置文件,定义了项目的依赖和构建配置
├── mvnw                          # 脚本文件,用于 Maven Wrapper (Linux/Mac)
├── mvnw.cmd                      # 脚本文件,用于 Maven Wrapper (Windows)
├── test.sh                       # 测试脚本 (Linux/Mac)
├── test.cmd                      # 测试脚本 (Windows)
├── .mvn/                         # Maven 配置目录
│   └── wrapper/                  # Maven Wrapper 相关配置
├── assets/                       # 存放模拟过程中使用的资源文件
│   ├── *.dll                     # Windows 动态链接库文件
│   └── *.so                      # Linux/Android 动态链接库文件
├── backend/                      # 后端逻辑实现,包含核心模拟功能
├── unidbg-api/                   # 核心接口和抽象类模块
│   └── src/                      # API 模块的源代码目录
├── unidbg-ios/                   # iOS 应用模拟模块
│   └── src/                      # iOS 模拟模块的源代码目录
├── unidbg-android/               # Android 应用模拟模块
│   ├── pom.xml                   # Maven 构建文件
│   ├── pull.sh                   # 拉取 Android 模拟所需依赖文件的脚本
│   └── src/                      # unidbg-android 模块的源代码目录
│       ├── main/
│       │   ├── java/             # 核心 Java 源代码
│       │   │   └── com/github/unidbg/  # 包含核心模拟器、文件系统、虚拟机组件
│       │   │   └── net/fornwall/jelf   # ELF 文件格式解析实现
│       │   └── resources/        # 资源文件,封装了 JNI 库、Android 系统库等
│       ├── test/
│       │   ├── java/             # 单元测试代码
│       │   ├── native/android/   # 测试 Android 原生库的 C/C++ 源代码
│       │   └── resources/        # 测试资源文件,包含预编译的二进制文件(log4j.properties这个是日志相关配置,可以对open,syscall这类的系统调用进行trace)
└── .mvn/                         # Maven Wrapper 相关配置目录

Unidbg 支持了数个后端,目前共五个 Backend,分别是 Unicorn、Unicorn2、Dynarmic(执行速度较快)、Hypervisor、KVM。new DynarmicFactory(true)中的true,标志着在出现异常时是否使用默认后端unicorn。

.setRootDir() //设置虚拟机根目录,可以实现io重定向,例如:app读取/data/data/1.txt,通过这个设置成D:/test,那么app读取的就会是D:/test/data/data/1.txt。

API

emulator 常用API

方法名返回类型描述
getMemory()Memory获取内存操作接口。
getPid()int获取进程的 PID。
createDalvikVM()VM创建虚拟机。
createDalvikVM(File apkFile)VM创建虚拟机并指定 APK 文件路径。
getDalvikVM()VM获取已创建的虚拟机。
showRegs()void显示当前寄存器状态,可指定寄存器。
getBackend()Backend获取后端 CPU。
getProcessName()String获取进程名。
getContext()RegisterContext获取寄存器上下文。
traceRead(long begin, long end)voidTrace 读内存操作。
traceWrite(long begin, long end)voidTrace 写内存操作。
traceCode(long begin, long end)voidTrace 汇编指令执行。
isRunning()boolean判断当前 Emulator 是否正在运行。

memory 常用api

unidbg自己配置了两个版本安卓sdk(路径为 unidbg-android/src/main/resouce/android ),一个是sdk19(Android 4.4)、一个是sdk23(Android 6.0),64位只能用sdk23 。但是unidbg对Arm32支持度和完善程度都高于64位。

方法名返回类型描述
setLibraryResolver(AndroidResolver resolver)void设置 Android SDK 版本解析器,目前支持 19 和 23 两个版本。
getStackPoint()long获取当前栈指针的值。
pointer(long address)UnidbgPointer获取指针,指向指定内存地址,可通过指针操作内存。
getMemoryMap()Collection<MemoryMap>获取当前内存的映射情况。
findModule(String moduleName)Module根据模块名获取指定模块。
findModuleByAddress(long address)Module根据地址获取指定模块。
loadLibrary(File file, boolean forceLoad)ElfModule加载 SO 文件,会调用 Linker.do_dlopen() 方法完成加载。
allocatestack(int size)UnidbgPointer在栈上分配指定大小的内存空间。
writestackstring(String value)UnidbgPointer将字符串写入栈内存中。
writestackBytes(byte[] value)UnidbgPointer将字节数组写入栈内存中。
malloc(int size, boolean runtime)UnidbgPointer分配指定大小的内存空间,返回指向该内存的指针。

VM 常用api

方法名返回类型描述
createDalvikVM(File apkFile)VM创建虚拟机,指定 APK 文件,file可为空
setVerbose(boolean verbose)void设置是否输出 JNI 运行日志。
loadLibrary(File soFile, boolean callInit)DalvikModule加载 SO 模块,参数二设置是否自动调用 init 函数。
setJni(Jni jni)void设置 JNI 交互接口,推荐实现 AbstractJni
getJNIEnv()Pointer获取 JNIEnv 指针,可作为参数传递。
getJavaVM()Pointer获取 JavaVM 指针,可作为参数传递。
callJNI_OnLoad(Emulator<?> emulator, Module module)void调用 JNI_OnLoad 函数。
addGlobalObject(DvmObject<?> obj)int向 VM 添加全局对象,返回该对象的 hash 值。
addLocalObject(DvmObject<?> obj)int向 VM 添加局部对象,返回该对象的 hash 值。
getObject(int hash)DvmObject<?>根据 hash 值获取虚拟机中的对象。
resolveClass(String className)DvmClass解析指定类名,构建并返回一个 DvmClass 对象。
getPackageName()String获取 APK 包名。
getVersionName()String获取 APK 版本名称。
getVersionCode()String获取 APK 版本号。
openAsset(String assetName)InputStream打开 APK 中的指定资源文件。
getManifestXml()String获取 AndroidManifest.xml 文件的文本内容。
getSignatures()CertificateMeta[]获取 APK 签名信息。
findClass(String className)DvmClass通过类名获取已经加载的类(DvmClass 对象)。
getEmulator()Emulator<?>获取模拟器对象 emulator

VM dalvikVM = emulator.createDalvikVM(new File("apk file path"))-创建虚拟机并指定APK文件,加载指定APK文件,unidbg可以帮我们完成一些小操作,例如:解析 Apk 基本信息,Apk 的版本名、版本号、包名、 Apk 签名等信息,减少补环境操作;解析和管理 Apk 资源文件,加载 Apk 后可以通过 openAsset获取 APK assets目录下的文件。

/**
 * 加载指定名称的库文件。
 * @Param libname 库文件的名称,不包括前缀 "lib" 和后缀 ".so"(例如 "example" 对应 "libexample.so")。
 * @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
 * @Return 加载后的 DalvikModule 对象,封装了加载的库模块。
 */
DalvikModule loadLibrary(String libname, boolean forceCallInit);

/**
 * 从原始字节数组中加载指定的库文件。
 * @param libname 库文件的名称,仅用于标识该库,与文件路径无关。
 * @param 传入buffer方便解析elf
 */
DalvikModule loadLibrary(String libname, byte[] raw, boolean forceCallInit);

/**
 * 从指定路径的加载ELF。
 * @param elfFile 表示库的 ELF 文件,必须是有效的 ELF 格式文件。例如:new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so")
 * @param forceCallInit 是否强制调用库的初始化函数(如 JNI_OnLoad)。
 */
DalvikModule loadLibrary(File elfFile, boolean forceCallInit);

so层函数调用

因为unidbg的一些实现的原因:

public final UnidbgPointer findNativeFunction(Emulator<?> emulator, String method) {
        // method这里是JNI抽象过的,现在这里解开然后拼名字。
        UnidbgPointer fnPtr = nativesMap.get(method);
        int index = method.indexOf('(');
        if (fnPtr == null && index == -1) {
            index = method.length();
        }
        StringBuilder builder = new StringBuilder();
        builder.append("Java_");
        mangleForJni(builder, getClassName()); // <-- 就是这个b getClassName
        builder.append("_");
        mangleForJni(builder, method.substring(0, index));
        String symbolName = builder.toString();
        if (fnPtr == null) {
            for (Module module : emulator.getMemory().getLoadedModules()) {
                Symbol symbol = module.findSymbolByName(symbolName, false); // <-- 还有这个
                if (symbol != null) {
                    fnPtr = (UnidbgPointer) symbol.createPointer(emulator);
                    break;
                }
            }
        }
        if (fnPtr == null) {
            throw new IllegalArgumentException("find method failed: " + method);
        }
        ...
        return fnPtr;
    }

这两兄弟配合,强烈建议是在对应Java层的调用位置去写对应的unidbg代码,比如:

package com.kanxue.test2;

public class MainActivity extends AppCompatActivity {
    public native boolean jnitest(String str);
    public native String stringFromJNI();
    static {
        System.loadLibrary("native-lib");
    }
    ...
}

这里就在项目结构这里写同名类

├── unidbg-android/               
│   └── src/                      # unidbg-android 模块的源代码目录
│       ├── main/
│       │   ├── java/             # 核心 Java 源代码
│       │   │   └── com/kanxue/test2/   # <-- 这里创建同名类来写
            └── resources/        # 资源文件,封装了 JNI 库、Android 系统库等

不然的话会出现同一个代码不同路径下会跑不通的情况。

Java层Jni函数调用

unidbg其实封装好了几个:

obj.callJniMethod(Emulator<?> emulator, String method, Object...args) // 后面4条最后也是调用这个函数
obj.callJniMethodBoolean(Emulator<?> emulator, String method, Object...args)
obj.callJniMethodInt(Emulator<?> emulator, String method, Object...args)
obj.callJniMethodLong(Emulator<?> emulator, String method, Object...args)
obj.callJniMethodObject(Emulator<?> emulator, String method, Object...args)

obj.callStaticJniMethod(Emulator<?> emulator, String method, Object...args) //后略

下面

protected static Number callJniMethod(Emulator<?> emulator, VM vm, DvmClass objectType, DvmObject<?> thisObj, String method, Object...args) {
        // 拿Native函数指针
        UnidbgPointer fnPtr = objectType.findNativeFunction(emulator, method);
        // 将当前对象加入到引入表,防止被回收
        vm.addLocalObject(thisObj);
        List<Object> list = new ArrayList<>(10);
        // so层JNIEnv和this指针
        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) {
                    // 将 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 对象
                    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());
    }

Unidbg将基本类型直接传递,基本的对象类型自己进行了封装。对于其他类型的数据,则需要借助resolveClass构造,例如Context

DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);

符号调用和偏移调用

DalvikModule dm = vm.loadLibrary(new File("..../libttEncrypt.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
Symbol symbol = module.findSymbolByName("导出符号");  
if (symbol != null){  
    //第一个模拟器实例,第二个jnienv,第三个jclass,第四个可变参数
    Number numbers = symbol.call(emulator, vm.getJNIEnv(), vm.addLocalObject(实例类), 可变参数);  
    int result = numbers.intValue(); 
    System.out.println(result);  
    //如果返回值是string,可以通过vm.getObject(retval)获取
    System.out.println(vm.getObject(result).getValue());
}else {  
    System.out.println("符号未找到");  
}
//第一个模拟器实例,第二个偏移地址(thumb指令集记得+1),第三个jnienv,第四个jclass (就是this),第五个可变参数
Number number = module.callFunction(emulator, 0x11240, vm.getJNIEnv(),vm.addLocalObject(SecurityUtils),vm.addLocalObject(new StringObject(vm, "超级")));
DvmObject<?> object = vm.getObject(number.intValue());  
System.out.println("result:" + object.getValue());

Hook

有内置和原生的hook(基于Unicorn)

HookZz&Dobby

内置三方hook

在给的样例里面(com.bytedance.frameworks.core.encrypt/TTEncrypt)

基础的Hook:

IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
hookZz.wrap(module.findSymbolByName("ss_encrypt"), new WrapCallback<RegisterContext>() { // inline wrap导出函数
  @Override
  public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
    Pointer pointer = ctx.getPointerArg(2);
    int length = ctx.getIntArg(3);
    byte[] key = pointer.getByteArray(0, length);
    Inspector.inspect(key, "ss_encrypt key");
  }
  @Override
  public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
    System.out.println("ss_encrypt.postCall R0=" + ctx.getLongArg(0));
    }
});

参数里的WrapCallback的泛型接口有三个RegisterContext(函数 Hook)HookZzArm32RegisterContext(针对ARM32位)HookZzArm64RegisterContext(针对ARM64位)因为可以访问某个寄存器的值,所以适用于inline hook

Inline Hook:

// Thumb记得+1
hookZz.instrument(module.base + 0x00000F5C + 1, new InstrumentCallback<Arm32RegisterContext>() {
    @Override
    public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些
        System.out.println("R3=" + ctx.getLongArg(3) + ", R10=0x" + Long.toHexString(ctx.getR10Long()));
    }
});

替换函数:

用的是dobby

// 使用 dobby 的 replace 方法替换 "Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum" 函数的实现
Dobby dobby = Dobby.getInstance(emulator);  
dobby.replace(module.findSymbolByName("Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum"), new ReplaceCallback() { // 使用Dobby inline hook导出函数  
    @Override  
    public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {  
                //替换整数型
        return HookStatus.LR(emulator,888888);  
        //字符串
        return HookStatus.LR(emulator, vm.addLocalObject(new StringObject(vm, "超级")));
    }  
});

Unicorn Hook

能实现函数级别、指令级别、文件级别、异常级别等hook

emulator.getBackend().hook_add_new(new CodeHook() {  
    @Override  
    public void hook(Backend backend, long address, int size, Object user) {  
        Arm64RegisterContext context = emulator.getContext();  
        if (address == module.base + 0x11330) {  
            int x0=emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0).intValue();  
            System.out.println("x0:"+vm.getObject(x0));  
            emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X0,vm.addLocalObject(new StringObject(vm, "超级")));  
        }  
    }  
    @Override  
    public void onAttach(UnHook unHook) {  
    }  
    @Override  
    public void detach() {  
    }  
}, module.base + 0x11240,module.base + 0x11340,null);  // hook、起始地址、结束地址、user_data
StringObject Result = SecurityUtils.callStaticJniMethodObject(emulator, "vipLevel(Ljava/lang/String;)Ljava/lang/String;", "123"); 
System.out.println("Result: " + Result.getValue());

Console Debugger

Unidbg提供的工具,允许用户在模拟执行过程中设置断点、单步调试、查看和修改内存及寄存器等操作,从而深入分析目标程序的行为。

下断点:

Debugger attach = emulator.attach();  
attach.addBreakPoint(module.base + 0x11070); //下断地址

断住之后会开一个交互窗口,交互指令:

命令功能说明
c继续执行程序
n跨过当前指令
bt回溯堆栈
st hex搜索堆栈
shw hex搜索可写堆
shr hex搜索可读堆
shx-hex搜索可执行堆
nb在下一个区块中断
s步入当前指令
s [decimal]执行指定数量的指令
s (blx)执行直到 blx 助记符(性能较低)
m (op) [size]显示内存,默认大小为 0x70,大小可为十六进制或十进制
mr0-mr7, mfp, mip, msp [size]显示指定寄存器的内存
m (address) [size]显示指定地址的内存,地址需以 0x 开头
wr0-wr7, wfp, wip, wsp <value>写入指定寄存器
wb(address), ws(address), wi(address) <value>写入指定地址的(字节、短、整数)内存,地址需以 0x 开头
wx (address) <hex>将字节写入指定地址的内存,地址需以 0x 开头
b (address)添加临时断点,地址需以 0x 开头,可为模块偏移量
b添加寄存器 PC 的断点
r删除寄存器 PC 的断点
blr添加寄存器 LR 的临时断点
p (assembly)在 PC 地址修补汇编指令
where显示 Java 堆栈跟踪
trace [begin-end]设置指令跟踪
traceRead [begin-end]设置内存读取跟踪
traceWrite [begin-end]设置内存写入跟踪
vm查看已加载的模块
vbs查看断点
d显示反汇编代码
d (0x)在指定地址显示反汇编代码
stop停止模拟
run [arg]运行测试
gc运行 System.gc()
threads显示线程列表
cc size将地址范围的汇编代码转为 C 函数

干坏事:

emulator.attach().addBreakPoint(module.findSymbolByName("verifyApkSign").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            System.out.println("替换函数 verifyApkSign");
            RegisterContext registerContext = emulator.getContext();
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLRPointer().peer);
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0);
            return true;
        }
    });

Patch

Unidbg的内存Patch借助Keystone汇编引擎

UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x1146C);  
//在进行 Patch 操作前,需确保已正确定位目标函数的地址和指令集类型(如 ARM 或 Thumb)
//如果是32位的,代码如下:Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb);
Keystone keystone = new Keystone(KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian);  
String s = "MOV W0, #0x99";  //具体要修改的汇编指令
byte[] machineCode = keystone.assemble(s).getMachineCode();   //转换为机器码
pointer.write(machineCode); //写入内存

神必小知识:

认arm和thumb可以看调用:BX addr+0x1,表示跳转至Thumb;BX addr,表示跳转至arm。


Unidbg Trace

同类型:IDA Instruction trace 和 frida Stalker

使用

一键启动:

emulator.traceCode();

指定范围内,比如只想追踪某个so不想追踪库函数:

// 获取目标模块对象
Module module = dm.getModule();
// 仅追踪目标模块地址范围内的指令
emulator.traceCode(module.base, module.base + module.size); // start address -- end base

trace要提早

  • 追踪 JNI_OnLoad: 将 traceCode 放在 dm.callJNI_OnLoad(emulator) 之前。
  • 追踪初始化函数 (init_array)JNI_OnLoad 之前还有初始化函数。要追踪它们,需要在加载模块的第一时间开启 Trace。最佳实践是使用 ModuleListener。或者是先加载环境中的libc.so,再去hook里面的strcmp,这样就能在初始化之前去hook到 .init 或者 .init_array 了。
memory.addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
    // 当我们关心的模块被加载时,立即开启trace
    if("lib52pojie.so".equals(module.name)){
        emulator.traceCode(module.base, module.base + module.size);
    }
}
});
// 这之后再加载模块
DalvikModule dm = vm.loadLibrary(new File("xxx.so"), true);
public void hookStrcmpByUnicorn(){
      emulator.attach().addBreakPoint(moduleLibc.findSymbolByName("strcmp").getAddress(), new BreakPointCallback() {
          @Override
          public boolean onHit(Emulator<?> emulator, long address) {
              RegisterContext registerContext = emulator.getContext();
              String arg1 = registerContext.getPointerArg(0).getString(0);

              emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
                  @Override
                  public boolean onHit(Emulator<?> emulator, long address) {
                      if(arg1.equals("abcdef")){
                          emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, -1);
                      }
                      return true;
                  }
              });
              return true;
          }
      });
  }
    public void hookStrcmpByHookZz(){
      IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook
      hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
      hookZz.wrap(moduleLibc.findSymbolByName("strcmp"), new WrapCallback<HookZzArm32RegisterContext>() {
          String arg1;
          @Override
          public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
              arg1 = ctx.getPointerArg(0).getString(0);
          }
          @Override
          public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
              if(arg1.equals("abcdef")){
                  ctx.setR0(-1);
              }
          }
      });
      hookZz.disable_arm_arm64_b_branch();
  }
  
  
DalvikModule dmLibc = vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib/libc.so"), true);
moduleLibc = dmLibc.getModule();

// hook
hookStrcmpByUnicorn();
// 或者
// hookStrcmpByHookZz();

时间估算:

CodeHook:unidbg 每分钟约可追踪 40 万行执行流,自己根据结果预估一下要不要睡觉(

private long instructionCount = 0; // 用于指令计数
emulator.getBackend().hook_add_new(new CodeHook() {  
  @Override  
  public void hook(Backend backend, long address, int size, Object user) {  
      instructionCount++;  
  }  
  
  @Override  
  public void onAttach(UnHook unHook) {}  
  
  @Override  
  public void detach() {}  
}, module.base, module.base + module.size, null);
System.out.println("总共执行ARM指令数: " + test.instructionCount);

只关心一个调用后的Trace:

long callAddr = module.base + 0xE53C; // 假设这是BL指令的地址
final TraceHook[] traceHook = new TraceHook[1];
// 在调用处开启追踪
emulator.attach().addBreakPoint(callAddr, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
    traceHook[0] = emulator.traceCode(module.base, module.base + module.size);
    return true;
}
});
// 在调用返回后关闭追踪
emulator.attach().addBreakPoint(callAddr + 4, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
    if (traceHook[0] != null) {
        traceHook[0].stopTrace();
    }
    return true;
}
});

甚至都可以只Trace一行

函数追踪

unidbg version >= 0.9.6

debugger.traceFunctionCall

有现成的轮子:

// in file unidbg-api\src\main\java\com\github\unidbg\unwind\Unwinder.java
// add this function: 

/**  
 * 将给定的内存地址格式化为包含详细信息、可读的字符串。  
 * 这个方法会尝试解析地址,并提供尽可能多的上下文信息,如模块名、函数名(符号)、偏移量等。  
 *  
 * @Param address 要格式化的绝对内存地址。  
 * @Return 一个包含地址详细信息的格式化字符串。  
 */  
public String formatAddressDetails(long address) {  
    // 1. 尝试根据地址查找其所属的模块(例如,一个 .so 文件)。  
    Module module = emulator.getMemory().findModuleByAddress(address);  

    // 2. 如果地址位于一个已加载的模块内:  
    if (module != null) {  
        // 2.1. 在模块中查找离该地址最近的符号(即函数或全局变量的名称)。  
        //      `true` 参数表示也查找非导出的内部符号。  
        Symbol symbol = module.findClosestSymbolByAddress(address, true);  

        // 2.2. 如果找到了一个符号:  
        if (symbol != null) {  
            // 2.2.1. 创建一个 demangler 实例,用于将 C++ "mangled"(混淆)的符号名还原为可读的函数签名。  
            GccDemangler demangler = DemanglerFactory.createDemangler();  
            // 2.2.2. 对符号名进行 demangle 操作。  
            String demangledName = demangler.demangle(symbol.getName());  
            // 2.2.3. 计算当前地址相对于符号起始地址的偏移量。  
            long offset = address - symbol.getAddress();  
            // 2.2.4. 返回最详细的格式化字符串,例如:"[libnative.so] JNI_OnLoad + 0x10 (at 0x...)"  
            return String.format("[%s] %s + 0x%x (at 0x%x)", module.name, demangledName, offset, address);  
        } else {  
            // 2.3. 如果在模块内但没有找到具体的符号,则将其视为一个未命名的子程序(subroutine)。  
            //      计算地址相对于模块基地址的偏移量。  
            long offset = address - module.base;  
            // 2.3.1. 返回一个通用的子程序格式,例如:"[libnative.so] sub_c80 (at 0x...)"  
            return String.format("[%s] sub_%x (at 0x%x)", module.name, offset, address);  
        }  
    }  

    // 3. 如果地址不属于任何模块,则尝试查找它是否位于一个已知的内存区域中(例如,栈或堆)。  
    MemRegion region = emulator.getSvcMemory().findRegion(address);  
    if (region != null) {  
        // 3.1. 如果找到了,返回区域名称,例如:"[stack] (at 0x...)"  
        return String.format("[%s] (at 0x%x)", region.getName(), address);  
    }  

    // 4. 如果以上所有尝试都失败了,返回一个表示“未知”的通用格式。  
    return String.format("[unknown] (at 0x%x)", address);  
}
/**  
 * 【模块化功能】: 附加一个函数调用追踪器 (traceFunctionCall)。  
 * 此功能会监听所有的函数调用,并以树状结构打印出调用关系。  
 * @param traceStream 日志输出流  
 */  
public void attachFunctionCallTracer(final PrintStream traceStream) {  
    Debugger debugger = emulator.attach();  
    System.out.println("函数调用关系追踪器已附加,结果将输出到日志文件。");  

    debugger.traceFunctionCall(null, new FunctionCallListener() {  
        private int depth = 0;  

        private String getPrefix(int currentDepth) {  
            if (currentDepth <= 0) {  
                return "";  
            }  
            StringBuilder sb = new StringBuilder();  
            for (int i = 0; i < currentDepth - 1; i++) {  
                sb.append("");  
            }  
            sb.append("├─ ");  
            return sb.toString();  
        }  

        @Override  
        public void onCall(Emulator<?> emulator, long callerAddress, long functionAddress) {  
            String prefix = getPrefix(depth + 1);  
            String details = emulator.getUnwinder().formatAddressDetails(functionAddress);  
            traceStream.printf("%sCALL -> %s%n", prefix, details);  
            depth++;  
        }  

        @Override  
        public void postCall(Emulator<?> emulator, long callerAddress, long functionAddress, Number[] args) {  
            depth--;  
            String prefix = getPrefix(depth + 1);  
            String details = emulator.getUnwinder().formatAddressDetails(functionAddress);  

            Backend backend = emulator.getBackend();  
            Number retVal = emulator.is64Bit() ? backend.reg_read(Arm64Const.UC_ARM64_REG_X0) : backend.reg_read(ArmConst.UC_ARM_REG_R0);  
            long retValLong = retVal.longValue();  

            // 尝试将返回值作为指针解析  
            String retValFormatted = String.format("0x%x", retValLong);  
            UnidbgPointer pointer = UnidbgPointer.pointer(emulator, retValLong);  
            if (pointer != null) {  
                String cstring = safeReadCString(pointer);  
                // 如果是一个可打印的字符串,则附加到日志中  
                if (isPrintable(cstring)) {  
                    retValFormatted += String.format(" -> \"%s\"", cstring);  
                }  
            }  

            traceStream.printf("%sRET  <- %s, ret=%s%n", prefix, details, retValFormatted);  
        }  
    });  
}  

使用EP:

public static void main(String[] args) {  
    MainActivity test = new MainActivity(false);  
    String traceFile = "unidbg-android/src/test/resources/traceFunctions.txt";  
    try (PrintStream traceStream = new PrintStream(new FileOutputStream(traceFile), true)) {  
        // 在调用 crack 之前附加追踪器,持久化日志  
        test.attachFunctionCallTracer(traceStream);  
        // 执行会触发函数调用的模拟代码  
        test.crack();  
    } catch (IOException e) {  
        e.printStackTrace();  
    } finally {  
        // 确保模拟器资源被释放  
        test.destroy();  
        System.out.println("总共执行ARM指令数: " + test.instructionCount);  
    }  
}  

补环境

JNI补环境是Unidbg不可不品鉴的一环。当Unidbg模拟执行so文件时候尝试通过JNI调用Java层的代码的时候,Unidbg必须能够提供这些Java方法的模拟实现。
如果 Unidbg 缺少某个方法的实现,或者默认实现不符合预期,程序就会抛出 UnsupportedOperationException 异常,导致模拟中断。
因此,JNI 补环境的目的就是拦截这些 JNI 调用,并提供一个符合目标 SO 文件逻辑预期的返回值或行为,从而“欺骗”SO 文件,使其认为自己运行在真实的 Android 环境中。(本质就是缺啥补啥)

 Java 层函数补全:继承 AbstractJni

这是最主要的 Java 层补环境方式。通过创建一个继承自com.github.unidbg.linux.android.dvm.AbstractJni 的自定义类,并重写其关键方法,可以模拟任意 JNI 调用。

  • 在创建了 DalvikVM 实例后,将 AbstractJni的实例设置给它。
  • 强烈建议开启详细日志 (vm.setVerbose(true)),这会在控制台打印出所有 JNI 调用的详细信息(包括方法签名),是定位哪个方法需要补全的关键
public class xxx extends AbstractJni {
  VM vm = emulator.createDalvikVM();
  vm.setJni(this);     // Unidbg内部会将一部分环境自己补全了,但是还是可能需要自己去补全
  vm.setVerbose(true); // 开启详细日志
}

重写方法

  • 根据 SO 文件调用的 Java 方法类型(静态/实例、返回类型),重写 AbstractJni 中对应的方法。
  • 方法命名规律call[Static]XXXMethodV,其中 Static 表示静态方法,XXX 表示返回类型(如 ObjectBooleanIntVoid 等)。
  • 核心参数
    • signature: 完整的方法签名字符串,如 "com/example/MyClass->getAppContext()Landroid/content/Context;",用于 switch 或 if 判断具体是哪个方法被调用。
    • vaList: 包含了调用时传递的所有参数。
JNI 原型AbstractJni 覆写方法典型用途
jobject CallStaticObjectMethodVcallStaticObjectMethodV返回 ContextApplication 等
jint CallIntMethodVcallIntMethodV返回版本号、随机数
jobject NewObjectVnewObjectV构造 ZipFileCipher
void Set<Object>FieldsetObjectField 等SO 写回 Java 字段
jint RegisterNativesinvokeRegisterNatives动态注册补环境
jint NewGlobalRefnewGlobalRef / deleteGlobalRef线程间对象共享

模拟静态方法

@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    switch (signature) {
        case "com/example/MyClass->getAppContext()Landroid/content/Context;":
            // 返回一个模拟的 Context 对象
            return vm.resolveClass("android/content/Context").newObject(null);
        // 可以添加更多 case 来处理其他需要补全的 Java 方法
        default:
            // 对于未处理的方法,务必调用父类的默认实现
            return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }
}

模拟构造方法(newObjectV)

@Override  
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {  
  switch (signature) {  
    case "java/util/HashMap-><init>()V": {  
        return ProxyDvmObject.createObject(vm, new HashMap<>());  
    }  
    
    case "com/zj/wuaipojie/ui/ChallengeTen$UserInfo-><init>(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;Ljava/util/Map;)V": {  
        System.out.println("【补环境 Level 3】拦截到 UserInfo 构造方法");  
        Map<String, DvmObject<?>> userInfoData = new HashMap<>();  
        userInfoData.put("status", vaList.getObjectArg(4));  
        userInfoData.put("properties", vaList.getObjectArg(5));  
        return dvmClass.newObject(userInfoData);  
    }  
  }  
  return super.newObjectV(vm, dvmClass, signature, vaList);  
}

模拟字段访问(Get/Set<Type>Field):

public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {  
    // 匹配枚举类的 PREMIUM 静态字段  
    if ("com/zj/wuaipojie/ui/ChallengeTen$AccountStatus->PREMIUM:Lcom/zj/wuaipojie/ui/ChallengeTen$AccountStatus;".equals(signature)) {  
        System.out.println("【补环境】拦截到获取 AccountStatus.PREMIUM 静态字段");  
        // 创建一个枚举实例(用字符串"PREMIUM"作为其值,方便后续name()方法返回正确结果)  
        DvmObject<?> premium = dvmClass.newObject("PREMIUM");  
        return premium;  
    }  
    return super.getStaticObjectField(vm, dvmClass, signature);  
}

@Override
public void setIntField(BaseVM vm, DvmObject<?> dvmObject, String signature, int value) {
    // signature的格式是:com/example/User->age:I
    if ("com/example/User->age:I".equals(signature)) {
        System.out.println("SO 正在设置 User 对象的 age 字段,值为: " + value);
        // 你可以在这里记录值,或者什么都不做
        return; // 注意set方法是void返回
    }
    super.setIntField(vm, dvmObject, signature, value);
}

处理不同类型的对象

  • Android 特有类 (如 ContextApplication): 使用 vm.resolveClass(className).newObject(value) 创建模拟对象。
  • JDK 标准库类 (如 HashMap): 使用 ProxyDvmObject.createObject(vm, realJavaObject) 来包装一个真实的 Java 对象进行模拟。这在处理集合类(如 Map)时非常有用。
    • 对于 JNI 来说,继承和多态是非常重要的概念。当 Native 代码通过一个父类或接口的引用来调用子类实例的方法时(例如通过 Map 引用调用 HashMap 实例的 put 方法),JNI 的运行时必须能够顺着继承链向上查找,才能找到正确的方法定义和 ID。

处理复杂的JNI数据结构

1. 数组 byte[]String[]、 int[]

Unidbg 的 DvmObject 和 ArrayObject 类可以帮助在 Java 的数组 和 Native 层的内存指针之间进行转换。

@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if ("com/example/Utils->getSignKeys()[Ljava/lang/String;".equals(signature)) {
    // 伪造的Key列表
    StringObject key1 = new StringObject(vm, "key_alpha");
    StringObject key2 = new StringObject(vm, "key_beta");
    // 创建一个包含这些DvmObject的ArrayObject
    return new ArrayObject(key1, key2);
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

2.结构体 (Struct)

  • 通过 jobject 传递:如果 Java 层有一个类与 C/C++ 结构体对应,Native 代码会通过 JNI 的 Get/Set<Type>Field 系列函数访问字段。在 Unidbg 中,需要模拟这些 JNI 调用,使用DvmObject 提供的 setValue 和 getValue 方法来操作模拟对象的字段。
  • 通过指针传递:Native 函数有时直接接收或返回一个指向结构体内存的指针(通常是 long 类型)。
    • 补全逻辑
      1. 使用emulator.getMemory().malloc(size, true) 在 Unidbg 的模拟内存中分配空间。
      2. 使用返回的UnidbgPointer 对象,通过 pointer.setInt(offset, value)pointer.setString(offset, value) 等方法填充结构体的各个成员。
      3. 将这个指针的地址(一个long 值)返回给调用方。

3.Map 等集合对象

  • 在 JNI 调用时,可以通过ProxyDvmObject.createObject(vm, realJavaMap) 将一个真实的 Java HashMap 对象包装成 DvmObject 传给 Native 层 。
  • 当 Native 代码通过 JNI 调用Map.getMap.keySet 等方法时,Unidbg 会拦截到这些调用。你需要在自定义的 AbstractJni 子类中实现这些方法的逻辑,直接操作那个真实的 realJavaMap 对象来返回正确的值。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇