unidbg 加载 so 并调用 so 中函数
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
unidbg 简介
unidbg 是一个基于 Java 的 Android 动态分析框架,专注于模拟 Android 应用中的 native 代码。以下是它的主要功能:
支持对so的加载;
支持对JNI接口函数的模拟调用;
支持常见syscalls的模拟调用;
支持ARM32和ARM64。
支持Android以及iOS
基于HookZz实现的inline hook
xHook实现的hook
iOS fishhook,substrate、whale hook等
支持gdb、IDA远程调试等高级功能。
…
Unidgb 项目地址:https://github.com/zhkl0228/unidbg
unidbg 底层引擎
unidbg 的 CPU 指令模拟主要由 dynarmic 和 unicorn 驱动。
dynarmic 是专注于 ARM 架构的模拟器,虽然它和 unicorn 都提供 ARM 模拟功能,但它们的设计目标和性能特点不同。unicorn 更加通用,而 dynarmic 则更注重效率。
dynarmic 项目地址:https://github.com/yuzu-mirror/dynarmic
Unicorn 项目地址:https://github.com/unicorn-engine/unicorn
dynarmic 与 unicorn 效率对比
clone unidbg 源码到本地并导入 IDEA,在 unidbg-android/src/test/java/com/kanxue/test2
MainActivity.java 中示例代码实现了爆破 so 中 test 函数 flag 参数(已知 so 中 test 函数参数为一个字符串,该字符串长度为3且仅包含大小写字母),如果 flag 正确函数会返回 true,通过不断调用该 jni 函数测试所有字母组合直到得到正确结果。
package com.kanxue.test2;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.arm.backend.DynarmicFactory;
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.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import java.io.File;
/**
* <a href="https://bbs.pediy.com/thread-263345.htm">CrackMe</a>
*/
public class MainActivity {
public static void main(String[] args) {
long start = System.currentTimeMillis();
MainActivity mainActivity = new MainActivity();
System.out.println("load offset=" + (System.currentTimeMillis() - start) + "ms");
mainActivity.crack();
}
private final AndroidEmulator emulator;
private final VM vm;
private MainActivity() {
emulator = AndroidEmulatorBuilder
.for32Bit()
.addBackendFactory(new DynarmicFactory(true))
.build();
Memory memory = emulator.getMemory();
LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver);
vm = emulator.createDalvikVM();
vm.setVerbose(false);
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/armeabi-v7a/libnative-lib.so"), false);
dm.callJNI_OnLoad(emulator);
}
private static final char[] LETTERS = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
};
private void crack() {
DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
long start = System.currentTimeMillis();
for (char a : LETTERS) {
for (char b : LETTERS) {
for (char c : LETTERS) {
String str = "" + a + b + c;
boolean success = obj.callJniMethodBoolean(emulator, "jnitest(Ljava/lang/String;)Z", str);
if (success) {
System.out.println("Found: " + str + ", off=" + (System.currentTimeMillis() - start) + "ms");
return;
}
}
}
}
}
}
代码中默认使用 dynarmic 引擎,如果我们注释掉下面这行代码默认会使用 unicorn。
.addBackendFactory(new DynarmicFactory(true))
当不使用 dynarmic 引擎时,JNI 函数执行时间是 102709 毫秒
load offset=1141ms
Found: XuE, off=102709ms
当使用 dynarmic 引擎时,jni 函数执行时间是 1647 毫秒,对于需要频繁调用 JNI 函数的情况而言效率提升近百倍
load offset=2703ms
Found: XuE, off=1647ms
加载 so 并调用 init、init_array
在 C/C++ 中,init 函数通常是动态库链接时执行的函数,而 init_array 是一个特殊的数组,用于指定在库加载时执行的一系列函数。
执行顺序:init 在 init_array 之前。
编写一个带 init 、init_array 方法的的 so,cpp 文件源码如下:
#include <jni.h>
#include <android/log.h>
// Android Log 标签
#define LOG_TAG "Unidbg"
// 定义 init 函数
extern "C" void _init(void) {
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Shared library initialized (init).");
}
// 使用 __attribute__((constructor)) 标记函数为构造函数,库加载时自动调用
__attribute__((constructor))
void init_array1() {
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Shared library initialized (init_array1).");
}
__attribute__((constructor))
void init_array2() {
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Shared library initialized (init_array2).");
}
so 编译完成后,放到 resources 目录下
undbg 默认提供了 android sdk 19 和 23 相关的依赖库
实现流程大概如下:
创建 Android 模拟器
关联 Android 库解析器
创建 Dalvik 虚拟机
调用 loadLibrary 方法加载 so
在调用 loadLibrary 方法时 forceCallInit 参数设置为 true 在加载 so 时就会自动调用 init、init_array 相关方法,重载的 loadLibrary 方法默认 forceCallInit = true,所以也可以不传。
完整代码:
package com.cyrus.example;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
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.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class Demo {
public static void main(String[] args) {
// 创建一个 64 位的 Android 模拟器实例
AndroidEmulator emulator = AndroidEmulatorBuilder
.for64Bit() // 设置为 64 位模拟
.build(); // 创建模拟器实例
// 获取模拟器的内存实例
Memory memory = emulator.getMemory();
// 创建一个库解析器,并设置 Android 版本为 23(Android 6.0)
LibraryResolver resolver = new AndroidResolver(23);
// 将库解析器设置到模拟器的内存中,确保加载库时能够解析符号
memory.setLibraryResolver(resolver);
// 加载共享库 libunidbg.so 到 Dalvik 虚拟机中,并设置为需要自动初始化库
Module module = emulator.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/cyrus/libunidbg.so"));
}
}
运行输出:
[main]I/Unidbg: Shared library initialized (init).
[main]I/Unidbg: Shared library initialized (init_array1).
[main]I/Unidbg: Shared library initialized (init_array2).
调用 so 中函数
示例代码
cpp 源码如下:
#include<cstring>
// 计算6个整数的和
extern "C" int add(int a, int b, int c, int d, int e, int f) {
int sum = a + b + c + d + e + f;
// 使用 Log 打印计算的和
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Sum of integers: %d", sum);
return sum;
}
// 打印字符串和字符串长度,最后返回字符串长度
extern "C" int string_length(const char* str) {
int length = strlen(str);
// 使用 Log 打印字符串和长度
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "String: %s", str);
__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Length: %d", length);
// 返回字符串长度
return length;
}
callFunction
用 IDA 打开 so 可以看到导出方法名称是 add 和 string_length
通过 callFunction 调用 so 中的方法,传参导出函数名和参数,返回值是 Number 具体根据返回类型调用对应的方法。
// 调用 add 方法
Number result = module.callFunction(emulator, "add", 1, 2, 3, 4, 5, 6);
System.out.println(result.intValue());
// 调用 string_length 方法
result = module.callFunction(emulator, "string_length", "abc");
System.out.println(result.intValue());
输出如下:
[main]I/Unidbg: Sum of integers: 21
[main]I/Unidbg: String: abc
[main]I/Unidbg: Length: 3
通过 offset callFunction
除了通过符号来调用,我们还可以通过偏移地址来调用 so 中的函数。
通过 IDA 反汇编可以看到 string_length 函数偏移是 0x25ED0
// 通过地址调用 string_length 方法
result = module.callFunction(emulator, 0x25ED0, "CYRUS STUDIO");
System.out.println("callFunction string_length by offset reuslt: " + result.intValue());
输出如下:
callFunction string_length by offset reuslt: 12
指针对象形式传参
除了基本类型,unidbg 还支持指针对象形式传参
// 使用 memory.malloc 分配 10 字节的内存并写入字符串
MemoryBlock block = memory.malloc(10, true);
UnidbgPointer strPtr = block.getPointer();
strPtr.write("hello".getBytes());
// 读取内存中的字符串
String content = strPtr.getString(0);
System.out.println("Read string from memory: " + content); // 打印读取的字符串
// 调用 string_length 并以为指针对象形式传参
result = module.callFunction(emulator, "string_length", new PointerNumber(strPtr));
System.out.println("Result from string_length function: " + result); // 打印 string_length 返回的结果
读取 X0 寄存器得到返回值
除了获取 callFunction 的返回值,我们还可以通过直接读取 X0 寄存器得到返回值
// 通过直接读取 X0 寄存器得到返回值
Number x0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0);
System.out.println("Value in register x0: " + x0); // 打印寄存器 x0 的值
输出如下:
Read string from memory: hello
Result from string_length function: 5
Value in register x0: 5
JNI 相关调用
在 DalvikVM 中有 _JavaVM 指针和 _JNIEnv 指针,就是用来实现 JNI 接口调用的
JniNativeInterface 一系列接口函数就是在这里实现的
创建 Dalvik 虚拟机实例
// 创建一个 Dalvik 虚拟机实例
VM vm = emulator.createDalvikVM();
// 启用虚拟机的调试输出
vm.setVerbose(true);
1. 调用 JNI_Onload
在 cpp 中实现 JNI_Onload 函数并动态注册 add 方法到 UnidbgActivity
// native add 函数
extern "C" JNIEXPORT jint JNICALL native_add(
JNIEnv* env, jobject thiz, jint a, jint b, jint c, jint d, jint e, jint f) {
// 计算并返回和
return a + b + c + d + e + f;
}
// 动态注册 add 方法
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = nullptr;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 获取 UnidbgActivity 类
jclass clazz = env->FindClass("com/cyrus/example/unidbg/UnidbgActivity");
if (clazz == nullptr) {
return JNI_ERR;
}
// 动态注册 add 方法
JNINativeMethod methods[] = {
{"add", "(IIIIII)I", (void*)native_add}
};
// 注册方法
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
调用 JNI_Onload
vm.callJNI_OnLoad(emulator, module);
在日志输出里可以看到调用了 FindClass 并动态注册了 add 方法
JNIEnv->FindClass(com/cyrus/example/unidbg/UnidbgActivity) was called from RX@0x120011b0[libunidbg.so]0x11b0
JNIEnv->RegisterNatives(com/cyrus/example/unidbg/UnidbgActivity, unidbg@0xe4fff6e0, 1) was called from RX@0x120011f4[libunidbg.so]0x11f4
RegisterNative(com/cyrus/example/unidbg/UnidbgActivity, add(IIIIII)I, RX@0x12000fbc[libunidbg.so]0xfbc)
2. 调用 JNI 函数
Android 示例代码
在 UnidbgActivity 中声明一个静态的 native 方法 staticAdd 和 非静态的 native 方法 stringLength
package com.cyrus.example.unidbg
class UnidbgActivity : AppCompatActivity() {
companion object {
// 加载原生库
init {
System.loadLibrary("unidbg")
}
// 声明静态 native 方法
@JvmStatic
external fun staticAdd(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int): Int
}
external fun stringLength(str: String): Int
}
cpp 中源码如下:
// 实现静态的 native 方法 staticAdd
extern "C" JNIEXPORT jint JNICALL Java_com_cyrus_example_unidbg_UnidbgActivity_staticAdd(
JNIEnv* env, jclass clazz, jint a, jint b, jint c, jint d, jint e, jint f) {
// 计算并返回和
return a + b + c + d + e + f;
}
// 静态注册 stringLength 方法
extern "C" JNIEXPORT jint JNICALL Java_com_cyrus_example_unidbg_UnidbgActivity_stringLength(
JNIEnv* env, jobject thiz, jstring str) {
const char* str_chars = env->GetStringUTFChars(str, nullptr);
int length = strlen(str_chars);
env->ReleaseStringUTFChars(str, str_chars);
return length;
}
Unidbg 代码
在 unidbg 中注册 UnidbgActivity,得到它的 DvmClass
// 注册 UnidbgActivity 类
DvmClass unidbgActivityClass = vm.resolveClass("com/cyrus/example/unidbg/UnidbgActivity");
通过 UnidbgActivity 的 DvmClass 调用静态 JNI 方法
// 调用 Java 类静态方法
int result = unidbgActivityClass.callStaticJniMethodInt(emulator, "staticAdd(IIIIII)I", 1, 2, 3, 4, 5, 6);
System.out.println("staticAdd result:" + result);
创建 Java 对象并调用非静态方法
// 创建 Java 对象
DvmObject unidbgActivity = unidbgActivityClass.newObject(null);
// 调用动态注册的 jni 函数 add(必须在调用 JNI_Onload 之后)
result = unidbgActivity.callJniMethodInt(emulator, "add(IIIIII)I", 1, 2, 3, 4, 5, 6);
System.out.println("add result:" + result);
// 调用 Java 对象方法 stringLength 并传参 String
result = unidbgActivity.callJniMethodInt(emulator, "stringLength(Ljava/lang/String;)I", "hello");
System.out.println("stringLength result:" + result);
调用 JNI 方法时,unidbg 默认会通过 ProxyDvmObject.createObject 把 java 对象映射为 对应的 DvmObject
比如 String 我们也可以通过 StringObject 传参
// 调用 Java 对象方法 stringLength 并通过 StringObject 传参
result = unidbgActivity.callJniMethodInt(emulator, "stringLength(Ljava/lang/String;)I", new StringObject(vm, "hello"));
System.out.println("stringLength(StringObject) result:" + result);
输出如下:
staticAdd result:21
add result:21
stringLength result:5
stringLength(StringObject) result:5
完整代码
package com.cyrus.example;
import com.github.unidbg.*;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import unicorn.Arm64Const;
import com.alibaba.fastjson.util.IOUtils;
import java.io.File;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
// 创建一个 64 位的 Android 模拟器实例
AndroidEmulator emulator = AndroidEmulatorBuilder
.for64Bit() // 设置为 64 位模拟
.build(); // 创建模拟器实例
// 获取模拟器的内存实例
Memory memory = emulator.getMemory();
// 创建一个库解析器,并设置 Android 版本为 23(Android 6.0)
LibraryResolver resolver = new AndroidResolver(23);
// 将库解析器设置到模拟器的内存中,确保加载库时能够解析符号
memory.setLibraryResolver(resolver);
// 加载共享库 libunidbg.so 到 Dalvik 虚拟机中,并设置为需要自动初始化库
Module module = emulator.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/cyrus/libunidbg.so"));
// 调用 add 方法
Number result = module.callFunction(emulator, "add", 1, 2, 3, 4, 5, 6);
System.out.println(result.intValue());
// 调用 string_length 方法
result = module.callFunction(emulator, "string_length", "abc");
System.out.println(result.intValue());
// 使用 memory.malloc 分配 10 字节的内存并写入字符串
MemoryBlock block = memory.malloc(10, true);
UnidbgPointer strPtr = block.getPointer();
strPtr.write("hello".getBytes());
// 读取内存中的字符串
String content = strPtr.getString(0);
System.out.println("Read string from memory: " + content); // 打印读取的字符串
// 调用 string_length 并以为指针对象形式传参
result = module.callFunction(emulator, "string_length", new PointerNumber(strPtr));
System.out.println("Result from string_length function: " + result); // 打印 string_length 返回的结果
// 通过直接读取 X0 寄存器得到返回值
Number x0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0);
System.out.println("Value in register x0: " + x0); // 打印寄存器 x0 的值
// 创建一个 Dalvik 虚拟机实例
VM vm = emulator.createDalvikVM();
// 启用虚拟机的调试输出
vm.setVerbose(true);
// 调用 JNI_Onload
vm.callJNI_OnLoad(emulator, module);
// 调用 JNI 方法
callJniMethod(emulator, vm);
// 清理资源
IOUtils.close(emulator);
}
public static void callJniMethod(AndroidEmulator emulator, VM vm) {
// 注册 UnidbgActivity 类
DvmClass unidbgActivityClass = vm.resolveClass("com/cyrus/example/unidbg/UnidbgActivity");
// 调用 Java 类静态方法
int result = unidbgActivityClass.callStaticJniMethodInt(emulator, "staticAdd(IIIIII)I", 1, 2, 3, 4, 5, 6);
System.out.println("staticAdd result:" + result);
// 创建 Java 对象
DvmObject unidbgActivity = unidbgActivityClass.newObject(null);
// 调用动态注册的 jni 函数 add(必须在调用 JNI_Onload 之后)
result = unidbgActivity.callJniMethodInt(emulator, "add(IIIIII)I", 1, 2, 3, 4, 5, 6);
System.out.println("add result:" + result);
// 调用 Java 对象方法 stringLength 并传参 String
result = unidbgActivity.callJniMethodInt(emulator, "stringLength(Ljava/lang/String;)I", "hello");
System.out.println("stringLength result:" + result);
// 调用 Java 对象方法 stringLength 并通过 StringObject 传参
result = unidbgActivity.callJniMethodInt(emulator, "stringLength(Ljava/lang/String;)I", new StringObject(vm, "hello"));
System.out.println("stringLength(StringObject) result:" + result);
}
}
Android 示例代码地址:https://github.com/CYRUS-STUDIO/AndroidExample