版权归作者所有,如有转发,请注明文章出处: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 与 unicorn 效率对比

clone unidbg 源码到本地并导入 IDEA,在 unidbg-android/src/test/java/com/kanxue/test2

word/media/image1.png

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 目录下

word/media/image2.png

undbg 默认提供了 android sdk 19 和 23 相关的依赖库

word/media/image3.png

实现流程大概如下:

  1. 创建 Android 模拟器

  2. 关联 Android 库解析器

  3. 创建 Dalvik 虚拟机

  4. 调用 loadLibrary 方法加载 so

在调用 loadLibrary 方法时 forceCallInit 参数设置为 true 在加载 so 时就会自动调用 init、init_array 相关方法,重载的 loadLibrary 方法默认 forceCallInit = true,所以也可以不传。

word/media/image4.png

完整代码:

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

word/media/image5.png

通过 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

word/media/image6.png

// 通过地址调用 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 接口调用的

word/media/image7.png

JniNativeInterface 一系列接口函数就是在这里实现的

word/media/image8.png

创建 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

word/media/image9.png

比如 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