boxmoe_header_banner_img

Welcome 欢迎来到GreetG的个人空间

加载中

文章导读

Android逆向工程完全指南


avatar
admin 2025 年 11 月 12 日 152

Android 逆向工程完全指南 - 从零基础到实战

本文档面向完全不懂 Android 的读者,从基础概念讲起,逐步深入到实际逆向案例。


目录

  1. 基础概念篇
  2. 工具介绍篇
  3. JNI 深度解析
  4. 实战案例分析
  5. 常见问题与调试技巧

基础概念篇

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 文件?

  1. 性能: C/C++ 比 Java 快得多
  2. 保密: 二进制代码比 Java 字节码更难反编译
  3. 跨平台: 一份 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) -> 反编译/分析 -> 理解逻辑 -> 复现功能

典型场景:

  1. 安全分析: 检测恶意软件
  2. 漏洞挖掘: 寻找安全漏洞
  3. 功能复现: 学习某个算法的实现(如本案例)
  4. 兼容性测试: 分析第三方 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;
};

在本案例中的作用:

  1. 发现 APP 调用了哪个 SO 文件的哪个函数
  2. 捕获函数的输入参数
  3. 观察函数的返回值
  4. 验证我们的 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);

在本案例中的作用:

  1. 加载 libthree.so 文件
  2. 模拟 Android 环境(JNI、系统库等)
  3. 调用 sign() 函数生成签名
  4. 提供 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

操作:

  1. 用 IDA 打开 libthree.so
  2. 搜索 "sign" 关键字
  3. 找到函数: Java_com_yuanrenxue_challenge_three_ChallengeThreeNativeLib_sign
  4. 记录函数偏移地址: 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)

内部流程:

  1. 根据方法签名 sign(I)[B 查找 native 方法
  2. 构造 JNI 调用栈(JNIEnv*, jobject, 参数...)
  3. 跳转到 0x21294 执行 native 代码
  4. 模拟执行所有指令(包括系统调用、JNI 回调等)
  5. 捕获返回值(jbyteArray 引用)
  6. 转换为 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)

逆向分析流程

  1. 静态分析 (IDA Pro): 找到函数偏移、分析参数和返回值
  2. 动态分析 (Frida): 捕获真实调用,查看输入输出
  3. 模拟执行 (Unidbg): 在本地复现函数调用
  4. 结果验证: 对比 Unidbg 结果与真实 APP 结果
  5. 生产部署: 提供 HTTP 服务供业务调用

Unidbg 核心步骤

  1. 创建 Android 模拟器(指定架构: 32 位或 64 位)
  2. 创建 Dalvik VM 并注册 JNI 回调
  3. 注册 Java 类(必须在加载 SO 之前)
  4. 加载 SO 文件
  5. 调用 JNI_OnLoad(如果需要)
  6. 调用目标方法
  7. 根据运行时错误实现缺失的 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));
}
// 寻找规律...

扩展阅读

进阶主题

  1. SO 加固破解: 处理 VMP、Ollvm 等加固的 SO
  2. 反调试对抗: 绕过 SO 中的反调试检测
  3. 算法还原: 通过汇编代码还原加密算法
  4. 协议分析: 结合 Wireshark 分析网络协议
  5. 自动化逆向: 使用 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

学习资源


附录

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()

结语

通过本文档,你应该已经掌握了:

  1. Android 和 JNI 的基础知识 - 理解 Java 层和 Native 层的关系
  2. 逆向分析的基本流程 - 静态分析 + 动态分析 + 模拟执行
  3. Frida 和 Unidbg 的使用 - 实战工具的应用
  4. JNI 参数和返回值处理 - 深入理解 JNIEnv、jobject、方法签名等概念
  5. 实际项目的开发经验 - 从分析到部署的完整流程

逆向工程需要大量实践,建议:

  • 多分析不同的 APP 和 SO 文件
  • 深入学习汇编语言(ARM/x86)
  • 理解常见的加密算法和反调试技术
  • 参与 CTF 竞赛和技术社区

重要提醒:

  • 逆向分析应当遵守法律法规
  • 不得用于非法破解和侵权行为
  • 仅用于学习研究和安全测试

祝学习愉快! 🚀

感谢您的支持
微信赞赏

微信扫一扫



评论(已关闭)

评论已关闭