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 与 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;
}

用 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

除了基本类型,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 返回的结果

除了获取 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/image6.png

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

word/media/image7.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 函数

在 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 中注册 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/image8.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 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);

        // 清理资源
        try {
            emulator.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    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

### 使用 Unidbg 加载调用 Android SO 动态库 #### 准备工作 为了使用 Unidbg 调试加载 Android 的 `.so` 文件,需先设置好开发环境。确保已安装 Java 开发工具包 (JDK),Unidbg 库加入项目依赖。 #### 创建 Unidbg 实例 创建一个新的 `Unidbg` 对象实例用于模拟 ARM 架构下的执行环境: ```java // 初始化 Unidbg 实例 Unidbg unidbg = new Unidbg(ArchEnum.ARM); ``` #### 设置文件系统路径映射 为了让被调试的应用程序能够找到所需的共享对象(`.so`),需要配置虚拟文件系统的根目录以及应用程序私有数据存储位置: ```java // 配置 so 文件所在的真实路径到 /data/app-lib/ 下的映射关系 unidbg.setAppPath("/path/to/apk"); unidbg.addFileSystem("/path/to/lib"); // 存放 .so 文件的实际路径 ``` #### 注册必要的模块和服务 某些情况下可能还需要注册特定的服务接口或实现类以便于更好地支持目标应用的功能需求: ```java // 如果需要的话可以在这里添加自定义服务或其他初始化操作 unidbg.getEmulator().addService(new MyCustomService()); ``` #### 加载指定的动态链接库 通过 `loadLibrary()` 方法传入要分析的目标 `.so` 名字来进行加载动作: ```java Dlfcn dlopen = unidbg.loadLibrary("libexample.so", Dlfcn.class); // libexample.so 是待测试的具体 so 文件名 ``` #### 定义回调函数处理 API 请求 当遇到未实现的标准 C/C++ 运行时函数或者其他外部符号引用时,可以通过重写相应的方法来自定义行为逻辑: ```java public class HookedFunction implements Function { @Override public ReturnAction hook(Emulator<?> emulator, long originFunction) { Memory memory = emulator.getMemory(); // 获取参数列表 Stack stack = ((ARM32RegisterContext)emulator.getContext()).getStack(); Pointer ptrArg0 = stack.popPointer(); Log.d("HookedFunction", "Called with arg0=" + ptrArg0.getStringUtf8()); // 返回期望的结果给原生层 return new ReturnAction(emulator).returnNow(0L /* 成功 */); } } ``` #### 执行具体功能 最后一步就是触发想要研究的那个导出函数入口点了。这通常涉及到构造合适的输入参数通过指针传递进去,之后读取返回值完成整个交互过程: ```java long addressOfTargetFunc = dlopen.dlsym(dlopen.dlopen("libexample.so"), "target_function_name"); if(addressOfTargetFunc != 0){ RegisterContext context = unidbg.getEmulator().getContext(); // 填充 R0 寄存器作为第一个参数... context.setR0(/* 参数1 */); // ...其他寄存器也可能用来携带更多参数... // 让 CPU 开始运行直到该地址处指令被执行完毕 unidbg.runUntilAddress(addressOfTargetFunc); } else { throw new RuntimeException("Failed to find symbol 'target_function_name'"); } // 取得结果 long resultValue = context.getR0(); // 或者根据实际情况调整获取方式 Log.i("Result:", Long.toString(resultValue)); ``` 以上即为利用 Unidbg 工具集加载调用 Android 平台上的本地二进制组件的一个基本流程概述[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值