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。

暂无评论

发送评论 编辑评论


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