第四章:Unidbg原理与环境搭建
本章字数:约28000字 阅读时间:约90分钟 难度等级:★★★★★
声明:本文中的公司名称、包名、API地址、密钥等均已脱敏处理。文中的"梦想世界"、"dreamworld"等均为虚构名称,与任何真实公司无关。
引言
在前三章的探索中,我们尝试了几乎所有常规的逆向手段:
- 网络抓包被代理检测绕过
- Frida注入导致APP崩溃
- Xposed框架被检测到
- 模拟器环境被识别
- APK修改被签名校验拦截
就在我几乎要放弃的时候,一个偶然的发现改变了一切------Unidbg。
这是一个能在PC上直接运行Android Native库的神器,它让我们可以绑过APP的所有检测机制,直接调用SO库中的签名函数。
本章将深入讲解Unidbg的原理、环境搭建,以及如何用它来突破梦想世界APP的安全防护。
4.1 什么是Unidbg?
4.1.1 Unidbg简介
Unidbg是由中国开发者zhkl0228开发的一个开源项目,它基于Unicorn引擎实现了Android和iOS的Native库模拟执行。
bash
┌─────────────────────────────────────────────────────────────────┐
│ Unidbg 项目概览 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 项目地址: https://github.com/zhkl0228/unidbg │
│ 开发语言: Java │
│ 核心引擎: Unicorn (CPU模拟器) │
│ 支持架构: ARM32, ARM64 │
│ 支持平台: Android, iOS │
│ 开源协议: Apache 2.0 │
│ │
│ 主要功能: │
│ ├── 模拟执行ARM/ARM64指令 │
│ ├── 模拟Android/iOS系统调用 │
│ ├── 模拟JNI环境 │
│ ├── 支持动态调试 │
│ └── 支持Hook和Trace │
│ │
└─────────────────────────────────────────────────────────────────┘
简单来说:Unidbg可以让你在PC上直接运行Android的SO库,而不需要真实的Android设备或模拟器。
4.1.2 为什么Unidbg能绕过检测?
这是最关键的问题。让我们对比一下传统方法和Unidbg的区别:
传统方法的执行流程:
scss
┌─────────────────────────────────────────────────────────────────┐
│ 传统方法执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 启动APP │
│ ↓ │
│ 2. Application.onCreate() 执行 │
│ ↓ │
│ 3. 加载SO库 → .init_array 执行 → 反调试检测启动 │
│ ↓ │
│ 4. JNI_OnLoad() 执行 → 更多检测代码 │
│ ↓ │
│ 5. Activity启动 → 环境检测 │
│ ↓ │
│ 6. 检测到Frida/Root/模拟器 → APP崩溃 │
│ │
│ 问题: 检测代码在我们能干预之前就已经执行了! │
│ │
└─────────────────────────────────────────────────────────────────┘
Unidbg的执行流程:
┌─────────────────────────────────────────────────────────────────┐
│ Unidbg执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 在PC上创建模拟的Android环境 │
│ ↓ │
│ 2. 只加载目标SO库(不启动APP) │
│ ↓ │
│ 3. 可选择性地执行JNI_OnLoad(或跳过) │
│ ↓ │
│ 4. 直接调用目标函数(如签名函数) │
│ ↓ │
│ 5. 获取返回结果 │
│ │
│ 优势: │
│ ✓ 不运行完整APP,大部分检测代码不会执行 │
│ ✓ 完全可控的环境,可以Hook任意函数 │
│ ✓ 可以伪造任何系统调用的返回值 │
│ ✓ 没有真实的Frida进程,检测无从下手 │
│ │
└─────────────────────────────────────────────────────────────────┘
核心原理:Unidbg创建了一个"干净"的虚拟环境,这个环境中:
- 没有Frida进程
- 没有Xposed框架
- 没有Root权限
- 不是模拟器
因为这些东西根本就不存在于Unidbg的虚拟世界中!
4.1.3 Unidbg的技术架构
scss
┌─────────────────────────────────────────────────────────────────┐
│ Unidbg技术架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 用户代码 (Java) │ │
│ │ - 创建模拟器实例 │ │
│ │ - 加载SO库 │ │
│ │ - 调用JNI方法 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Unidbg框架层 │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ DalvikVM │ │ Memory │ │ Syscall │ │ │
│ │ │ (JNI模拟) │ │ (内存管理) │ │ (系统调用) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Unicorn引擎 │ │
│ │ - ARM/ARM64指令模拟 │ │
│ │ - 寄存器管理 │ │
│ │ - 内存访问模拟 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Capstone反汇编 │ │
│ │ - 指令解析 │ │
│ │ - 调试支持 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 Unicorn引擎深度解析
4.2.1 什么是Unicorn?
Unicorn是一个轻量级的多平台、多架构CPU模拟器框架,它是Unidbg的核心引擎。
┌─────────────────────────────────────────────────────────────────┐
│ Unicorn引擎特性 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 支持的CPU架构: │
│ ├── ARM (32位) │
│ ├── ARM64 (AArch64) │
│ ├── x86 │
│ ├── x86-64 │
│ ├── MIPS │
│ ├── SPARC │
│ └── M68K │
│ │
│ 核心功能: │
│ ├── 指令级模拟执行 │
│ ├── 内存映射和管理 │
│ ├── 寄存器读写 │
│ ├── Hook机制(代码、内存、中断) │
│ └── 多种编程语言绑定 │
│ │
│ 语言绑定: │
│ Python, Java, Go, Ruby, Rust, Haskell, .NET, ... │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2.2 Unicorn的工作原理
Unicorn基于QEMU的CPU模拟代码,但去掉了QEMU中与设备模拟相关的部分,只保留了纯CPU模拟功能。
python
# Unicorn基本使用示例(Python)
from unicorn import *
from unicorn.arm64_const import *
# ARM64机器码: mov x0, #0x1234
CODE = b"\x80\x46\x82\xd2"
# 初始化ARM64模拟器
mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
# 映射内存
ADDRESS = 0x10000
mu.mem_map(ADDRESS, 2 * 1024 * 1024)
# 写入代码
mu.mem_write(ADDRESS, CODE)
# 执行
mu.emu_start(ADDRESS, ADDRESS + len(CODE))
# 读取结果
x0 = mu.reg_read(UC_ARM64_REG_X0)
print(f"X0 = 0x{x0:x}") # 输出: X0 = 0x1234
4.2.3 为什么选择Unicorn?
| 特性 | Unicorn | QEMU | 真实设备 |
|---|---|---|---|
| 启动速度 | 毫秒级 | 秒级 | 分钟级 |
| 资源占用 | 低 | 中 | 高 |
| 可控性 | 完全可控 | 部分可控 | 有限 |
| Hook能力 | 指令级 | 有限 | 需要工具 |
| 调试能力 | 强 | 中 | 需要工具 |
| 环境隔离 | 完全隔离 | 部分隔离 | 无隔离 |
4.3 Unidbg环境搭建
4.3.1 开发环境准备
首先,我们需要准备开发环境:
java
┌─────────────────────────────────────────────────────────────────┐
│ 开发环境要求 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 必需组件: │
│ ├── JDK 8+ (推荐JDK 11) │
│ ├── Maven 3.6+ │
│ ├── IDE (IntelliJ IDEA推荐) │
│ └── Git │
│ │
│ 可选组件: │
│ ├── IDA Pro (用于分析SO库) │
│ ├── Ghidra (免费的反汇编工具) │
│ └── jadx (用于反编译APK) │
│ │
│ 操作系统: │
│ ├── macOS (推荐) │
│ ├── Linux │
│ └── Windows │
│ │
└─────────────────────────────────────────────────────────────────┘
安装JDK:
bash
# macOS (使用Homebrew)
brew install openjdk@11
# 设置环境变量
export JAVA_HOME=/usr/local/opt/openjdk@11
export PATH=$JAVA_HOME/bin:$PATH
# 验证安装
java -version
# openjdk version "11.0.x" ...
安装Maven:
bash
# macOS
brew install maven
# 验证安装
mvn -version
# Apache Maven 3.8.x ...
4.3.2 创建Maven项目
创建一个新的Maven项目来使用Unidbg:
bash
# 创建项目目录
mkdir dw_mall_security_chain
cd dw_mall_security_chain
# 创建Maven标准目录结构
mkdir -p src/main/java/com/dreamworld
mkdir -p src/main/resources
pom.xml配置:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dreamworld</groupId>
<artifactId>security-chain</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- JitPack仓库(Unidbg托管在这里) -->
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- Unidbg核心依赖 -->
<dependency>
<groupId>com.github.zhkl0228</groupId>
<artifactId>unidbg-android</artifactId>
<version>0.9.8</version>
</dependency>
<!-- HTTP客户端 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.32</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<!-- 可执行JAR打包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.dreamworld.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
4.3.3 准备资源文件
要使用Unidbg模拟执行SO库,我们需要准备以下资源:
bash
┌─────────────────────────────────────────────────────────────────┐
│ 所需资源文件 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 必需文件: │
│ ├── APK文件 │
│ │ └── android-dreamworld-arm64-v8a-prod-v8.x.x.apk │
│ │ (用于提供APK上下文,如签名信息) │
│ │ │
│ ├── 目标SO库 │
│ │ └── libSecurityCore.so │
│ │ (包含签名函数的核心库) │
│ │ │
│ └── 依赖SO库 │
│ └── libc++_shared.so │
│ (C++标准库,很多SO库都依赖它) │
│ │
│ 文件来源: │
│ - APK: 从应用商店下载或从设备提取 │
│ - SO库: 从APK中解压 (lib/arm64-v8a/ 目录) │
│ │
└─────────────────────────────────────────────────────────────────┘
从APK中提取SO库:
bash
# 解压APK(APK本质上是ZIP文件)
unzip android-dreamworld-arm64-v8a-prod-v8.x.x.apk -d apk_extracted
# 查看lib目录结构
ls -la apk_extracted/lib/
# arm64-v8a/ (64位ARM)
# armeabi-v7a/ (32位ARM,可能没有)
# 复制需要的SO库
mkdir -p unilib
cp apk_extracted/lib/arm64-v8a/libSecurityCore.so unilib/
cp apk_extracted/lib/arm64-v8a/libc++_shared.so unilib/
4.3.4 项目目录结构
最终的项目结构如下:
bash
dw_mall_security_chain/
├── pom.xml # Maven配置
├── unilib/ # SO库目录
│ ├── libSecurityCore.so # 目标SO库
│ └── libc++_shared.so # C++标准库
├── apk/ # APK目录
│ └── android-dreamworld-v8.x.x.apk # 原始APK
└── src/main/java/com/dreamworld/
├── Main.java # 主入口
├── unidbg/
│ └── UnidbgJNIWrapper.java # Unidbg封装
├── security/
│ └── SecurityStub.java # 安全接口封装
├── network/
│ └── ApiClient.java # API客户端
└── utils/
└── LogUtils.java # 日志工具
4.4 Unidbg核心API详解
4.4.1 创建模拟器实例
Unidbg提供了两种模拟器:32位和64位。根据目标SO库的架构选择:
java
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
// 创建64位ARM模拟器(ARM64/AArch64)
AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.dreamworld.app") // 设置进程名
.build();
// 或者创建32位ARM模拟器
// AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit().build();
模拟器配置选项:
java
AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.dreamworld.app") // 进程名(影响/proc/self/cmdline)
.addBackendFactory(new Unicorn2Factory(true)) // 使用Unicorn2后端
.build();
// 获取内存管理器
Memory memory = emulator.getMemory();
// 设置库解析器(指定Android API级别)
memory.setLibraryResolver(new AndroidResolver(23)); // Android 6.0
4.4.2 创建DalvikVM
DalvikVM是Unidbg模拟的Android虚拟机,用于处理JNI调用:
java
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.DalvikModule;
// 创建DalvikVM,传入APK文件
File apkFile = new File("apk/android-dreamworld-v8.x.x.apk");
VM vm = emulator.createDalvikVM(apkFile);
// 设置JNI回调处理器
vm.setJni(this); // this需要实现Jni接口或继承AbstractJni
// 设置是否输出详细日志
vm.setVerbose(false); // 生产环境建议关闭
为什么需要APK文件?
APK文件提供了以下信息:
- 签名信息:某些SO库会验证APK签名
- 包名 :用于
getPackageName()等JNI调用 - 资源文件:某些SO库可能读取assets目录
4.4.3 加载SO库
java
import com.github.unidbg.Module;
// 加载依赖库(如果有的话)
File libcxx = new File("unilib/libc++_shared.so");
if (libcxx.exists()) {
vm.loadLibrary(libcxx, false); // false表示不调用JNI_OnLoad
}
// 加载目标SO库
File targetLib = new File("unilib/libSecurityCore.so");
DalvikModule dm = vm.loadLibrary(targetLib, false);
// 获取Module对象(用于后续操作)
Module module = dm.getModule();
// 调用JNI_OnLoad(动态注册JNI方法)
dm.callJNI_OnLoad(emulator);
关于JNI_OnLoad:
JNI_OnLoad是SO库的入口函数,通常在这里进行:
- JNI方法的动态注册
- 全局变量初始化
- 某些检测代码的执行
c
// JNI_OnLoad的典型实现
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
// 动态注册JNI方法
JNINativeMethod methods[] = {
{"getPriId", "()Ljava/lang/String;", (void*)native_getPriId},
{"rsaSign", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void*)native_rsaSign},
// ...
};
jclass clazz = (*env)->FindClass(env, "com/dreamworld/secutil/JNIWrapper");
(*env)->RegisterNatives(env, clazz, methods, sizeof(methods)/sizeof(methods[0]));
return JNI_VERSION_1_6;
}
4.4.4 解析JNI类
java
import com.github.unidbg.linux.android.dvm.DvmClass;
// 解析JNI包装类
// 这个类名需要与SO库中注册的类名一致
DvmClass jniWrapperClass = vm.resolveClass("com/dreamworld/secutil/JNIWrapper");
如何找到正确的类名?
- 反编译APK:使用jadx查看Java代码中的native方法声明
- 分析SO库:使用IDA Pro查看JNI_OnLoad中的RegisterNatives调用
- 查看日志:开启Unidbg的verbose模式,观察类加载日志
4.4.5 调用JNI方法
这是最关键的部分------调用SO库中的JNI方法:
java
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.DvmObject;
// 方法签名格式: 方法名(参数类型)返回类型
// 例如: getPriId()Ljava/lang/String;
// 调用无参数方法
StringObject result = jniWrapperClass.callStaticJniMethodObject(
emulator,
"oGetPriId2b3c4d5e6f7a8b9c0d1e2f3a4b()Ljava/lang/String;" // 混淆后的方法名
);
String priId = result.getValue();
// 调用带参数的方法
StringObject signResult = jniWrapperClass.callStaticJniMethodObject(
emulator,
"oRsaSign3c4d5e6f7a8b9c0d1e2f3a4b5c(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
new StringObject(vm, ""), // 第一个参数
new StringObject(vm, "data_to_sign") // 第二个参数
);
String signature = signResult.getValue();
JNI类型签名对照表:
| Java类型 | JNI签名 | 示例 |
|---|---|---|
| void | V | ()V |
| boolean | Z | ()Z |
| byte | B | ()B |
| char | C | ()C |
| short | S | ()S |
| int | I | ()I |
| long | J | ()J |
| float | F | ()F |
| double | D | ()D |
| String | Ljava/lang/String; | ()Ljava/lang/String; |
| Object | Ljava/lang/Object; | ()Ljava/lang/Object; |
| int[] | [I | ()[I |
| String[] | [Ljava/lang/String; | ()[Ljava/lang/String; |
4.5 实战:加载梦想世界APP的SO库
4.5.1 分析目标SO库
在开始编码之前,我们需要先分析目标SO库的结构。
使用IDA Pro打开libSecurityCore.so:
┌─────────────────────────────────────────────────────────────────┐
│ libSecurityCore.so 分析结果 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 文件信息: │
│ ├── 架构: ARM64 (AArch64) │
│ ├── 大小: 约2.5MB │
│ └── 类型: 共享库 (.so) │
│ │
│ 导出函数: │
│ ├── JNI_OnLoad → 动态注册入口 │
│ └── Java_com_dreamworld_* → 静态注册的JNI方法(如果有) │
│ │
│ 关键发现: │
│ ├── 使用动态注册(RegisterNatives) │
│ ├── 方法名经过混淆(如oGetPriId2b3c4d5e6f7a8b9c0d1e2f3a4b) │
│ ├── 包含反调试代码(在.init_array中) │
│ └── 依赖libc++_shared.so │
│ │
└─────────────────────────────────────────────────────────────────┘
4.5.2 找到JNI方法签名
通过反编译APK,我们找到了JNI方法的声明:
java
// 反编译得到的JNIWrapper类
package com.dreamworld.secutil;
public class JNIWrapper {
static {
System.loadLibrary("SecurityCore");
}
// 获取设备密钥ID
public static native String oGetPriId2b3c4d5e6f7a8b9c0d1e2f3a4b();
// RSA签名
public static native String oRsaSign3c4d5e6f7a8b9c0d1e2f3a4b5c(String prefix, String data);
// 获取密钥对
public static native String oGetKeyPair6f7a8b9c0d1e2f3a4b5c6d();
// 设置环境
public static native void oSetEnv1a2b3c4d5e6f7a8b9c0d1e2f3a4(String env);
// 解密密钥
public static native String[] oFormatDK4d5e6f7a8b9c0d1e2f3a4b5c(
String tempAesKey, String tempIv, String aesKey, String hmacKey);
// HMAC签名
public static native String oHmacSign5e6f7a8b9c0d1e2f3a4b5c6d(String key, String data);
// RSA验签
public static native boolean oRsaVerify7a8b9c0d1e2f3a4b5c6d7e(
String publicKey, String signature, String data, String padding, String hash);
// 加密
public static native String oEncrypt8b9c0d1e2f3a4b5c6d7e8f9a(String data);
}
方法名混淆分析:
这些方法名看起来像是MD5哈希值,这是一种常见的混淆手段:
- 原始方法名被替换为哈希值
- 增加逆向分析的难度
- 但不影响功能调用
4.5.3 编写Unidbg封装类
现在我们来编写完整的Unidbg封装类:
java
package com.dreamworld.unidbg;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.security.MessageDigest;
/**
* Unidbg JNI包装器
* 用于在PC上模拟执行梦想世界APP的Native库
*/
public class UnidbgJNIWrapper extends AbstractJni {
private static final String TAG = "UnidbgJNIWrapper";
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass jniWrapperClass;
// JNI方法签名常量
private static final String METHOD_GET_PRI_ID =
"oGetPriId2b3c4d5e6f7a8b9c0d1e2f3a4b()Ljava/lang/String;";
private static final String METHOD_RSA_SIGN =
"oRsaSign3c4d5e6f7a8b9c0d1e2f3a4b5c(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";
private static final String METHOD_GET_KEY_PAIR =
"oGetKeyPair6f7a8b9c0d1e2f3a4b5c6d()Ljava/lang/String;";
private static final String METHOD_SET_ENVIRONMENT =
"oSetEnv1a2b3c4d5e6f7a8b9c0d1e2f3a4(Ljava/lang/String;)V";
private static final String METHOD_FORMAT_DK =
"oFormatDK4d5e6f7a8b9c0d1e2f3a4b5c(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;";
private static final String METHOD_HMAC_SIGN =
"oHmacSign5e6f7a8b9c0d1e2f3a4b5c6d(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";
/**
* 构造函数 - 初始化Unidbg环境
*/
public UnidbgJNIWrapper(String apkPath, String libDir) {
System.out.println("[" + TAG + "] 初始化Unidbg JNI环境");
File apkFile = new File(apkPath);
File libDirectory = new File(libDir);
// 验证文件存在
if (!apkFile.exists()) {
throw new RuntimeException("APK文件不存在: " + apkPath);
}
// ========== 步骤1: 创建ARM64模拟器 ==========
System.out.println("[" + TAG + "] 创建ARM64模拟器");
emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.dreamworld.app")
.build();
// 配置内存
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // Android 6.0
// ========== 步骤2: 创建DalvikVM ==========
System.out.println("[" + TAG + "] 创建DalvikVM");
vm = emulator.createDalvikVM(apkFile);
vm.setJni(this); // 设置JNI回调处理器
vm.setVerbose(false); // 关闭详细日志
// ========== 步骤3: 加载依赖库 ==========
File libcxx = new File(libDirectory, "libc++_shared.so");
if (libcxx.exists()) {
System.out.println("[" + TAG + "] 加载依赖库: libc++_shared.so");
vm.loadLibrary(libcxx, false);
}
// ========== 步骤4: 加载目标SO库 ==========
File targetLib = new File(libDirectory, "libSecurityCore.so");
if (!targetLib.exists()) {
throw new RuntimeException("SO文件不存在: " + targetLib.getAbsolutePath());
}
System.out.println("[" + TAG + "] 加载目标SO库: libSecurityCore.so");
DalvikModule dm = vm.loadLibrary(targetLib, false);
module = dm.getModule();
// ========== 步骤5: 调用JNI_OnLoad ==========
System.out.println("[" + TAG + "] 调用JNI_OnLoad进行动态注册");
dm.callJNI_OnLoad(emulator);
// ========== 步骤6: 解析JNI类 ==========
System.out.println("[" + TAG + "] 解析JNI包装类");
jniWrapperClass = vm.resolveClass("com/dreamworld/secutil/JNIWrapper");
System.out.println("[" + TAG + "] Unidbg JNI环境初始化完成");
}
// ... 后续方法实现
}
4.5.4 实现JNI方法调用
继续完善UnidbgJNIWrapper类,实现各个JNI方法的调用:
java
/**
* 获取设备私钥ID
* 这是安全激活的第一步
*/
public String getPriId() {
System.out.println("[" + TAG + "] 调用getPriId()");
try {
StringObject result = jniWrapperClass.callStaticJniMethodObject(
emulator,
METHOD_GET_PRI_ID
);
if (result == null) {
throw new RuntimeException("getPriId返回null");
}
String priId = result.getValue();
System.out.println("[" + TAG + "] PriId: " + priId);
return priId;
} catch (Exception e) {
System.err.println("[" + TAG + "] getPriId失败: " + e.getMessage());
throw new RuntimeException("getPriId失败", e);
}
}
/**
* RSA签名
* 用于key-suite接口的请求签名
*
* @param prefix 签名前缀(通常为空字符串)
* @param payload 待签名数据
* @return Base64编码的签名结果
*/
public String rsaSign(String prefix, String payload) {
System.out.println("[" + TAG + "] 调用rsaSign()");
System.out.println("[" + TAG + "] 待签名数据: " + payload);
try {
StringObject result = jniWrapperClass.callStaticJniMethodObject(
emulator,
METHOD_RSA_SIGN,
new StringObject(vm, prefix),
new StringObject(vm, payload)
);
if (result == null) {
throw new RuntimeException("rsaSign返回null");
}
String signature = result.getValue();
System.out.println("[" + TAG + "] 签名长度: " + signature.length());
return signature;
} catch (Exception e) {
System.err.println("[" + TAG + "] rsaSign失败: " + e.getMessage());
throw new RuntimeException("rsaSign失败", e);
}
}
/**
* 设置环境
* 必须在获取密钥之前调用!
*
* @param env 环境标识 (0=测试, 1=生产)
*/
public void setEnvironment(int env) {
System.out.println("[" + TAG + "] 调用setEnvironment(" + env + ")");
try {
jniWrapperClass.callStaticJniMethod(
emulator,
METHOD_SET_ENVIRONMENT,
new StringObject(vm, String.valueOf(env))
);
System.out.println("[" + TAG + "] 环境设置完成");
} catch (Exception e) {
System.err.println("[" + TAG + "] setEnvironment失败: " + e.getMessage());
}
}
/**
* 解密密钥
* 用于解密服务器返回的加密密钥
*
* @param tempAesKey RSA加密的临时AES密钥
* @param tempIv 临时IV
* @param aesKey 加密的AES密钥
* @param hmacKey 加密的HMAC密钥
* @return [解密后的AES密钥, 解密后的HMAC密钥]
*/
public String[] formatDK(String tempAesKey, String tempIv,
String aesKey, String hmacKey) {
System.out.println("[" + TAG + "] 调用formatDK()");
try {
DvmObject<?> result = jniWrapperClass.callStaticJniMethodObject(
emulator,
METHOD_FORMAT_DK,
new StringObject(vm, tempAesKey),
new StringObject(vm, tempIv),
new StringObject(vm, aesKey != null ? aesKey : ""),
new StringObject(vm, hmacKey != null ? hmacKey : "")
);
if (result == null) {
System.err.println("[" + TAG + "] formatDK返回null");
return null;
}
// 解析返回的字符串数组
if (result instanceof com.github.unidbg.linux.android.dvm.array.ArrayObject) {
com.github.unidbg.linux.android.dvm.array.ArrayObject arrayObj =
(com.github.unidbg.linux.android.dvm.array.ArrayObject) result;
int length = arrayObj.length();
String[] keys = new String[length];
for (int i = 0; i < length; i++) {
DvmObject<?> element = arrayObj.getValue()[i];
if (element instanceof StringObject) {
keys[i] = ((StringObject) element).getValue();
}
}
System.out.println("[" + TAG + "] formatDK成功,返回" + length + "个密钥");
return keys;
}
return null;
} catch (Exception e) {
System.err.println("[" + TAG + "] formatDK失败: " + e.getMessage());
return null;
}
}
/**
* HMAC签名
* 用于API请求的签名
*
* @param hacKey HMAC密钥
* @param stringToSign 待签名字符串
* @return 签名结果
*/
public String hmacSign(String hacKey, String stringToSign) {
System.out.println("[" + TAG + "] 调用hmacSign()");
try {
StringObject result = jniWrapperClass.callStaticJniMethodObject(
emulator,
METHOD_HMAC_SIGN,
new StringObject(vm, hacKey),
new StringObject(vm, stringToSign)
);
if (result == null) {
return null;
}
String signature = result.getValue();
System.out.println("[" + TAG + "] HMAC签名长度: " + signature.length());
return signature;
} catch (Exception e) {
System.err.println("[" + TAG + "] hmacSign失败: " + e.getMessage());
return null;
}
}
/**
* 关闭Unidbg环境
* 释放资源
*/
public void close() {
System.out.println("[" + TAG + "] 关闭Unidbg环境");
if (emulator != null) {
try {
emulator.close();
} catch (Exception e) {
System.err.println("[" + TAG + "] 关闭失败: " + e.getMessage());
}
}
}
4.5.5 处理JNI回调
SO库在执行过程中可能会调用Java方法(JNI回调),我们需要实现这些回调:
java
// ========== JNI回调方法实现 ==========
/**
* 处理静态方法调用
*/
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass,
String signature, VarArg varArg) {
// 处理ActivityThread.currentActivityThread()
if ("android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;"
.equals(signature)) {
return dvmClass.newObject(null);
}
// 处理MessageDigest.getInstance()
if ("java/security/MessageDigest->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;"
.equals(signature)) {
String algorithm = varArg.getObjectArg(0).getValue().toString();
try {
MessageDigest digest = MessageDigest.getInstance(algorithm);
return vm.resolveClass("java/security/MessageDigest").newObject(digest);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 其他未处理的调用交给父类
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
/**
* 处理实例方法调用
*/
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject,
String signature, VarArg varArg) {
// 处理ActivityThread.getApplication()
if ("android/app/ActivityThread->getApplication()Landroid/app/Application;"
.equals(signature)) {
return vm.resolveClass("android/app/Application").newObject(null);
}
// 处理Application.getPackageManager()
if ("android/app/Application->getPackageManager()Landroid/content/pm/PackageManager;"
.equals(signature)) {
return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
}
// 处理MessageDigest.digest()
if ("java/security/MessageDigest->digest()[B".equals(signature)) {
MessageDigest digest = (MessageDigest) dvmObject.getValue();
return new ByteArray(vm, digest.digest());
}
// 处理MessageDigest.digest(byte[])
if ("java/security/MessageDigest->digest([B)[B".equals(signature)) {
MessageDigest digest = (MessageDigest) dvmObject.getValue();
ByteArray array = varArg.getObjectArg(0);
return new ByteArray(vm, digest.digest(array.getValue()));
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
/**
* 处理void方法调用
*/
@Override
public void callVoidMethod(BaseVM vm, DvmObject<?> dvmObject,
String signature, VarArg varArg) {
// 处理MessageDigest.update()
if ("java/security/MessageDigest->update([B)V".equals(signature)) {
MessageDigest digest = (MessageDigest) dvmObject.getValue();
ByteArray array = varArg.getObjectArg(0);
digest.update(array.getValue());
return;
}
super.callVoidMethod(vm, dvmObject, signature, varArg);
}
}
为什么需要实现这些回调?
SO库中的Native代码可能会通过JNI调用Java方法,例如:
- 获取Application上下文
- 调用Java的加密API
- 读取系统属性
如果不实现这些回调,Unidbg会抛出UnsupportedOperationException。
4.6 第一次运行测试
4.6.1 编写测试代码
java
package com.dreamworld;
import com.dreamworld.unidbg.UnidbgJNIWrapper;
public class Main {
private static final String APK_PATH = "apk/android-dreamworld-v8.x.x.apk";
private static final String LIB_DIR = "unilib";
public static void main(String[] args) {
System.out.println("╔══════════════════════════════════════════════════════════════╗");
System.out.println("║ 梦想世界APP安全激活调用链 - Unidbg测试 ║");
System.out.println("╚══════════════════════════════════════════════════════════════╝");
System.out.println();
UnidbgJNIWrapper wrapper = null;
try {
// 步骤1: 初始化Unidbg环境
System.out.println(">>> 步骤1: 初始化Unidbg环境");
wrapper = new UnidbgJNIWrapper(APK_PATH, LIB_DIR);
System.out.println("✓ Unidbg环境初始化成功");
System.out.println();
// 步骤2: 设置生产环境(重要!)
System.out.println(">>> 步骤2: 设置生产环境");
wrapper.setEnvironment(1); // 1 = 生产环境
System.out.println("✓ 环境设置完成");
System.out.println();
// 步骤3: 获取设备密钥ID
System.out.println(">>> 步骤3: 获取设备密钥ID");
String priId = wrapper.getPriId();
System.out.println("✓ PriId: " + priId);
System.out.println();
// 步骤4: 测试RSA签名
System.out.println(">>> 步骤4: 测试RSA签名");
String testData = "test:data:for:signing";
String signature = wrapper.rsaSign("", testData);
System.out.println("✓ 签名成功,长度: " + signature.length());
System.out.println(" 签名前20字符: " + signature.substring(0, 20) + "...");
System.out.println();
System.out.println("╔══════════════════════════════════════════════════════════════╗");
System.out.println("║ 测试完成! ║");
System.out.println("╚══════════════════════════════════════════════════════════════╝");
} catch (Exception e) {
System.err.println("测试失败: " + e.getMessage());
e.printStackTrace();
} finally {
if (wrapper != null) {
wrapper.close();
}
}
}
}
4.6.2 运行测试
bash
# 编译项目
mvn clean compile
# 运行测试
mvn exec:java -Dexec.mainClass="com.dreamworld.Main" -q
预期输出:
csharp
╔══════════════════════════════════════════════════════════════╗
║ 梦想世界APP安全激活调用链 - Unidbg测试 ║
╚══════════════════════════════════════════════════════════════╝
>>> 步骤1: 初始化Unidbg环境
[UnidbgJNIWrapper] 初始化Unidbg JNI环境
[UnidbgJNIWrapper] 创建ARM64模拟器
[UnidbgJNIWrapper] 创建DalvikVM
[UnidbgJNIWrapper] 加载依赖库: libc++_shared.so
[UnidbgJNIWrapper] 加载目标SO库: libSecurityCore.so
[UnidbgJNIWrapper] 调用JNI_OnLoad进行动态注册
[UnidbgJNIWrapper] 解析JNI包装类
[UnidbgJNIWrapper] Unidbg JNI环境初始化完成
✓ Unidbg环境初始化成功
>>> 步骤2: 设置生产环境
[UnidbgJNIWrapper] 调用setEnvironment(1)
[UnidbgJNIWrapper] 环境设置完成
✓ 环境设置完成
>>> 步骤3: 获取设备密钥ID
[UnidbgJNIWrapper] 调用getPriId()
[UnidbgJNIWrapper] PriId: f1e2d3c4b5a6978869574a3b2c1d0e0f
✓ PriId: f1e2d3c4b5a6978869574a3b2c1d0e0f
>>> 步骤4: 测试RSA签名
[UnidbgJNIWrapper] 调用rsaSign()
[UnidbgJNIWrapper] 待签名数据: test:data:for:signing
[UnidbgJNIWrapper] 签名长度: 344
✓ 签名成功,长度: 344
签名前20字符: RjOEWKNX2lfFXHlcN1...
╔══════════════════════════════════════════════════════════════╗
║ 测试完成! ║
╚══════════════════════════════════════════════════════════════╝
成功了! SO库成功加载,JNI方法成功调用!
4.6.3 关键发现
通过这次测试,我们发现了几个重要的点:
scss
┌─────────────────────────────────────────────────────────────────┐
│ 关键发现 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 环境设置的重要性 │
│ - 必须先调用setEnvironment(1)设置生产环境 │
│ - 不同环境返回不同的密钥ID │
│ · env=0 (测试): priId = a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6 │
│ · env=1 (生产): priId = f1e2d3c4b5a6978869574a3b2c1d0e0f │
│ │
│ 2. 方法调用顺序 │
│ - setEnvironment() → getPriId() → rsaSign() │
│ - 顺序错误会导致获取错误的密钥ID │
│ │
│ 3. 签名格式 │
│ - RSA签名结果是Base64编码 │
│ - 长度约344字符(2048位RSA密钥) │
│ │
│ 4. 无检测触发 │
│ - SO库成功加载,没有崩溃 │
│ - 说明Unidbg成功绕过了检测机制 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.7 深入理解:为什么检测没有触发?
4.7.1 分析SO库的检测代码
让我们用IDA Pro分析一下SO库中的检测代码:
c
// .init_array 中的反调试初始化(伪代码)
void __attribute__((constructor)) anti_debug_init() {
// 检测Frida
if (check_frida_port() || check_frida_process()) {
raise(SIGSEGV); // 触发崩溃
}
// 检测调试器
if (check_ptrace() || check_tracerpid()) {
raise(SIGSEGV);
}
// 启动后台检测线程
pthread_create(&detection_thread, NULL, continuous_detection, NULL);
}
// Frida端口检测
int check_frida_port() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(27042); // Frida默认端口
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
close(sock);
return 1; // 检测到Frida
}
close(sock);
return 0;
}
// 进程检测
int check_frida_process() {
DIR *dir = opendir("/proc");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
char cmdline_path[256];
snprintf(cmdline_path, sizeof(cmdline_path), "/proc/%s/cmdline", entry->d_name);
FILE *f = fopen(cmdline_path, "r");
if (f) {
char cmdline[256];
fgets(cmdline, sizeof(cmdline), f);
fclose(f);
if (strstr(cmdline, "frida") || strstr(cmdline, "gum-js-loop")) {
return 1; // 检测到Frida
}
}
}
closedir(dir);
return 0;
}
4.7.2 Unidbg如何绕过这些检测?
css
┌─────────────────────────────────────────────────────────────────┐
│ Unidbg绕过检测的原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 检测方法1: Frida端口检测 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SO库代码: connect(sock, "127.0.0.1:27042") │ │
│ │ ↓ │ │
│ │ Unidbg: 模拟socket系统调用 │ │
│ │ ↓ │ │
│ │ 返回: ECONNREFUSED (连接被拒绝) │ │
│ │ ↓ │ │
│ │ 结果: 检测失败,认为没有Frida │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 检测方法2: /proc目录扫描 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SO库代码: opendir("/proc") │ │
│ │ ↓ │ │
│ │ Unidbg: 模拟文件系统调用 │ │
│ │ ↓ │ │
│ │ 返回: 空目录或模拟的进程列表(不含frida) │ │
│ │ ↓ │ │
│ │ 结果: 检测失败,认为没有Frida进程 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 检测方法3: ptrace检测 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SO库代码: ptrace(PTRACE_TRACEME, 0, 0, 0) │ │
│ │ ↓ │ │
│ │ Unidbg: 模拟ptrace系统调用 │ │
│ │ ↓ │ │
│ │ 返回: 0 (成功) │ │
│ │ ↓ │ │
│ │ 结果: 检测失败,认为没有被调试 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 核心原理: │
│ Unidbg完全控制了所有系统调用的返回值, │
│ 可以让检测代码"看到"一个干净的环境。 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.7.3 .init_array的处理
你可能会问:.init_array中的检测代码不是在库加载时就执行了吗?
答案是:是的,但Unidbg可以控制它的执行。
java
// 加载SO库时,第二个参数控制是否执行.init_array
DalvikModule dm = vm.loadLibrary(targetLib, false); // false = 不自动执行
// 如果需要,可以手动执行
// dm.callInit(emulator); // 执行.init_array
// 调用JNI_OnLoad(这是必须的,用于动态注册)
dm.callJNI_OnLoad(emulator);
在我们的案例中,即使.init_array执行了,检测代码也会因为Unidbg的系统调用模拟而失败。
4.8 常见问题与解决方案
4.8.1 UnsupportedOperationException
问题:
css
com.github.unidbg.linux.android.dvm.UnsupportedOperationException:
android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;
原因:SO库调用了未实现的JNI方法。
解决方案:在AbstractJni子类中实现该方法:
java
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject,
String signature, VarArg varArg) {
if ("android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;"
.equals(signature)) {
// 返回一个模拟的SharedPreferences对象
return vm.resolveClass("android/content/SharedPreferences").newObject(new HashMap<>());
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
4.8.2 找不到SO库依赖
问题:
lua
java.lang.IllegalStateException: load library failed: libc++_shared.so
原因:缺少依赖的SO库。
解决方案:
- 从APK中提取所有需要的SO库
- 按正确顺序加载(先加载依赖库)
java
// 先加载依赖库
vm.loadLibrary(new File(libDir, "libc++_shared.so"), false);
// 再加载目标库
vm.loadLibrary(new File(libDir, "libSecurityCore.so"), false);
4.8.3 JNI方法找不到
问题:
makefile
java.lang.NoSuchMethodError: getPriId
原因:
- 类名不正确
- 方法签名不正确
- JNI_OnLoad未调用(动态注册未执行)
解决方案:
- 确认类名与SO库中注册的一致
- 确认方法签名正确(包括参数类型和返回类型)
- 确保调用了
dm.callJNI_OnLoad(emulator)
4.8.4 内存不足
问题:
makefile
java.lang.OutOfMemoryError: Java heap space
原因:Unidbg模拟器占用大量内存。
解决方案:增加JVM堆内存:
bash
# 运行时指定内存
mvn exec:java -Dexec.mainClass="com.dreamworld.Main" -Dexec.args="" \
-Dexec.vmArgs="-Xmx2g"
# 或在pom.xml中配置
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>com.dreamworld.Main</mainClass>
<arguments>
<argument>-Xmx2g</argument>
</arguments>
</configuration>
</plugin>
4.8.5 ARM64 vs ARM32
问题:加载32位SO库到64位模拟器(或反之)。
解决方案:确保模拟器架构与SO库架构匹配:
java
// 检查SO库架构
// 使用file命令
// $ file libSecurityCore.so
// libSecurityCore.so: ELF 64-bit LSB shared object, ARM aarch64
// 64位SO库使用64位模拟器
AndroidEmulator emulator = AndroidEmulatorBuilder.for64Bit().build();
// 32位SO库使用32位模拟器
// AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit().build();
4.9 调试技巧
4.9.1 开启详细日志
java
// 开启DalvikVM详细日志
vm.setVerbose(true);
// 开启Unidbg调试日志
emulator.traceCode(); // 跟踪所有执行的指令
emulator.traceRead(); // 跟踪内存读取
emulator.traceWrite(); // 跟踪内存写入
4.9.2 Hook Native函数
java
// Hook指定地址的函数
emulator.attach().addBreakPoint(module.base + 0x12345, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// 打印寄存器状态
RegisterContext context = emulator.getContext();
System.out.println("X0 = " + context.getLongArg(0));
System.out.println("X1 = " + context.getLongArg(1));
return true; // 继续执行
}
});
4.9.3 使用IDA Pro配合调试
- 在IDA Pro中找到目标函数的偏移地址
- 在Unidbg中设置断点
- 观察寄存器和内存状态
java
// 假设IDA显示函数地址为0x12345
long functionOffset = 0x12345;
long actualAddress = module.base + functionOffset;
emulator.attach().addBreakPoint(actualAddress, (emulator, address) -> {
System.out.println("Hit breakpoint at: 0x" + Long.toHexString(address));
// 打印调用栈
emulator.getUnwinder().unwind();
return true;
});
4.10 性能优化
4.10.1 复用模拟器实例
创建模拟器实例是昂贵的操作,应该复用:
java
public class UnidbgPool {
private static UnidbgJNIWrapper instance;
public static synchronized UnidbgJNIWrapper getInstance() {
if (instance == null) {
instance = new UnidbgJNIWrapper(APK_PATH, LIB_DIR);
}
return instance;
}
public static synchronized void close() {
if (instance != null) {
instance.close();
instance = null;
}
}
}
4.10.2 减少不必要的日志
java
// 生产环境关闭详细日志
vm.setVerbose(false);
// 使用SLF4J控制日志级别
// 在simplelogger.properties中配置
org.slf4j.simpleLogger.defaultLogLevel=warn
4.10.3 内存管理
java
// 及时释放不需要的对象
// Unidbg会自动管理大部分内存,但大对象需要注意
// 如果需要多次调用,考虑重置VM状态而不是重新创建
// (目前Unidbg不直接支持,需要重新创建实例)
4.11 本章小结
4.11.1 核心知识点
yaml
┌─────────────────────────────────────────────────────────────────┐
│ 本章核心知识点 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Unidbg原理 │
│ - 基于Unicorn引擎的CPU模拟 │
│ - 模拟Android/iOS的系统调用 │
│ - 模拟JNI环境 │
│ - 可以绕过大部分检测机制 │
│ │
│ 2. 环境搭建 │
│ - JDK 8+ + Maven │
│ - 从JitPack引入unidbg-android依赖 │
│ - 准备APK和SO库文件 │
│ │
│ 3. 核心API │
│ - AndroidEmulatorBuilder: 创建模拟器 │
│ - VM: DalvikVM虚拟机 │
│ - DalvikModule: SO库模块 │
│ - DvmClass: JNI类 │
│ - callStaticJniMethodObject: 调用JNI方法 │
│ │
│ 4. JNI回调处理 │
│ - 继承AbstractJni │
│ - 实现callStaticObjectMethod等方法 │
│ - 处理SO库对Java方法的调用 │
│ │
│ 5. 调试技巧 │
│ - 开启verbose日志 │
│ - 使用断点和Hook │
│ - 配合IDA Pro分析 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.11.2 实战成果
通过本章的学习和实践,我们成功实现了:
- 搭建Unidbg开发环境
- 加载梦想世界APP的SO库
- 调用getPriId()获取设备密钥ID
- 调用rsaSign()进行RSA签名
- 绕过了APP的所有检测机制
这为后续的完整调用链实现奠定了基础。
4.11.3 下一步
在下一章中,我们将:
- 深入分析JNI调用链:理解每个方法的作用和调用顺序
- 实现完整的安全激活流程:从获取密钥到API调用
- 处理服务器响应:解密返回的密钥数据
- 构建可用的数据获取系统:实现商品数据抓取
本章思考题
-
为什么Unidbg能绕过Frida检测? 如果APP在检测代码中使用了更复杂的方法(如检测Unicorn引擎的特征),Unidbg还能绕过吗?
-
JNI_OnLoad和.init_array的区别是什么? 为什么有些检测代码放在.init_array中而不是JNI_OnLoad中?
-
如果SO库使用了OLLVM混淆,Unidbg还能正常工作吗?为什么?
-
在生产环境中使用Unidbg有什么注意事项? 如何保证稳定性和性能?
章节附录
A. Unidbg项目结构
bash
unidbg/
├── unidbg-android/ # Android模拟支持
│ ├── src/main/java/
│ │ └── com/github/unidbg/
│ │ ├── linux/android/
│ │ │ ├── dvm/ # DalvikVM实现
│ │ │ └── AndroidEmulatorBuilder.java
│ │ └── ...
│ └── src/main/resources/
│ └── android/sdk/ # Android SDK文件
├── unidbg-ios/ # iOS模拟支持
├── unidbg-api/ # 核心API
└── backend/
├── unicorn/ # Unicorn后端
└── dynarmic/ # Dynarmic后端
B. 常用JNI签名速查
java
// 基本类型
"()V" // void method()
"()I" // int method()
"()J" // long method()
"()Z" // boolean method()
"()Ljava/lang/String;" // String method()
// 带参数
"(I)V" // void method(int)
"(II)I" // int method(int, int)
"(Ljava/lang/String;)V" // void method(String)
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
// String method(String, String)
// 数组
"()[B" // byte[] method()
"()[Ljava/lang/String;" // String[] method()
"([B)V" // void method(byte[])
C. 参考资源
| 资源 | 链接 |
|---|---|
| Unidbg GitHub | github.com/zhkl0228/un... |
| Unicorn Engine | www.unicorn-engine.org/ |
| JNI规范 | docs.oracle.com/javase/8/do... |
| ARM64指令集 | developer.arm.com/documentati... |
本章完