这里单独记录用的比较多的Unidbg代码片段.
Android逆向-Unidbg Cheatsheet
标准模板
代码模板
1 | package com.kanxue.test2; |
初始化
1 | // 创建模拟器实例,建议使用实际进程名,可以规避进程名校验 |
导入包
1 | // 导入通用且标准的类库 |
开启日志
1 | Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG); |
安卓模拟器(AndroidEmulator)
创建安卓模拟器(AndroidEmulator)
1 | import com.github.unidbg.AndroidEmulator; |
创建Dalvik虚拟机(VM)
1 | VM createDalvikVM(); |
创建内存(Memory)
1 | // 创建模拟器内存接口 |
启动多线程支持
1 | emulator.getSyscallHandler().setEnableThreadDispatcher(true); |
跟踪(trace)
1 | emulator.traceWrite(0xaaaaa, 0xdddddd) |
Dalvik虚拟机(VM)
输出DEBUG日志
1 | vm.setVerbose(true) |
加载Java类
1 | DvmClass cNative = vm.resolveClass("com/xxx/xxx"); |
加载so模块(DalvikModule)
1 | // 加载 so 到虚拟内存,第二个参数的意思表示是否执行动态库的初始化代码 |
执行init
vm.loadLibrary
第二个参数的意思表示是否执行动态库的初始化代码,即init段的函数。
设置JNI(调用so必须开启这个选项)⭐
1 | // 设置 JNI |
打印debug日志
1 | // 打印日志 |
内存(Memory)
解析安卓系统API
1 | // 设置系统类库解析 |
DalvikModule
创建DalvikModule
1 | // 加载 so 到虚拟内存,第二个参数的意思表示是否执行动态库的初始化代码 |
获取Module句柄
1 | // 获取 so 模块的句柄 |
有关获得具体的Module:
1 | // 获取某个具体SO的句柄 |
调用JNI_Onload函数
1 | // 调用 JNI_Onload |
Module
获取模块基地址
1 | module.base |
如果只主动加载一个SO,其基址恒为0x40000000 ,这是一个检测Unidbg的点,因而可以在 com/github/unidbg/memory/Memory.java 中做修改:
1 | public interface Memory extends IO, Loader, StackMemory { |
获取函数地址
获取导出函数地址
1 | // 加载so到虚拟内存 |
获取非导出函数地址
1 | // 加载so到虚拟内存 |
主动调用
调用JNI_Onload函数
1 | // 调用 JNI_Onload |
调用Native函数(构造传参表)(有符号or无符号,给地址)
构造函数原型和传参一直都是写这种脚本的头号问题,之前写Xposed插件,Frida插件时也遇到过这种问题.这里我也有点没搞懂,不过现就放在这里吧.
如果拥有完整APK的话,应该可以通过Jadx在smali里面直接找到现成定义.
1 | // 构建函数参数格式 |
下面是一个另一个例子:
1 | int __fastcall Java_com_kanxue_test2_MainActivity_jnitest(JNIEnv *env, jclass cls, jstring str); |
下面是成功执行的脚本:
有符号
有符号也可以有两种形式:
1 |
|
以及:
1 | Module mod = dm.getModule(); |
无符号,传地址(万金油)
没有符号的话就只能选择直接传入地址。
但是这样的话就需要注意,如果是THUMB指令,Hook时需要在IDA上的地址上再+1,Patch时不需要
1 | Module mod = dm.getModule(); |
jclass/jobject⭐
函数第二个参数有可能就是这个,不过由于大概率不会用上,因此平常直接加个0就好了。
1 | args.add(0) |
调用JNI方法
上面刚刚其实也是对一个JNI方法进行了尝试,但是是把他当成一个一般函数来使用的。下面使用JNI方法的专用调用方法,来简化过程:
1 | DvmObject<?> obj = ProxyDvmObject.createObject(vm, this); |
这里创建Dvm对象的时候,有个this
,是把这个脚本的类映射到了虚拟机里面。因而这样调用JNI方法,实际上就确实是这个以这个脚本的名义去调用。
除此之外还有其他方法。
调用静态JNI方法
通过vm.resolveClass
获取类对象,然后通过callStaticJniMethodObject
方法来执行。
1 | private final DvmClass TTEncryptUtils; |
又有:
1 | private DvmClass MainActivityUtils; |
函数调用返回值
上接构造参数,并强制调用函数一节.最后通过module.callFunction
强制调用了一个Native函数,并得到了返回值number
.
1 | // 获取最终返回值,同时运行过程中的汇编代码和寄存器值会写入到文件中 |
Trace(很好用!)
1 | // unicorn trace(贼好用!!!堪比 ida trace!!!) |
这样,在模拟执行so的时候,就会把每一步执行的trace流也给记录下来.后期排查的时候也很好用.
Symbol获取so符号表
只要在IDA的Names界面里面有,就可以导出符号。
1 | Symbol sbox0 = module.findSymbolByName("sbox0"); |
通过符号得到地址引用
1 | Symbol sbox0 = module.findSymbolByName("sbox0"); // 在libttEncrypt.so模块中查找sbox0导出符号 |
指针
创建指针
1 | // Memory.pointer方法 |
从指针读取数据
get
作为前缀的一系列方法:
1 | byte[] sbox0_array = sbox0_ref.getByteArray(0, 256); // 从地址指针获取数据 |
修改数据
setInt
等一系列方法:
1 | public void patchVerify(){ |
写入数据
1 | ptr.write("aaa".getBytes()); |
Inspect监视内存数据
1 | Inspector.inspect(data, "ttEncrypt"); |
IDA附加
1 | if (logging) { |
好像暂时只能附加IDA7.4的
Hook
HookZz wrap&instrument
这是最好用的。就用这个。
1 | IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz |
以及
1 | public void hook_312E0(){ |
Dobby replace
1 | Dobby dobby = Dobby.getInstance(emulator); |
xHook PLT hook
1 | IxHook xHook = XHookImpl.getInstance(emulator); // 加载xHook,支持Import hook,文档看https://github.com/iqiyi/xHook |
Unicorn Hook
用Unicorn自带的Hook code方法。
Android逆向-实战so分析-某洲_v3.5.8_unidbg学习_android逆向so分析_哔哩哩!的博客-CSDN博客
这是一个代码级别Hook
1 | emulator.getBackend().hook_add_new(new CodeHook() { |
获取寄存器的值
1 | public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { |
执行引擎
Dynarmic
1 | private static AndroidEmulator createARMEmulator() { |
Unicorn2
默认情况下自动使用
1 | private static AndroidEmulator createARMEmulator() { |
Unicorn2的执行速度比Dynarmic应该有慢几十倍。Dynarmic跑一个爆破结果5s能结束,Unicorn2跑了1分钟多没反应。
参数构造
1 | // 构建函数参数格式 |
TreeMap
1 | public void s(){ |
Int封装类
1 | DvmInteger input2_3 = DvmInteger.valueOf(vm, 2); |
ArrayObject
1 | List<Object> list = new ArrayList<>(10); |
汇编Patch
简单点的:
1 | public void patchVerify(){ |
复杂点的考虑用Keystone做辅助:
1 | public void patchVerify1(){ |
补环境⭐
一般是补全AbstractJni里的方法。如果一个so调用了自定义的方法,Unidbg没有实现,那么就得自己去补全。
两种方法:
- 在源码
AbstractJni.java/callStaticObjectMethodV
里面补全,添加一个switch-case。 - 在脚本里面补全,因为为了调用Jni方法,首先主类必须继承AbstractJni。效果是一样的。
1 | case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;": { |
然后具体看一下该补什么:用Jadx看一下代码:
1 |
|
Proxy什么的我们不管,那么最后返回的应该是正常的getApplicationContext
。
1 |
|
1 |
|
1 |
|
还可以单独写个utils.java
来直接在Java里面实现一些函数的功能(直接赋值jadx的伪代码)
1 |
|
反射
主类不继承AbstractJni,同时不用vm.setJni(this)
使用:
1 | vm.setDvmClassFactory(new ProxyClassFactory()); |
然后把缺失的Java类直接从Jadx里面复制过来,以包的形式复制到com/或者相应的文件夹下。
文件读写
重定向到了
1 | C:/users/<>/AppData/Local/Temp/rootfs/default |
也可以让主类自行实现IOResolver接口:
1 | // 1 |
SharedPreferences
1 | <?xml version='1.0' encoding='utf-8' standalone='yes' ?> |
1 |
|
Syscall Open
开启下面的系统调用Logger,然后这里实现:
1 |
|
两种方式补文件访问
1是Unidbg提供的rootfs虚拟文件系统,2是代码方式文件重定向 大家自行选择,有人可能会问,如果我不想传入文件,能不能只传入”字符串“,当然可以,从SimpleFileIO换成ByteArrayFileIO即可。
APK资源文件加载
可能会加载libandroid.so
这样没有被Unidbg实现的so。
- Patch/Hook 这个不支持的SO所使用的函数
- 使用Unidbg VirtualModule
1 | // 加载时间要在模块so之前 |
系统调用
1 | Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG); |
自定义系统调用
【精选】Unidbg适合做算法还原吗?(一)_douyin unidbg_白龙~的博客-CSDN博客
1 | package com.learnproperty; |
调试器
1 | // 初始化debugger |
1 | c: continue |
疑问
- init段的执行是在
vm.loadLibrary
时指定的,那么如果init段里面有函数需要被hook什么的该怎么处理,能够提前使用Unicorn Hook嘛?(已解决,在问题总结里) - JNI函数都是自动Log的(通过setVerbose),那么能不能自定义JNI行为?(Frida我就没找到方法去进一步自定义JNIEnv里的函数)
问题总结
Unidbg 问题汇总(一)_unidbg byte-CSDN博客
Long参数的传递
在编译成arm32的SO时,long一定概率会被转成两个int.
jbytearray 怎么查看
Frida:
1 | hexdump(ptr(Java.vm.tryGetEnv().getByteArrayElements(args[0]))) |
Unidbg:
1 | public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { |
std::string 的读写
1 | public String readStdString(Pointer strptr){ |
TraceCode为什么trace不到module init中的指令
init段的自动加载是在loadLibrary
函数里面的。然而一般实现执行这个函数,再执行traceCode
。
因此可以第一次先用loadLibrary
得到基址和结束地址,然后调换一下位置。
HOOK 框架使用问题
Unidbg作者在注释中写道:HookZz在arm32位上支持较好,Dobby在64位上支持较好.(因此将两者,或者说Dobby以及其前身HookZz作为两个独立Hook工具)
类型继承
需要打通继承链,否则容易莫名其妙报错,找不到报错的位置
1 | DvmClass Map = vm.resolveClass("java/util/Map"); |
getenv
Unidbg的env默认是空的,如果有so里面调用getenv
这种函数,那就可能会报错。
二选一:
1 | public void setEnv(){ |
1 | public void hookgetEnv(){ |
其他
有关ARM&Thumb指令
Thumb指令下,Hook时需要地址+1,而patch时不需要。
参考
逆向工具之unidbg(在pc端模拟执行so文件中的函数)_so逆向添加函数_hestyle的博客-CSDN博客
Android逆向-实战so分析-某洲_v3.5.8_unidbg学习_android逆向so分析_哔哩哩!的博客-CSDN博客
SO逆向入门实战教程二:calculateS_nativenewcalculates 算法_白龙~的博客-CSDN博客
unidbg 简介、基本使用、调用so中方法、unidbg-web_unicorn unidbg-CSDN博客(用的版本太老了,很多的代码不具有参考价值)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/08/28/Android逆向-Unidbg-Cheatsheet/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!