2025-05-01
先把边界讲清楚:这篇只面向自有 App、授权审计、兼容性排查、恶意样本研究和安全学习。不讨论越权分析、商业保护规避、账号滥用、接口攻击,也不提供绕开线上校验的做法。
Android 逆向里,jadx 和 unidbg 经常被放在一起提。一个偏静态,一个偏 native 模拟。jadx 帮你把 APK、DEX 里的逻辑摊开,看到 Java/Kotlin 层的调用路径;unidbg 则把 .so 里的 JNI 调用搬到本地实验台,方便你喂参数、看返回、补环境。
用得顺的话,它们不是“神奇破解器”,而是一套分析流程:
jadx 先画地图
定位 Java 到 native 的入口
unidbg 搭实验台
补齐必要的 JNI 环境
记录输入、输出、依赖和结论
jadx 的官方定位很直接:把 Android Dex 和 Apk 文件反编译成 Java 源码视图,并提供命令行和 GUI 工具。它还可以解码 AndroidManifest.xml 和资源文件,GUI 里支持跳转定义、查找引用、全文搜索等功能。
常用入口很简单:
jadx-gui sample.apk
jadx -d out sample.apk
jadx --deobf -d out_deobf sample.apk
GUI 适合读逻辑,命令行适合导出和沉淀分析材料。刚打开一个样本,不要急着搜某个函数,先看整体轮廓:
包名和入口 Activity
声明的权限
关键 service、receiver、provider
网络、存储、加密、签名相关类
System.loadLibrary 调用了哪些 so
native 方法声明在哪里
看 APK 时,很多线索藏在固定关键词附近。
System.loadLibrary
native
JNI_OnLoad
RegisterNatives
sign
encrypt
decrypt
token
hash
Base64
Cipher
MessageDigest
这些词不是让你直接下结论,而是帮你定位“数据从哪里来、经过哪里、到哪里去”。比如一个请求签名参数,不一定在 Java 层完整生成;Java 可能只负责收集字段,核心拼接和摘要在 .so 里。
看 native 入口时,重点不是只找方法名,而是把上下游一起记下来:
哪个按钮、接口或任务触发
传入了哪些字段
返回值去了哪里
异常怎么处理
是否依赖设备信息、配置、缓存
这一步做细,后面搭 unidbg 才不会迷路。
一种是按 JNI 命名规则导出:
public class SignHelper {
static {
System.loadLibrary("secure");
}
public static native String sign(String body);
}
对应 native 侧可能有类似符号:
Java_com_demo_SignHelper_sign
另一种是 RegisterNatives 动态注册。jadx 里只看到 native 声明,不一定能直接从名字对应到 .so 符号。此时要结合导出符号、字符串、JNI_OnLoad 逻辑和调用栈去还原关系。
对于授权审计来说,静态阶段要产出一张表:
Java 类名
native 方法签名
所在 so
疑似 native 函数
输入参数
返回类型
上游调用点
这张表就是 unidbg 实验的路线图。
unidbg 官方仓库描述里强调,它可以模拟 Android native library,并支持 JNI Invocation API,让 JNI_OnLoad 可以被调用。它不是完整 Android 系统,而是一个让 native 库在本地可控环境里跑起来的分析框架。
典型目标不是“跑完整 App”,而是让一个 native 函数在本地被调用:
加载 APK 或 so
创建 Android 模拟环境
调用 JNI_OnLoad
resolve 目标 Java 类
构造参数
调用 native 方法
观察返回值和异常
一个简化骨架如下:
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class SignLab extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;
private final DvmClass signClass;
public SignLab() {
emulator = AndroidEmulatorBuilder
.for64Bit()
.setProcessName("com.demo.app")
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("sample.apk"));
vm.setJni(this);
vm.setVerbose(false);
DalvikModule module = vm.loadLibrary(new File("libsecure.so"), true);
module.callJNI_OnLoad(emulator);
signClass = vm.resolveClass("com/demo/SignHelper");
}
public String sign(String body) {
DvmObject<?> result = signClass.callStaticJniMethodObject(
emulator,
"sign(Ljava/lang/String;)Ljava/lang/String;",
new StringObject(vm, body)
);
return result == null ? null : result.getValue().toString();
}
public void close() {
emulator.close();
}
public static void main(String[] args) {
SignLab lab = new SignLab();
System.out.println(lab.sign("amount=100&uid=42"));
lab.close();
}
}
这段代码只是骨架,真实样本通常还要补系统方法、上下文对象、设备字段、文件读取、SharedPreferences、Base64、日志等环境。
unidbg 里常见报错不是坏事,它是在告诉你 native 代码缺了什么环境。比较稳的处理方式是:
先看报错调用点
确认 native 侧为什么需要它
只补目标函数确实依赖的最小行为
给每个 stub 写注释
记录这个返回值为什么这样设
比如 native 层调用了 Context.getPackageName(),你可以在 AbstractJni 里补一个稳定值:
@Override
public DvmObject<?> callObjectMethodV(
VM vm,
DvmObject<?> dvmObject,
String signature,
VaList vaList
) {
if ("android/content/Context->getPackageName()Ljava/lang/String;".equals(signature)) {
return new StringObject(vm, "com.demo.app");
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
注意:stub 不是随便编故事。你补的每个返回值都会影响结果。越是签名、加密、设备绑定这类逻辑,越要把输入来源记录清楚。
实际分析里,流程通常是往返的。
先用 jadx 看 Java 层:
请求参数怎么组装
native 方法在哪里调用
返回值怎么放进 header 或 body
异常后是否有降级逻辑
再用 unidbg 试 native:
相同输入是否得到预期格式
缺哪些 Java 环境
是否读取文件或系统属性
返回值是否稳定
遇到缺参数,就回 jadx 找上游;遇到 native 调用异常,就回 SO 符号和字符串里找线索。不要把 unidbg 当黑盒调用器,它更像可控实验台。
逆向分析最怕“跑通一次就结束”。你要留下可复盘材料:
样本来源和授权说明
APK 摘要
jadx 导出目录
关键类和方法表
native 方法映射表
unidbg 工程路径
补过的 JNI stub
测试输入和输出
无法确认的假设
这样做有两个好处:一是别人能复核你的结论;二是样本更新后,你能快速对比哪里变了。