目录
Android 逆向之 Uni Debug 全面解析
一、Uni Debug 概述
(一)Unicorn 框架
(二)Uni Debug 简介
二、Uni Debug 的使用场景、优缺点
(一)使用场景
(二)优点
(三)缺点
三、Uni Debug 的使用步骤
(一)配置资源文件
(二)文件结构解析
(三)案例分析与代码示例
四、Uni Debug 的 API 介绍
(一)Emulator 常用 API
(二)内存常用 API
(三)VN 常用 API
五、Uni Debug 的 Hook 机制
(一)内置第三方 Hook 框架
(二)原生 Hook 方式
六、Uni Debug 的 Console Debug 功能
七、Uni Debug 的 Patch 功能
在 Android 逆向工程领域,理解和掌握相关工具及技术至关重要。本文将深入探讨 Uni Debug,为大家详细介绍其原理、使用方法及应用场景。
Unicorn 是由新加坡南洋理工大学团队在 2015 年开源的 CPU 模拟器框架。它支持多种架构,如 X86、X64、arm、m64 等。其主要特点包括:
Uni Debug 是一个开源的轻量级模拟器,主要用于执行安卓平台上的 native 代码,由凯神在 2019 年开源,基于 Unicorn 构建,用 Java 语言编写,可在 IDE 打开和运行。它在 Android 逆向分析中具有重要地位:
其优势在于提供了隐蔽的监控手段,可模拟复杂的 native 环境,且由于开源特性,得到社区广泛支持和持续更新,成为安卓 native 逆向分析领域的有力工具。
以下是一个基于 Uni Debug 的简单案例,用于分析一个安卓应用中的 native 函数执行过程。
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.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
public class UniDebugExample {
public static void main(String[] args) {
// 创建 32 位安卓模拟器实例
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit().build();
// 获取内存管理接口
Memory memory = emulator.getMemory();
// 设置系统库版本为 23(对应安卓 6.0)
memory.setLibraryResolver(new AndroidResolver(23));
// 创建 Dalvik 虚拟机实例
VM vm = emulator.createDalvikVM(null);
// 设置是否详细输出 JNI 调用日志
vm.setVerbose(true);
// 加载指定路径的 so 文件,这里假设 so 文件名为 libexample.so
Module module = vm.loadLibrary(new File("path/to/libexample.so"), false);
// 以下是模拟调用 native 函数的示例
DvmClass dvmClass = vm.resolveClass("com/example/MyClass");
DalvikModule dalvikModule = vm.getDalvikModule("com.example.MyClass");
// 假设要调用的 native 函数名为 nativeMethod,其签名为 (Ljava/lang/String;)Z
Object result = dalvikModule.callStaticJniMethod(emulator, "nativeMethod", "input_string");
System.out.println("Function call result: " + result);
// 释放资源
emulator.close();
}
}
在上述代码中:
nativeMethod
的 native 函数,并传入一个字符串参数 input_string
。emulator.getMemory()
可以获取内存管理接口,用于后续的内存操作,如内存分配、读取和写入等。emulator.getPid()
可获取当前模拟器进程的 ID,在多进程分析场景中具有重要作用。emulator.createDalvikVM()
用于创建 Dalvik 虚拟机实例,为执行安卓应用的代码提供运行环境。emulator.getVM()
可以获取已经创建的虚拟机对象,方便对虚拟机进行进一步的操作和管理。emulator.getCurrentRegisters()
能够获取当前寄存器的值,对于分析代码执行状态和数据流向非常有帮助。emulator.getBackendProcessName()
可获取后端 CPU 进程的名称,有助于了解代码执行的底层环境。memory.setLibraryResolver()
用于设置内存的系统库版本,确保模拟环境与目标应用的兼容性。memory.getPointer()
可以获取当前内存指针的值,在内存操作中起到关键作用。memory.getMemoryObject()
可获取内存对象,通过该对象可以进行更详细的内存操作,如内存映射、模块地址查找等。memory.getMemoryMap()
用于获取内存的映射情况,帮助开发者了解内存的分配和使用状态。memory.loadLibrary()
可加载指定的 so 文件到内存中,为函数调用和代码执行提供必要的库支持。vm.createApk()
可创建与虚拟机关联的 APK 文件,在分析应用的 APK 相关资源和逻辑时非常有用。vm.setVerbose()
用于设置是否输出 JNI 的运行日志,方便开发者在调试过程中查看详细的 JNI 调用信息。vm.loadLibrary()
加载 so 模块到虚拟机中,确保相关函数和代码能够在模拟环境中正确执行。vm.getJNIEnv()
可获取 JNIENV 指针,这是进行 JNI 调用和操作的关键。vm.getJavaVM()
用于获取 Java VN 指针,在 Java 与 native 代码交互过程中起到重要作用。Uni Debug 内置了一些第三方的 Hook 框架,如 Doby(前身是 Hook JJ)、While 和 X Hook。
Uni Debug 基于 Unicorn 引擎开发了原生的 Hook 方式,如利用 Unicorn 本身实现指令集的 Hook、块级内存读写异常 Hook 等,并封装了 Console Debug。
以下是一个使用 Uni Debug 原生 Hook 方式的示例代码:
import com.github.unidbg.Emulator;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.pointer.UnicornPointer;
public class UniDebugHookExample {
public static void main(String[] args) {
Emulator emulator = // 创建模拟器实例的代码
// 使用 HookZz 进行函数 Hook
HookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x1234, new ReplaceCallback() {
@Override
public void preCall(HookContext context, UnicornPointer[] args) {
// 在函数调用前的操作,例如获取参数值
int arg1 = args[0].getInt(0);
System.out.println("Before function call, arg1: " + arg1);
}
@Override
public void postCall(HookContext context, UnicornPointer[] args, UnicornPointer retval) {
// 在函数调用后的操作,例如获取返回值
int returnValue = retval.getInt(0);
System.out.println("After function call, return value: " + returnValue);
}
});
// 执行相关代码,触发 Hook 操作
emulator.close();
}
}
在上述代码中:
HookZz.getInstance()
获取 HookZz 实例。wrap
方法对指定地址(module.base + 0x1234
)的函数进行 Hook,通过实现 ReplaceCallback
接口的 preCall
和 postCall
方法,分别在函数调用前和调用后执行自定义的操作,如获取参数值和返回值,并进行输出。Console Debug 是 Uni Debug 提供的强大工具,允许用户在模拟执行过程中设置断点、单步调试、查看和修改内存以及寄存器的操作,从而深入分析目标程序的行为。
以下是一个 Console Debug 的使用示例:
import com.github.unidbg.Emulator;
import com.github.unidbg.Debugger;
import com.github.unidbg.Module;
public class UniDebugConsoleExample {
public static void main(String[] args) {
Emulator emulator = // 创建模拟器实例的代码
// 获取 Debugger 实例
Debugger debugger = emulator.getDebugger();
// 附加到目标进程或模块
debugger.attach(module);
// 添加断点,假设断点地址为 0x5678
debugger.addBreakpoint(0x5678);
// 运行模拟器
emulator.run();
// 当程序断在断点处时,可以进行以下操作
// 查看寄存器值,例如查看 X0 寄存器的值
int x0Value = debugger.readRegister("X0");
System.out.println("X0 register value: " + x0Value);
// 查看内存值,假设查看地址 0x1234 处的内存值
byte[] memoryValue = debugger.readMemory(0x1234, 4);
System.out.println("Memory value at 0x1234: " + Arrays.toString(memoryValue));
// 继续执行程序
debugger.continueExecution();
emulator.close();
}
}
在上述代码中:
Debugger
实例,通过 attach
方法附加到目标模块。addBreakpoint
方法在指定地址添加断点,然后运行模拟器。readRegister
方法读取寄存器的值,使用 readMemory
方法读取指定地址的内存值,并进行输出。最后可以使用 continueExecution
方法继续执行程序。Uni Debug 的 Patch 功能主要用于对二进制文件进行修改,有两种形式:一是在文件系统中修改二进制文件,二是在内存中修改。其应用场景广泛,在某些情况下比传统的 Patch 方法更具优势。
以下是一个简单的 Patch 示例代码:
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.patcher.Patch;
import com.github.unidbg.patcher.PatchContext;
public class UniDebugPatchExample {
public static void main(String[] args) {
Emulator emulator = // 创建模拟器实例的代码
// 创建 Patch 实例并进行配置
Patch patch = new Patch() {
@Override
public void patch(PatchContext context) {
// 假设要将地址 0x8765 处的指令修改为特定的汇编指令
context.writeMemory(0x8765, new byte[]{0x12, 0x34, 0x56});
}
};
// 应用 Patch
patch.apply(emulator);
// 加载目标模块,假设目标模块名为 libtarget.so
Module module = emulator.loadLibrary("libtarget.so", false);
// 获取目标函数并执行,假设目标函数名为 targetFunction
// 这里需要根据实际情况获取函数签名和参数等信息
// 以下是一个示例,实际可能需要调整
Object result = module.callFunction("targetFunction", "input_param");
// 输出结果以查看 Patch 效果
System.out.println("Function call result after patch: " + result);
emulator.close();
}
}
在上述补充代码中:
emulator.loadLibrary
加载目标 so
文件模块,确保目标函数所在的模块已被正确加载到模拟器环境中。module.callFunction
调用目标函数 targetFunction
,并传入一个示例参数 input_param
。这里的函数名和参数需要根据实际被 Patch
的目标函数情况进行修改。Patch
是否达到了预期的效果,即是否改变了目标函数的行为。请注意,在实际使用中,需要根据具体的目标二进制文件和要修改的内容准确地设置 Patch
的地址和指令,以及正确地调用相关函数和处理参数。