GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 模拟独立运行so的方法 Unidbg

非常欢迎来到 Unidbg 的世界!这是 Android 逆向工程中一个极其强大且优雅的工具。下面我将为您提供一份从零开始的 Unidbg 入门指南。

一、Unidbg 是什么?为什么需要它?

想象一下,你在逆向一个 App 时,发现最关键的加密算法不在 Java 层,而是被编译成了原生代码,放在了一个 *.so 动态链接库里。你会怎么办?

  1. 动态调试:使用 Frida 或 IDA 去 Hook、调试。但问题是,App 可能有强大的反调试、反虚拟机、反 Hook 机制,让你的调试器瞬间崩溃。

  2. 静态分析:用 IDA 反编译 so 文件。但如果算法逻辑非常复杂或被虚拟化保护(VMP)、混淆了,分析起来将是一场噩梦,耗时耗力。

Unidbg 提供了第三种,也是更优的解决方案:

Unidbg 是一个基于 Java 的“黑盒”模拟执行工具。它可以直接在你的电脑上(无需真机环境)模拟运行 Android 或 iOS 的 so 文件中的函数。

它的核心思想是: 我不需要知道你这个算法具体是怎么实现的(黑盒),我只需要给你提供运行所需的环境(内存、寄存器、JNI接口、系统函数等),让你自己把结果算出来告诉我。

为什么需要 Unidbg?

  • 无视反调试:由于是模拟环境,so 文件根本感知不到调试器的存在,所有基于 ptracefopen("/proc/self/status") 等反调试手段全部失效。

  • 高效:一旦编写好调用脚本,算法执行速度极快,非常适合用于大规模、自动化的爬虫工程。

  • 可控:可以随时中断、查看寄存器/内存状态,并且输入输出非常清晰。

  • 应对虚拟化保护:虽然不能直接还原 VMP 的指令,但可以通过 Unidbg 直接执行得到结果,绕过分析指令本身的过程。


二、Unidbg 的核心工作流程

使用 Unidbg 调用一个 so 文件中的函数,通常分为以下几步:

  1. 创建模拟器:选择一个 CPU 架构(如 ARM32 或 ARM64)来初始化模拟环境。

  2. 加载 so 文件:将目标 so 文件加载到模拟器的内存中。

  3. 设置运行参数:为 JNI 环境、系统函数(Hook)、内存等做好准备。

  4. 定位目标函数:找到你想要调用的那个函数的偏移地址或符号名。

  5. 传入参数:根据函数原型,通过寄存器或栈设置好传入参数。

  6. 模拟执行:让模拟器从函数入口点开始运行。

  7. 获取返回值:函数执行完毕后,从寄存器(如 R0, X0)或内存中取出计算结果。


三、环境搭建

  1. 安装 Java:确保电脑上安装了 JDK 8 或 JDK 11。

  2. 获取 Unidbg:

    • 推荐直接克隆 GitHub 上的项目,这样可以获取最新的示例和代码。

    bash
     
    git clone https://github.com/zhkl0228/unidbg.git
  3. 导入 IDE:使用 IntelliJ IDEA 打开这个项目。IDEA 对 Java 和 Maven 的支持最好。打开后,Maven 会自动下载所有依赖包。

项目中的 unidbg-android/src/test/java 目录下有海量的示例,这是最好的学习资料。


四、第一个 Unidbg 示例:调用一个无参函数

假设我们有一个 so 文件 libnative-lib.so,里面有一个函数 native_add(),功能是计算 1 + 2。

C 代码原型:

c
 
int native_add(int a, int b) {
    return a + b;
}

对应的 Unidbg Java 代码:

java
 
package com.example;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.IOException;

public class SimpleDemo {
    public static void main(String[] args) throws IOException {
        // 1. 创建模拟器实例,选择 ARM64 架构
        AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit().build();

        // 2. 获取模拟器的内存接口
        Memory memory = emulator.getMemory();
        // 设置库解析器,用于处理系统库的加载
        memory.setLibraryResolver(new AndroidResolver(23)); // API Level 23

        // 3. 创建 Android VM (DalvikVM) 实例,这对于运行 JNI 相关的代码很重要
        VM vm = emulator.createDalvikVM();
        // 加载目标 so 文件到内存中
        Module module = vm.loadLibrary(new File("unidbg/src/test/resources/example_binaries/libnative-lib.so"), false); // false 表示不进行初始化调用

        // 4. 打印 so 文件加载的基地址
        System.out.println("load library base=" + module.base);

        // 5. 调用函数
        // 5.1 找到函数的偏移地址或符号
        // 假设我们通过 IDA 分析,知道 native_add 的偏移地址是 0x1234
        Number result = module.callFunction(emulator, 0x1234, 1, 2);
        // 也可以尝试通过符号名查找(如果 so 文件未被 strip 掉符号表)
        // Number result = module.callFunction(emulator, "native_add", 1, 2);

        // 6. 获取并打印结果
        // ARM32 下返回值在 R0,ARM64 下在 X0。Unidbg 帮我们封装好了,直接取即可。
        System.out.println("Native add result: " + result.intValue());

        // 关闭模拟器
        emulator.close();
    }
}

五、核心概念与高级技巧

  1. JNI 交互:

    • 很多 so 函数是 Java_com_example_MainActivity_nativeMethod 的形式,它需要一个 JNIEnv* 和 jobject 参数。

    • Unidbg 的 DalvikVM 完美模拟了这一点。你需要创建一个 DvmObject 来表示 this 对象,然后使用 vm.getJNIEnv() 来获取 JNIEnv。

    • 示例:

      java
       
      DvmClass mainActivity = vm.resolveClass("com/example/MainActivity");
      DvmObject<?> obj = mainActivity.newObject(null);
      module.callFunction(emulator, "Java_com_example_MainActivity_encrypt", vm.getJNIEnv(), obj.hashCode(), ...);
  2. Hook 系统函数:

    • so 文件经常会调用 strlenmallocfopen 等系统函数。Unidbg 需要实现这些函数才能让 so 顺利运行。

    • AndroidResolver 已经提供了大部分系统函数的实现。

    • 你可以手动 Hook 这些函数来打印日志或修改行为,这对于调试至关重要。

    • 示例(Hook strlen):

      java
       
      memory.addHookListener(new HookListener() {
          @Override
          public long hook(SvcMemory memory, String library, String symbol, long old) {
              if ("libc.so".equals(library) && "strlen".equals(symbol)) {
                  return new ReplaceableSvc() {
                      @Override
                      public long handle(Emulator<?> emulator) {
                          // 获取 strlen 的第一个参数,即字符串地址
                          UnidgPointer pointer = emulator.getContext().getPointerArg(0);
                          String str = pointer.getString(0);
                          System.out.println("[Hook strlen] String: " + str);
                          // 调用原始的系统函数实现并返回结果
                          return old == 0 ? 0 : emulator.getBackend().nativeSvc.call(emulator, old);
                      }
                  };
              }
              return old;
          }
      });
  3. 补全环境:

    • 如果 so 文件调用了一个 Unidbg 尚未实现的系统函数,模拟会卡住。你需要根据日志提示,自己实现这个函数(称为 SVC Hook 或 IRQ Hook)。

    • 这需要一定的 ARM 汇编和 Linux 系统知识,是 Unidbg 进阶的必经之路。

六、学习建议

  1. 从示例开始:疯狂地阅读、运行、调试 unidbg/android/src/test/java 下的官方示例。这是最快的学习路径。

  2. 结合逆向工具:

    • 使用 IDA Pro 或 Ghidra 静态分析 so 文件,确定你要调用的函数偏移地址和参数类型( int? char*?结构体指针?)。

    • 使用 Frida 在真机上快速验证函数的功能和参数,然后再用 Unidbg 复现。

  3. 耐心调试:遇到问题别怕,仔细阅读 Unidbg 打印的执行日志,它非常详细,会告诉你执行到了哪里、在哪里卡住了、需要什么函数。

  4. 查阅文档和社区:GitHub Issues 里有很多宝藏,很多人会遇到和你一样的问题。

Unidbg 的学习曲线前期可能有些陡峭,但一旦掌握,它将成为你逆向武器库中最强大、最优雅的工具之一。它让你真正做到了“以我为主”,让复杂的 Native 代码在为你打工。祝你学习顺利!

 
 
 

posted on 2025-08-24 18:05  GKLBB  阅读(206)  评论(0)    收藏  举报