Android逆向18-Unicorn
Android逆向18-Unicorn
来自吾爱破解-正己
https://www.52pojie.cn/thread-1701353-1-1.html
Unicorn 是一个由新加坡南洋理工大学团队在2015年开源的CPU模拟器框架,它支持多种架构,包括X86/X64/ARM/ARM64/MIPS等。Unicorn 的主要特点是:
- 多架构支持:它能够模拟不同架构的CPU指令,这使得它在跨平台分析和测试中非常有用。
- 高性能:Unicorn 通过使用JIT(Just-In-Time)编译技术,将模拟的指令翻译成宿主机的本地指令,从而提高了执行效率。
- 丰富的接口:Unicorn 提供了多种语言的绑定,包括Python、Java、C#等,使得在不同编程环境中都能方便地使用。
- Hook和拦截功能:Unicorn 允许用户在模拟执行过程中设置Hook,拦截和处理特定的指令或内存访问,这对于逆向工程和动态分析非常有用。
- 专注于CPU模拟:与一些既模拟CPU又模拟操作系统的模拟器不同,Unicorn 专注于CPU指令的模拟,提供了简单的Hook接口、内存操作接口和指令执行接口,而不涉及操作系统层面的模拟。
什么是unidbg?
Unidbg(Unicorn Debugger)是一个开源的轻量级模拟器,主要设计用于模拟执行Android平台上的Native代码。它由凯神在2019年开源,基于Maven构建,使用Java语言编写,可以在IDE中打开和运行。Unidbg能够模拟Android Native函数的执行,让逆向工程师和安全研究人员能够分析和理解二进制文件的运行行为。它支持模拟系统调用和JNI调用,使得可以在模拟环境中执行依赖这些调用的代码。Unidbg基于Unicorn项目,Unidbg的优势在于它提供了一种隐蔽的监控手段,可以模拟复杂的Native环境,帮助用户进行深入的动态分析。由于其开源特性,Unidbg得到了社区的广泛支持和持续更新,成为了Android Native逆向分析领域中一个强有力的工具。
竞争者: AndroidNativeEmu 和继任者 ExAndroidNativeEmu (Unidbg优点:模拟实现了更多的系统调用和 JNI)
unidbg的使用场景与优缺点
特性 | 描述 |
---|---|
使用场景 | |
模拟执行 | 执行目标 SO 文件中用户关注的函数,获取与真机等价的结果;替代 Frida/Xposed Call 进行 RPC 调用。 |
监控观察 | 观察样本对环境的信息获取与修改;监控所有类型的外部信息访问,包括系统调用、库函数、JNI 调用、文件读写等。 |
辅助算法分析和还原 | 提供 Hook/Debug/Trace 等分析能力,结合时间旅行调试器(Time-Travel Debugging),无疑是Android Native 上强大的分析神器。 |
优点 | |
低成本 | 减少设备成本和改机成本,无需购置和维护大量真机或租借云手机。 |
灵活性 | 可以模拟或代{过}{滤}理所有函数调用接口,方便模拟设备环境变化。 |
监控能力 | 能够监控 Native 层的详细执行流,包括 JNI 调用和文件访问。 |
分析能力 | 结合时间旅行调试器,提供强大的算法分析和还原能力。 |
缺点 | |
学习成本高 | 尤其是环境补全(补环境)部分,如果补得不好,即使跑出结果也无法使用。 |
执行速度慢 | 基于 Unicorn 的模拟执行速度相比真机慢很多,尽管有 Dynarmic 等方案可以提高速度,但牺牲了部分辅助算法还原的能力。 |
功能限制 | 没有为特定场景做专门的优化,也没有提供配置管理功能;没有实现对所有系统调用的良好模拟,可能导致某些逻辑处理失败。 |
扩展性差 | 作为一个 Java 项目,Unidbg 无法作为 IDA 或 Ghidra 插件,也难以轻松嵌入到其他项目中,不如 Python 项目灵活。 |
1 | ├── README.md # 项目介绍和使用指南 |
1 | log4j.logger.com.github.unidbg.linux.file=DEBUG //把INFO改成DEBUG |
项目式学习
1 | package com.kanxue.test2; |
1 | emulator = AndroidEmulatorBuilder.for32Bit() |
- 1.for32Bit()的意思是创建32位Android模拟器实例,for64Bit()则是创建64位,apk lib 里只有armeabi-v7a,那就只能选择 32 位,apk lib 里只有arm64-v8a,就选择 64 位。区别:64位的执行速度较快,浮动10%左右;Unidbg 对 ARM32 的支持和完善程度高于 ARM64
- 2.Unidbg 支持了数个后端,目前共五个 Backend,分别是 Unicorn、Unicorn2、Dynarmic(执行速度较快)、Hypervisor、KVM。new DynarmicFactory(true)中的true,标志着在出现异常时是否使用默认后端unicorn。
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) |
void |
Trace 读内存操作。 |
traceWrite(long begin, long end) |
void |
Trace 写内存操作。 |
traceCode(long begin, long end) |
void |
Trace 汇编指令执行。 |
isRunning() |
boolean |
判断当前 Emulator 是否正在运行。 |
1 | LibraryResolver resolver = new AndroidResolver(23); |
23 和 19 分别对应于 sdk23(Android 6.0) 和 sdk19(Android 4.4)的运行库环境,处理 64 位 SO 时只能选择 SDK23。
memory常用Api
方法名 | 返回类型 | 描述 |
---|---|---|
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
1 | // 创建Dalvik虚拟机实例 |
方法名 | 返回类型 | 描述 |
---|---|---|
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 。 |
- 1.
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"))
-创建虚拟机并指定APK文件,加载指定APK文件,unidbg可以帮我们完成一些小操作,例如:解析 Apk 基本信息,Apk 的版本名、版本号、包名、 Apk 签名等信息,减少补环境操作;解析和管理 Apk 资源文件,加载 Apk 后可以通过openAsset
获取 APKassets
目录下的文件。 - 2.
loadLibrary
三个重载方法
1 | /** |
jni函数的调用
1 | //第一个参数传入模拟器实例 |
基本类型直接传递,int、long、boolean、double 等。
下面几种对象类型unidbg也帮我们封装好了
String
byte 数组
short 数组
int 数组
float 数组
double 数组
Enum 枚举类型
特殊参数构造:对于其他数据类型需要借助resolveClass
构造,例如Context
1 | DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null); |
符号调用与偏移调用
符号调用
1 | Symbol symbol = module.findSymbolByName("导出符号"); |
偏移调用
1 | //第一个模拟器实例,第二个偏移地址(thumb记得+1),第三个jnienv,第四个jclass,第五个可变参数 |
unidbg之hook
类别 | 描述 | 优点 | 缺点 |
---|---|---|---|
内置的第三方 Hook 框架 | 包括 Dobby(前身为 HookZz)、Whale 和 xHook 等。HookZz对于32位支持较好,Dobby64位,XHook不能Hook Sub_xxx 子函数。 | - 功能丰富:支持多种 Hook 方式,如 Inline Hook 和 PLT Hook。 - 易于使用:提供简洁的 API 接口,便于快速上手。 |
- 可能被检测:某些应用可能检测到这些 Hook 框架的存在。 - 局限性:Inline Hook 在短函数或相邻地址的函数中可能出现问题;PLT Hook 无法 Hook 非导出函数。 |
基于 Unicorn 引擎的原生 Hook 功能 | 利用 Unicorn 引擎实现的指令级Hook,块级Hook,内存读写Hook,异常Hook等功能,Unidbg 在此基础上还封装了 Console Debugger。 | - 隐蔽性强:原生 Hook 方式更难被应用检测到。 - 灵活性高:可对任意代码位置进行 Hook,无特定限制。 |
- 使用复杂度较高:需要深入理解底层机制,配置相对复杂。 |
HookZz&Dobby
1 | // 获取 HookZz 实例,用于后续的 Hook 操作 |
获取 HookZz 实例:
1 | IHookZz hookZz = HookZz.getInstance(emulator); |
wrap_hook函数:
1 | hookZz.wrap(functionAddress, new WrapCallback<RegisterContext>() { |
wrap
函数有两个重载,一个基于符号寻址,一个基于地址寻址,本质没区别,符号寻址的最终也是会调用symbol.getAddress()
参数里的WrapCallback的泛型接口有三个RegisterContext(函数 Hook)
、HookZzArm32RegisterContext(针对ARM32位)
和HookZzArm64RegisterContext(针对ARM64位)
因为可以访问某个寄存器的值,所以适用于inline hook
而在HookZzArm64RegisterContext
中则是通过以下的方法去获取对应的寄存器的值
instrument_inline_hook函数
1 | // 使用 HookZz 的 instrument 方法对特定地址的指令进行 Inline Hook, thumb记得+1 |
replace替换函数
1 | // 使用 dobby 的 replace 方法替换 "Java_com_zj_wuaipojie_util_SecurityUtil_diamondNum" 函数的实现 |
1 | emulator.getBackend().hook_add_new(new CodeHook() { |
Console Debugge
Console Debugger(控制台调试器)是 Unidbg 提供的一个强大工具,允许用户在模拟执行过程中设置断点、单步调试、查看和修改内存及寄存器等操作,从而深入分析目标程序的行为。
1 | Debugger attach = emulator.attach(); |
替换返回值
1 | emulator.attach().addBreakPoint(module.findSymbolByName("verifyApkSign").getAddress(), new BreakPointCallback() { |
命令 | 功能说明 | |
---|---|---|
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 函数 |
unidbg之patch
Patch 就是直接对二进制文件进行修改,Patch本质上只有两种形式
- patch 二进制文件
- 在内存里 patch
Patch的应用场景很多,在一些场景比Hook更好用,这就是需要介绍它的原因。Patch 二进制文件的形式是大多数人所熟悉的,在IDA中使用KeyPatch打补丁的体验很友好。这里主要介绍unidbg的内存Patch。
1 | UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x1146C); |