Android 逆向工程完全指南 - 从零基础到实战
本文档面向完全不懂 Android 的读者,从基础概念讲起,逐步深入到实际逆向案例。
目录
基础概念篇
1.1 什么是 Android?
Android 是一个基于 Linux 的移动操作系统,主要用于智能手机和平板电脑。从逆向工程的角度,你需要了解以下核心架构:
┌─────────────────────────────────────┐
│ 应用层 (Java/Kotlin) │ <- 我们通常看到的 APP 代码
├─────────────────────────────────────┤
│ 应用框架层 (Android Framework) │ <- 系统 API
├─────────────────────────────────────┤
│ 系统库层 (C/C++) │ <- Native 代码运行在这里
├─────────────────────────────────────┤
│ Linux 内核 │ <- 操作系统核心
└─────────────────────────────────────┘
关键点:
- Java 层: APP 的主要逻辑,使用 Java 或 Kotlin 编写
- Native 层: 核心算法通常用 C/C++ 编写,编译成
.so文件(类似 Windows 的.dll) - JNI (Java Native Interface): Java 和 Native 之间的桥梁
1.2 什么是 SO 文件?
SO (Shared Object) = 共享对象 = 动态链接库
Windows: .dll 文件
Linux: .so 文件
macOS: .dylib 文件
为什么 APP 要使用 SO 文件?
- 性能: C/C++ 比 Java 快得多
- 保密: 二进制代码比 Java 字节码更难反编译
- 跨平台: 一份 C 代码可以编译到多个平台
示例结构:
app.apk (解压后)
├── classes.dex <- Java 代码(编译后)
├── lib/
│ ├── arm64-v8a/ <- 64 位 ARM 处理器
│ │ └── libthree.so <- 这就是我们要逆向的文件!
│ ├── armeabi-v7a/ <- 32 位 ARM 处理器
│ └── x86_64/ <- Intel 处理器(模拟器常用)
└── resources.arsc <- 资源文件
1.3 什么是逆向工程?
正向开发:
源代码 (.c, .java) -> 编译 -> 二进制文件 (.so, .dex)
逆向工程:
二进制文件 (.so, .dex) -> 反编译/分析 -> 理解逻辑 -> 复现功能
典型场景:
- 安全分析: 检测恶意软件
- 漏洞挖掘: 寻找安全漏洞
- 功能复现: 学习某个算法的实现(如本案例)
- 兼容性测试: 分析第三方 SDK
工具介绍篇
2.1 Frida - 动态分析神器
Frida 是什么?
Frida 是一个动态代码插桩框架,可以在运行时修改应用程序的行为。
传统分析: 静态分析代码 -> 猜测逻辑
Frida: 运行 APP -> 实时注入代码 -> 查看真实数据流
核心能力:
- Hook 函数(拦截函数调用)
- 查看/修改函数参数和返回值
- 调用任意函数
- 追踪代码执行流程
简单示例 (Hook Java 方法):
// 找到目标类
var MyClass = Java.use("com.example.MyClass");
// Hook sign 方法
MyClass.sign.implementation = function(arg) {
console.log("[+] sign() called with arg: " + arg);
// 调用原始方法
var result = this.sign(arg);
console.log("[+] sign() returned: " + result);
return result;
};
在本案例中的作用:
- 发现 APP 调用了哪个 SO 文件的哪个函数
- 捕获函数的输入参数
- 观察函数的返回值
- 验证我们的 Unidbg 实现是否正确
2.2 Unidbg - 模拟执行引擎
Unidbg 是什么?
Unidbg 是一个基于 Unicorn 引擎的 Android 模拟器,可以在不需要真实设备的情况下执行 SO 文件。
真实设备: 需要手机/模拟器 -> 安装 APP -> 抓包/Hook -> 分析
Unidbg: 直接加载 SO -> 调用函数 -> 获得结果
核心优势:
- 速度快: 不需要启动完整的 Android 系统
- 可控: 完全掌控执行环境
- 无需设备: 在服务器上批量调用
- 易调试: 可以设置断点、查看内存
简单示例:
// 创建 Android 模拟器
AndroidEmulator emulator = AndroidEmulatorBuilder
.for64Bit() // 64 位模拟器
.build();
// 加载 SO 文件
DalvikModule dm = vm.loadLibrary(new File("libexample.so"), false);
// 调用函数(偏移地址 0x1234)
Number result = module.callFunction(emulator, 0x1234, arg1, arg2);
在本案例中的作用:
- 加载
libthree.so文件 - 模拟 Android 环境(JNI、系统库等)
- 调用
sign()函数生成签名 - 提供 HTTP 服务供 Python 爬虫调用
2.3 IDA Pro / Ghidra - 静态分析工具
用途: 反汇编 SO 文件,查看函数内部逻辑
libthree.so (二进制)
↓ 用 IDA 打开
汇编代码 / 伪 C 代码
在本案例中的发现:
- 找到
sign()函数的偏移地址:0x21294 - 分析函数参数:
int + 8 个 double - 发现返回值是 20 字节的 byte 数组
JNI 深度解析
3.1 JNI 是什么?
JNI (Java Native Interface) 是 Java 调用 Native 代码的桥梁。
Java 代码 Native 代码 (C/C++)
┌──────────────┐ ┌──────────────┐
│ MyClass.java │ │ mylib.so │
│ │ │ │
│ native void │ <-- JNI --> │ JNIEXPORT │
│ doSomething()│ │ Java_... │
└──────────────┘ └──────────────┘
3.2 JNI 函数命名规则
Native 函数名遵循严格的命名规范:
Java_<包名>_<类名>_<方法名>
示例:
package com.yuanrenxue.challenge.three;
public class ChallengeThreeNativeLib {
public native byte[] sign(int arg, ...);
}
对应的 Native 函数名:
Java_com_yuanrenxue_challenge_three_ChallengeThreeNativeLib_sign
IDA 中实际看到的名字 (经过编译器处理):
_Z45Java_com_yuanrenxue_challenge_three_...
3.3 JNI 函数参数详解
标准 JNI 函数签名:
JNIEXPORT <返回类型> JNICALL Java_包名_类名_方法名(
JNIEnv* env, // 参数 1: JNI 环境指针(必须)
jobject thiz, // 参数 2: Java 对象引用(实例方法)
<其他参数> // 参数 3+: 实际的业务参数
)
*参数 1: JNIEnv (JNI 环境指针)**
JNIEnv* 是一个函数表指针,提供了所有 JNI 操作的入口。
作用:
- 创建 Java 对象
- 调用 Java 方法
- 访问 Java 字段
- 抛出异常
- 数组操作
- 字符串转换
常用方法:
// 创建 byte 数组
jbyteArray array = (*env)->NewByteArray(env, 20);
// 设置数组内容
(*env)->SetByteArrayRegion(env, array, 0, 20, buffer);
// 调用 Java 方法
(*env)->CallVoidMethod(env, obj, methodID, args...);
// 获取字符串
const char* str = (*env)->GetStringUTFChars(env, jstr, NULL);
在 Unidbg 中:
// 获取 JNIEnv 指针
vm.getJNIEnv() // 返回 JNIEnv* 的地址
参数 2: jobject / jclass (this 指针)
- 实例方法:
jobject thiz- 指向调用该方法的 Java 对象(相当于 Java 中的this) - 静态方法:
jclass clazz- 指向 Java 类对象(Class 对象)
示例:
// Java 代码
public class MyClass {
private int value = 100;
public native int getValue(); // 实例方法
public static native int getMax(); // 静态方法
}
// Native 代码 - 实例方法
JNIEXPORT jint JNICALL Java_MyClass_getValue(
JNIEnv* env,
jobject thiz // <- 可以通过这个访问 MyClass 的实例字段
) {
// 获取 value 字段
jclass cls = (*env)->GetObjectClass(env, thiz);
jfieldID fid = (*env)->GetFieldID(env, cls, "value", "I");
return (*env)->GetIntField(env, thiz, fid);
}
// Native 代码 - 静态方法
JNIEXPORT jint JNICALL Java_MyClass_getMax(
JNIEnv* env,
jclass clazz // <- 指向 MyClass.class
) {
return 2147483647;
}
在本案例中:
sign()是实例方法,所以有jobject thiz参数- 但我们的函数逻辑不使用
thiz,所以在 Unidbg 中可以传0
参数 3+: 业务参数
JNI 类型与 Java 类型的对应关系:
| Java 类型 | JNI 类型 | C 类型 | 说明 |
|---|---|---|---|
| boolean | jboolean | unsigned char | 1 字节 |
| byte | jbyte | signed char | 1 字节 |
| char | jchar | unsigned short | 2 字节(Unicode) |
| short | jshort | short | 2 字节 |
| int | jint | int | 4 字节 |
| long | jlong | long long | 8 字节 |
| float | jfloat | float | 4 字节 |
| double | jdouble | double | 8 字节 |
| String | jstring | - | 引用类型 |
| byte[] | jbyteArray | - | 数组引用 |
| Object | jobject | - | 对象引用 |
本案例的函数签名:
__int64 __fastcall Java_com_yuanrenxue_challenge_three_ChallengeThreeNativeLib_sign(
__int64 a1, // JNIEnv* env
__int64 a2, // jobject thiz
unsigned int a3, // int 参数(业务参数)
double a4, // double 参数 1
double a5, // double 参数 2
double a6, // double 参数 3
double a7, // double 参数 4
double a8, // double 参数 5
double a9, // double 参数 6
double a10, // double 参数 7
double a11 // double 参数 8
)
3.4 JNI 方法签名(Method Signature)
JNI 使用特殊的字符串表示方法签名:
sign(IDDDDDDDD)[B
│ │ ││
│ │ │└─ 返回值: byte[]
│ │ └── 8 个 double
│ └─────────── int
└──────────────── 方法名
类型符号表:
| 符号 | Java 类型 | 说明 |
|---|---|---|
| Z | boolean | |
| B | byte | |
| C | char | |
| S | short | |
| I | int | |
| J | long | |
| F | float | |
| D | double | |
| L; | Ljava/lang/String; | 对象类型(L开头,;结尾) |
| [ | [I, [B, [Ljava/lang/String; | 数组([ 前缀) |
| V | void | 仅用于返回值 |
示例:
// Java 方法
public native byte[] sign(int arg);
// JNI 签名
sign(I)[B
public native String encrypt(String data, byte[] key);
// JNI 签名
encrypt(Ljava/lang/String;[B)Ljava/lang/String;
public native void init();
// JNI 签名
init()V
public native long calculate(double x, double y, int flag);
// JNI 签名
calculate(DDI)J
3.5 JNI 返回值处理
基本类型 - 直接返回
JNIEXPORT jint JNICALL Java_MyClass_add(JNIEnv* env, jobject thiz, jint a, jint b) {
return a + b; // 直接返回
}
引用类型 - 需要创建对象
返回字符串:
JNIEXPORT jstring JNICALL Java_MyClass_getName(JNIEnv* env, jobject thiz) {
const char* name = "Alice";
return (*env)->NewStringUTF(env, name);
}
返回 byte 数组(本案例):
JNIEXPORT jbyteArray JNICALL Java_..._sign(...) {
// 1. 计算签名,结果存在 buffer 中
unsigned char buffer[20];
calculate_signature(buffer); // 假设的签名计算函数
// 2. 创建 Java byte[] 对象
jbyteArray result = (*env)->NewByteArray(env, 20);
// 3. 将数据复制到 Java 数组
(*env)->SetByteArrayRegion(env, result, 0, 20, (jbyte*)buffer);
// 4. 返回数组引用
return result;
}
在 Unidbg 中:
// Unidbg 自动处理了 JNI 返回值
DvmObject<?> result = instance.callJniMethodObject(emulator, "sign(I)[B", arg);
// 提取 byte[]
byte[] bytes = (byte[]) result.getValue();
实战案例分析
4.1 案例背景
目标: 猿人学爬虫挑战赛 - ChallengeThree
任务: 逆向分析 APP 的签名算法,在不使用真实 APP 的情况下生成有效签名。
技术栈:
- 目标 APP: Android APK(包含
libthree.so) - 分析工具: Frida(动态分析) + IDA Pro(静态分析)
- 模拟工具: Unidbg(模拟执行)
- 应用场景: Python 爬虫调用签名服务
4.2 逆向分析流程
步骤 1: 静态分析 - 找到目标函数
使用工具: IDA Pro
操作:
- 用 IDA 打开
libthree.so - 搜索 "sign" 关键字
- 找到函数:
Java_com_yuanrenxue_challenge_three_ChallengeThreeNativeLib_sign - 记录函数偏移地址:
0x21294
反编译结果:
__int64 __fastcall Java_com_yuanrenxue_challenge_three_ChallengeThreeNativeLib_sign(
__int64 a1, // JNIEnv*
__int64 a2, // jobject (this)
unsigned int a3, // int 参数
double a4, // double 参数 1
double a5, // double 参数 2
double a6, // double 参数 3
double a7, // double 参数 4
double a8, // double 参数 5
double a9, // double 参数 6
double a10, // double 参数 7
double a11 // double 参数 8
) {
// ... 复杂的计算逻辑 ...
// 关键点: 分配 20 字节内存
v14 = (_BYTE *)malloc(20LL);
// 核心算法(我们不需要完全理解)
sub_20520((__int64)&v17, v14, v13);
// 创建 Java byte[] 对象(长度 20)
v16 = sub_21214(v12, 20LL);
// 填充数据
sub_21248(v12, v16, 0LL, 20LL, v14);
// 返回 byte[]
return v16;
}
关键信息:
- ✅ 函数偏移:
0x21294 - ✅ 参数: 1 个
int+ 8 个double - ✅ 返回值: 20 字节的
byte[]
步骤 2: 动态分析 - 捕获真实参数
使用工具: Frida
Frida 脚本 (简化版):
Java.perform(function() {
var NativeLib = Java.use("com.yuanrenxue.challenge.three.ChallengeThreeNativeLib");
NativeLib.sign.implementation = function(arg, d1, d2, d3, d4, d5, d6, d7, d8) {
console.log("[+] sign() called");
console.log(" int: " + arg);
console.log(" doubles: " + d1 + ", " + d2 + ", " + d3 + ", " +
d4 + ", " + d5 + ", " + d6 + ", " + d7 + ", " + d8);
// 调用原函数
var result = this.sign(arg, d1, d2, d3, d4, d5, d6, d7, d8);
// 转换为十六进制
var hex = "";
for (var i = 0; i < result.length; i++) {
hex += ("0" + (result[i] & 0xFF).toString(16)).slice(-2);
}
console.log(" Result: " + hex);
return result;
};
});
捕获到的真实调用:
[+] sign() called
int: 4
doubles: 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
Result: 9e607d2c7e7c7c8816ecbe27e61d8f024bbcdd2b
步骤 3: Unidbg 实现 - 模拟执行
目标: 在不使用真实 APP 的情况下,调用 sign() 函数
代码结构:
ChallengeThreeUnidbg.java
├── 创建 64 位 Android 模拟器
├── 加载 libthree.so
├── 注册 Java 类
├── 实现 JNI 回调(如果需要)
└── 调用 sign() 方法
核心代码解析:
public class ChallengeThreeUnidbg extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass nativeLibClass;
public ChallengeThreeUnidbg() throws IOException {
// ==================== 1. 创建模拟器 ====================
emulator = AndroidEmulatorBuilder
.for64Bit() // 64 位(arm64-v8a)
.setProcessName("com.yuanrenxue.challenge")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // Android 6.0
// ==================== 2. 创建 Dalvik VM ====================
vm = emulator.createDalvikVM();
vm.setVerbose(false); // 调试时设为 true
vm.setJni(this); // 注册 JNI 回调实现
// ==================== 3. 注册 Java 类 ====================
// 必须在加载 SO 之前注册!
nativeLibClass = vm.resolveClass(
"com/yuanrenxue/challenge/three/ChallengeThreeNativeLib"
);
// ==================== 4. 加载 SO 文件 ====================
DalvikModule dm = vm.loadLibrary(new File(SO_PATH), false);
module = dm.getModule();
// ==================== 5. 调用 JNI_OnLoad ====================
// 很多 SO 在 JNI_OnLoad 中注册 native 方法
dm.callJNI_OnLoad(emulator);
}
// ==================== 6. 调用 sign 方法 ====================
public byte[] sign(int arg) {
// 创建类实例
DvmObject<?> instance = nativeLibClass.newObject(null);
// 调用 JNI 方法
// 方法签名: sign(I)[B
// - I = int
// - [B = byte[]
DvmObject<?> result = instance.callJniMethodObject(
emulator,
"sign(I)[B", // JNI 方法签名
arg // 参数
);
// 提取 byte[]
if (result != null) {
return (byte[]) result.getValue();
}
return null;
}
}
关键点解释:
为什么要创建 64 位模拟器?
.for64Bit() // 因为 SO 文件在 arm64-v8a 目录
arm64-v8a= 64 位 ARM 架构armeabi-v7a= 32 位 ARM 架构
为什么要注册 Java 类?
vm.resolveClass("com/yuanrenxue/challenge/three/ChallengeThreeNativeLib");
- JNI 方法名包含包名和类名
- Unidbg 需要知道这个类存在
- 如果不注册,调用时会报
ClassNotFoundException
为什么要调用 JNI_OnLoad?
dm.callJNI_OnLoad(emulator);
- 很多 SO 文件在
JNI_OnLoad中动态注册 native 方法 - 如果不调用,可能找不到
sign()方法
callJniMethodObject 做了什么?
instance.callJniMethodObject(emulator, "sign(I)[B", arg)
内部流程:
- 根据方法签名
sign(I)[B查找 native 方法 - 构造 JNI 调用栈(JNIEnv*, jobject, 参数...)
- 跳转到
0x21294执行 native 代码 - 模拟执行所有指令(包括系统调用、JNI 回调等)
- 捕获返回值(jbyteArray 引用)
- 转换为 Java 对象返回
步骤 4: 结果转换 - 匹配 APP 行为
问题: Unidbg 返回的是 byte[],但 APP 最终使用的是十六进制字符串
分析:
通过逆向 APP 的 Java 代码,发现有一个 OooO0oO() 方法:
// 反编译的 APP 代码
public static String OooO0oO(byte[] bArr) {
char[] cArr = new char[bArr.length * 2];
for (int i = 0; i < bArr.length; i++) {
int i2 = bArr[i] & 0xFF;
int i3 = i * 2;
char[] hexChars = "0123456789ABCDEF".toCharArray();
cArr[i3] = hexChars[i2 >>> 4]; // 高 4 位
cArr[i3 + 1] = hexChars[i2 & 15]; // 低 4 位
}
return new String(cArr);
}
完整流程:
sign(4)
↓
byte[] rawBytes = [0x9e, 0x60, 0x7d, ...] (20 字节)
↓
OooO0oO(rawBytes)
↓
String hex = "9E607D2C7E7C7C8816ECBE27E61D8F024BBCDD2B" (40 字符)
在 Unidbg 中实现:
public String signToHex(int arg) {
byte[] rawBytes = sign(arg);
if (rawBytes == null) return null;
// 应用 OooO0oO() 转换
return OooO0oO(rawBytes);
}
private static String OooO0oO(byte[] bArr) {
char[] cArr = new char[bArr.length * 2];
for (int i = 0; i < bArr.length; i++) {
int i2 = bArr[i] & 0xFF;
int i3 = i * 2;
char[] f8549OooO0oO = "0123456789ABCDEF".toCharArray();
cArr[i3] = f8549OooO0oO[i2 >>> 4];
cArr[i3 + 1] = f8549OooO0oO[i2 & 15];
}
return new String(cArr);
}
步骤 5: 提供 HTTP 服务 - 供爬虫调用
目标: Python 爬虫通过 HTTP 请求获取签名
实现:
public class SignServer {
private static ChallengeThreeUnidbg caller;
private static final int PORT = 8899;
public static void main(String[] args) throws IOException {
// 初始化 Unidbg(只初始化一次)
caller = new ChallengeThreeUnidbg();
// 创建 HTTP 服务器
HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0);
// 注册路由
server.createContext("/sign", new SignHandler());
server.createContext("/health", new HealthHandler());
server.start();
System.out.println("Sign Server Started on port " + PORT);
}
static class SignHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
// 解析参数: /sign?arg=4
String query = exchange.getRequestURI().getQuery();
int arg = parseArg(query);
// 调用签名方法
String signature = caller.signToHex(arg);
// 返回 JSON
String json = String.format(
"{"success": true, "arg": %d, "signature": "%s"}",
arg, signature
);
sendResponse(exchange, 200, json);
}
}
}
Python 调用示例:
import requests
# 请求签名
resp = requests.post('http://localhost:8899/sign?arg=4')
data = resp.json()
print(data)
# 输出: {'success': True, 'arg': 4, 'signature': '9E607D2C7E7C7C8816ECBE27E61D8F024BBCDD2B'}
# 使用签名发起爬虫请求
headers = {'X-Signature': data['signature']}
requests.get('https://api.yuanrenxue.com/challenge/3', headers=headers)
4.3 JNI 回调实现
问题: SO 文件可能回调 Java 方法
示例场景:
// Native 代码需要获取 Android 系统信息
jclass looperClass = (*env)->FindClass(env, "android/os/Looper");
jobject looper = (*env)->CallStaticObjectMethod(env, looperClass, myLooperMethod);
如果不实现回调: 程序会崩溃或返回错误
在 Unidbg 中实现:
public class ChallengeThreeUnidbg extends AbstractJni {
@Override
public DvmObject<?> callStaticObjectMethodV(
BaseVM vm,
DvmClass dvmClass,
String signature,
VaList vaList
) {
System.out.println("[JNI] callStaticObjectMethodV: " + signature);
switch (signature) {
case "android/os/Looper->myLooper()Landroid/os/Looper;":
// 返回一个假的 Looper 对象
return vm.resolveClass("android/os/Looper").newObject(null);
case "com/example/Utils->getDeviceId()Ljava/lang/String;":
// 返回假的设备 ID
return new StringObject(vm, "device_12345");
}
throw new UnsupportedOperationException(signature);
}
@Override
public int callStaticIntMethodV(
BaseVM vm,
DvmClass dvmClass,
String signature,
VaList vaList
) {
System.out.println("[JNI] callStaticIntMethodV: " + signature);
if (signature.equals("android/os/Build$VERSION->SDK_INT:I")) {
return 23; // Android 6.0
}
return super.callStaticIntMethodV(vm, dvmClass, signature, vaList);
}
}
调试技巧:
vm.setVerbose(true); // 打印所有 JNI 调用
输出示例:
[JNI] callStaticObjectMethodV: android/os/Looper->myLooper()Landroid/os/Looper;
[JNI] callVoidMethod: android/os/Handler-><init>(Landroid/os/Looper;)V
[JNI] callStaticIntMethodV: android/os/Build$VERSION->SDK_INT:I
根据输出逐步实现缺失的回调。
4.4 完整测试流程
测试代码:
public static void main(String[] args) {
ChallengeThreeUnidbg caller = null;
try {
// 1. 初始化 Unidbg
caller = new ChallengeThreeUnidbg();
// 2. 测试调用
System.out.println("--- Test Case: sign(4) ---");
String hex = caller.signToHex(4);
System.out.println("Unidbg result: " + hex);
// 3. 与 Frida 捕获的结果对比
String fridaResult = "9E607D2C7E7C7C8816ECBE27E61D8F024BBCDD2B";
System.out.println("Frida result: " + fridaResult);
System.out.println("Match: " + hex.equals(fridaResult));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (caller != null) {
caller.destroy();
}
}
}
预期输出:
[+] Unidbg initialized
Module base: 0x40000000
Target function: 0x40021294
[*] Calling sign(4)
Raw bytes length: 20
Final hex string: 9E607D2C7E7C7C8816ECBE27E61D8F024BBCDD2B
--- Test Case: sign(4) ---
Unidbg result: 9E607D2C7E7C7C8816ECBE27E61D8F024BBCDD2B
Frida result: 9E607D2C7E7C7C8816ECBE27E61D8F024BBCDD2B
Match: true
常见问题与调试技巧
5.1 常见错误
错误 1: UnsatisfiedLinkError
java.lang.UnsatisfiedLinkError: No implementation found for byte[]
com.yuanrenxue.challenge.three.ChallengeThreeNativeLib.sign(int, ...)
原因:
- SO 文件路径错误
- 未调用
JNI_OnLoad - 类名/方法名不匹配
解决:
// 检查文件是否存在
File soFile = new File(SO_PATH);
System.out.println("SO exists: " + soFile.exists());
// 确保类名完全匹配
vm.resolveClass("com/yuanrenxue/challenge/three/ChallengeThreeNativeLib");
// 调用 JNI_OnLoad
dm.callJNI_OnLoad(emulator);
错误 2: 返回 null
byte[] result = sign(4);
// result == null
原因:
- 缺少 JNI 回调实现
- 函数内部逻辑错误
- 参数错误
解决:
// 1. 启用 verbose 模式
vm.setVerbose(true);
// 2. 查看日志,找到缺失的回调
[JNI] callStaticObjectMethodV: android/os/Looper->myLooper()Landroid/os/Looper;
java.lang.UnsupportedOperationException: android/os/Looper->myLooper()Landroid/os/Looper;
// 3. 实现回调
@Override
public DvmObject<?> callStaticObjectMethodV(...) {
if (signature.equals("android/os/Looper->myLooper()Landroid/os/Looper;")) {
return vm.resolveClass("android/os/Looper").newObject(null);
}
...
}
错误 3: 结果不匹配
Unidbg: 9E607D2C...
Frida: A1B2C3D4...
原因:
- 参数传递错误
- 环境变量不同(时间戳、随机数等)
- SO 文件版本不一致
解决:
// 1. 打印详细参数
System.out.println("Input arg: " + arg);
System.out.println("Raw bytes: " + Arrays.toString(rawBytes));
// 2. 对比中间结果
// 在 IDA 中设置断点,查看关键变量
// 3. Hook Native 层函数
emulator.attach().addBreakPoint(module.base + 0x20520, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext ctx = emulator.getContext();
System.out.println("x0: 0x" + Long.toHexString(ctx.getLongArg(0)));
System.out.println("x1: 0x" + Long.toHexString(ctx.getLongArg(1)));
return true;
}
});
5.2 调试技巧
技巧 1: 设置断点
// 在函数入口设置断点
emulator.attach().addBreakPoint(module.base + 0x21294, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
System.out.println("[Breakpoint] sign() entry");
// 打印寄存器
RegisterContext ctx = emulator.getContext();
System.out.println(" x0 (JNIEnv*): 0x" + Long.toHexString(ctx.getLongArg(0)));
System.out.println(" x1 (jobject): 0x" + Long.toHexString(ctx.getLongArg(1)));
System.out.println(" x2 (int arg): " + ctx.getIntArg(2));
return true; // 继续执行
}
});
技巧 2: 追踪内存读写
// 监控某个地址的读写
emulator.getMemory().addHook(new WriteHook() {
@Override
public void hook(Backend backend, long address, int size, long value, Object user) {
System.out.println("[Memory Write] 0x" + Long.toHexString(address) +
" = 0x" + Long.toHexString(value));
}
});
技巧 3: 导出执行日志
// 记录所有指令执行
emulator.attach().addBreakPoint(module, new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
System.out.println("0x" + Long.toHexString(address));
}
});
5.3 性能优化
优化 1: 复用 Unidbg 实例
// 错误: 每次调用都创建新实例
for (int i = 0; i < 1000; i++) {
ChallengeThreeUnidbg caller = new ChallengeThreeUnidbg(); // 慢!
caller.sign(i);
caller.destroy();
}
// 正确: 复用实例
ChallengeThreeUnidbg caller = new ChallengeThreeUnidbg();
for (int i = 0; i < 1000; i++) {
caller.sign(i); // 快!
}
caller.destroy();
优化 2: 关闭 Verbose
vm.setVerbose(false); // 生产环境关闭日志
优化 3: 预加载依赖
// 如果 SO 依赖其他库,提前加载
vm.loadLibrary(new File("libssl.so"), false);
vm.loadLibrary(new File("libcrypto.so"), false);
vm.loadLibrary(new File("libthree.so"), false);
总结
知识点回顾
Android 基础
- ✅ Android 应用分为 Java 层和 Native 层
- ✅ SO 文件是编译后的 C/C++ 代码(类似 Windows 的 DLL)
- ✅ 不同 CPU 架构需要不同的 SO 文件(arm64-v8a, armeabi-v7a 等)
JNI 核心概念
- ✅ **JNIEnv***: JNI 环境指针,提供所有 JNI 操作的接口
- ✅ jobject / jclass: Java 对象引用(实例方法用 jobject,静态方法用 jclass)
- ✅ JNI 类型映射: Java int → jint, Java double → jdouble, 等等
- ✅ JNI 方法签名: 使用特殊字符表示(如
sign(I)[B) - ✅ 返回引用类型: 需要通过 JNIEnv 创建对象(如 NewByteArray)
逆向分析流程
- 静态分析 (IDA Pro): 找到函数偏移、分析参数和返回值
- 动态分析 (Frida): 捕获真实调用,查看输入输出
- 模拟执行 (Unidbg): 在本地复现函数调用
- 结果验证: 对比 Unidbg 结果与真实 APP 结果
- 生产部署: 提供 HTTP 服务供业务调用
Unidbg 核心步骤
- 创建 Android 模拟器(指定架构: 32 位或 64 位)
- 创建 Dalvik VM 并注册 JNI 回调
- 注册 Java 类(必须在加载 SO 之前)
- 加载 SO 文件
- 调用 JNI_OnLoad(如果需要)
- 调用目标方法
- 根据运行时错误实现缺失的 JNI 回调
调试技巧
- ✅ 启用
vm.setVerbose(true)查看详细 JNI 调用 - ✅ 设置断点查看寄存器和内存
- ✅ 对比 Frida 捕获的结果验证正确性
- ✅ 逐步实现 JNI 回调,直到程序正常运行
本案例的技术要点
目标函数
Java_com_yuanrenxue_challenge_three_ChallengeThreeNativeLib_sign(
JNIEnv* env, // JNI 环境指针
jobject thiz, // Java 对象引用(本例中未使用)
int arg, // 业务参数(实际 APP 中传入的值)
double d1-d8 // 8 个 double 参数(本例中传 0.0)
) -> byte[20] // 返回 20 字节的签名
调用流程
Python 爬虫
↓ HTTP 请求
SignServer (Java)
↓ 调用
ChallengeThreeUnidbg
↓ 模拟执行
libthree.so (Native)
↓ 返回
byte[20] 原始签名
↓ 转换
String (40 个十六进制字符)
↓ 返回
Python 爬虫
核心难点及解决方案
| 难点 | 解决方案 |
|---|---|
| 不知道函数在哪里 | IDA Pro 搜索函数名,找到偏移地址 |
| 不知道参数是什么 | Frida Hook Java 方法,捕获真实调用 |
| 返回 null | 实现缺失的 JNI 回调(callStaticObjectMethodV 等) |
| 结果不匹配 | 分析 Java 层的后处理逻辑(OooO0oO 方法) |
| 需要批量调用 | 复用 Unidbg 实例,提供 HTTP 服务 |
实用场景
1. 爬虫签名生成
# Python 爬虫调用签名服务
def get_signature(page):
resp = requests.post(f'http://localhost:8899/sign?arg={page}')
return resp.json()['signature']
for page in range(1, 100):
sig = get_signature(page)
headers = {'X-Signature': sig}
data = requests.get(f'https://api.example.com/data?page={page}', headers=headers)
print(data.json())
2. 自动化测试
// 批量测试不同参数的签名结果
for (int i = 1; i <= 1000; i++) {
String sig = caller.signToHex(i);
// 验证签名格式
assert sig.length() == 40;
assert sig.matches("[0-9A-F]+");
}
3. 签名算法研究
// 分析输入和输出的关系
Map<Integer, String> results = new HashMap<>();
for (int i = 1; i <= 10; i++) {
results.put(i, caller.signToHex(i));
}
// 寻找规律...
扩展阅读
进阶主题
- SO 加固破解: 处理 VMP、Ollvm 等加固的 SO
- 反调试对抗: 绕过 SO 中的反调试检测
- 算法还原: 通过汇编代码还原加密算法
- 协议分析: 结合 Wireshark 分析网络协议
- 自动化逆向: 使用 Ghidra Script、IDA Python 等
相关工具
- 静态分析: IDA Pro, Ghidra, Hopper, Binary Ninja
- 动态分析: Frida, Xposed, Substrate
- 模拟执行: Unidbg, Qiling Framework, Unicorn
- 网络抓包: Charles, Fiddler, mitmproxy, Wireshark
- 脱壳加固: Frida-Unpack, FART, GDA
学习资源
- 《Android 软件安全与逆向分析》
- 《Android 安全攻防权威指南》
- Frida 官方文档: https://frida.re/docs/
- Unidbg GitHub: https://github.com/zhkl0228/unidbg
- 猿人学爬虫题库: https://www.yuanrenxue.com
附录
A. 完整代码清单
unidbg-android/src/main/java/com/yuanrenxue/
├── ChallengeThreeUnidbg.java # Unidbg 实现(核心)
├── SignServer.java # HTTP 签名服务
├── SOCallerExample.java # 通用 SO 调用示例
├── SOCallerTemplate.java # 快速开发模板
└── ChallengeThree-GUIDE.md # 使用指南
B. 快速启动命令
# 1. 编译项目
mvn clean package
# 2. 运行测试
java -cp target/classes com.yuanrenxue.ChallengeThreeUnidbg
# 3. 启动签名服务
java -cp target/classes com.yuanrenxue.SignServer
# 4. Python 调用示例
python3 <<EOF
import requests
resp = requests.post('http://localhost:8899/sign?arg=4')
print(resp.json())
EOF
C. JNI 类型速查表
| Java 类型 | JNI 类型 | 签名 | C 类型 | 字节数 |
|---|---|---|---|---|
| void | - | V | void | 0 |
| boolean | jboolean | Z | unsigned char | 1 |
| byte | jbyte | B | signed char | 1 |
| char | jchar | C | unsigned short | 2 |
| short | jshort | S | short | 2 |
| int | jint | I | int | 4 |
| long | jlong | J | long long | 8 |
| float | jfloat | F | float | 4 |
| double | jdouble | D | double | 8 |
| Object | jobject | Ljava/lang/Object; | - | - |
| String | jstring | Ljava/lang/String; | - | - |
| Class | jclass | Ljava/lang/Class; | - | - |
| byte[] | jbyteArray | [B | - | - |
| int[] | jintArray | [I | - | - |
| Object[] | jobjectArray | [Ljava/lang/Object; | - | - |
D. Unidbg JNI 回调速查
| Java 操作 | 对应的 Unidbg 回调方法 |
|---|---|
| Object.method() | callObjectMethodV() |
| Class.staticMethod() | callStaticObjectMethodV() |
| void method() | callVoidMethod() |
| int method() | callIntMethodV() |
| boolean method() | callBooleanMethodV() |
| String.method() | callObjectMethodV() (返回 StringObject) |
| field = value | setObjectField() |
| value = field | getObjectField() |
| new Object() | newObjectV() |
结语
通过本文档,你应该已经掌握了:
- Android 和 JNI 的基础知识 - 理解 Java 层和 Native 层的关系
- 逆向分析的基本流程 - 静态分析 + 动态分析 + 模拟执行
- Frida 和 Unidbg 的使用 - 实战工具的应用
- JNI 参数和返回值处理 - 深入理解 JNIEnv、jobject、方法签名等概念
- 实际项目的开发经验 - 从分析到部署的完整流程
逆向工程需要大量实践,建议:
- 多分析不同的 APP 和 SO 文件
- 深入学习汇编语言(ARM/x86)
- 理解常见的加密算法和反调试技术
- 参与 CTF 竞赛和技术社区
重要提醒:
- 逆向分析应当遵守法律法规
- 不得用于非法破解和侵权行为
- 仅用于学习研究和安全测试
祝学习愉快! 🚀
评论(已关闭)
评论已关闭