13 KiB
13 KiB
name, description, capabilities
| name | description | capabilities | |
|---|---|---|---|
| re-android-native | Android 原生库 JNI 逆向:so 提取、JNI 注册还原、Native 逻辑分析。 触发词:JNI、.so、Android native、xhook、IDA so |
|
Android 原生库(JNI/.so)逆向
入口判定(Decision Gate)
目标出现时先判形态,命中即转对应技能(自动调度分流):
目标
├── APK
│ ├── lib/*.so 存在?──是──→ 本技能(re-android-native)
│ └── 无 so(纯 Java)──→ re-apk
├── 单独 .so
│ ├── JNI 符号(Java_* / JNI_OnLoad / RegisterNatives 调用点)──→ 本技能
│ ├── 加密库 API(SSL_*/EVP_*/crypto_* 等)──→ re-android-crypto(加密语义)
│ └── 普通 ELF(无 JNI 特征)──→ re-binary-core(re-format-elf)
├── 加固 so(壳壳/熵高/导入表极小)──→ re-anti-analysis / re-mobile-pack(脱壳后回本技能)
└── 加密体系审计目标(Keystore/Cipher)──→ re-android-crypto
何时使用 / 何时不用
- 用:APK 里的
lib/*.so原生库——JNI 接口还原、注册方式(静态/动态)、native 逻辑分析 - 用:Java 层逻辑清楚但核心算法/反调试在 native(游戏、加固、加密逻辑)
- 用:Java 层只看到
System.loadLibrary+native声明,要定位与还原 native 实现 - 不用:只要 APK Java 层静态(走 re-apk);只要运行时 hook 不需要理解 .so 结构(走 re-frida)
- 不用:so 是加固壳壳(先按 re-mobile-pack / re-anti-analysis 脱壳,见坑 5)
- 注意:动态插桩按 re-analyze/platform-tips 最高原则在受控设备 / 模拟器快照内执行
工具准备
静态分析(readelf / Ghidra 导入 .so)免沙箱;动态(frida / xhook)按 re-analyze/platform-tips 最高原则在受控环境执行。所有工具先验证再使用。
反编译工作台(so 加载)—— re-ghidra / re-ida
- re-ghidra(默认):
File > Import File直接导入 ELF .so(arm64/arm/x86_64 都支持),Data Type Manager 载入 jni.h 类型(见 re-ghidra 工具准备) - re-ida:加载 so 后 ARM64 反编译;
Option > Load new type载入 jni.h - 验证: 导入 arm64 .so 后能反编译出
JNI_OnLoad或Java_*导出函数
jni.h —— JNIEnv 函数表结构理解(Android NDK 自带)
- 无独立发行版包,随 Android NDK 提供。官方途径:Android Studio SDK Manager 安装 NDK,或命令行:
sdkmanager "ndk;27.2.12479018" # 先确认 sdkmanager 在 PATH(Android SDK cmdline-tools) # 或 GitHub android-ndk 官方仓库下载对应版本 zip - 关键文件:
$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/jni.h(macOS 为 darwin-x86_64 路径) - 验证: 能打开 jni.h,看到
JNINativeInterface(函数表)与JNIEnv(函数表指针)定义
readelf(binutils)—— 导出表/架构识别(re-format-elf 联动)
- Linux:
apt install binutils/dnf install binutils/pacman -S binutils - macOS:
brew install binutils(greadelf) - 验证:
readelf --version
frida(re-frida)—— JNI 运行时观察
- 主机
pip install frida-tools;设备 frida-server 推送安装见 re-frida 工具准备 - 验证:
frida-ps -U能列出设备进程
adb —— 从设备提取 so / 安装 APK
- Linux:
apt install adb(Debian/Ubuntu)/dnf install android-tools/pacman -S android-tools - macOS:
brew install android-platform-tools;Windows: 官方 platform-tools zip - 验证:
adb version
操作步骤
按顺序执行,每步产物(so 路径、readelf 输出、Ghidra 工程、frida 脚本)记录证据路径 + sha256(见 re-triage),供报告引用。
-
so 提取与架构识别:
unzip app.apk -d app/ # APK 是 zip find app -name "*.so" # lib/ 下所有原生库 file app/lib/arm64-v8a/libtarget.so # 确认架构(ELF 64-bit ARM aarch64) readelf -h app/lib/arm64-v8a/libtarget.so # Machine: AArch64 / x86-64 readelf -d app/lib/arm64-v8a/libtarget.so # 依赖库与动态段- ABI 目录:
arm64-v8a(真机主力)、armeabi-v7a(32 位)、x86_64/x86(模拟器)——多 ABI 差异见坑 4 - 已安装 App:
adb pull /data/app/.../lib/arm64-v8a /tmp/lib/(需 root 或 debuggable) - 记录:目标 ABI、导出表规模、依赖(
readelf -dNEEDED)、是否 strip(readelf -s符号数量)
- ABI 目录:
-
JNI 接口还原(JNIEnv 函数表):
readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i java # 静态注册的 Java_* 导出 readelf --dyn-syms app/lib/arm64-v8a/libtarget.so | grep -i JNI # JNI_OnLoad / JNI_OnUnload- JNIEnv 是函数表指针:
JNIEnv* env实际指向JNINativeInterface函数表(jni.h 定义),函数调用是(*env)->GetStringUTFChars(env, ...)形式的表槽访问——反编译里看到"从结构体偏移取函数指针再调用"就是 JNI API,见坑 1 - Ghidra/IDA 载入 jni.h 类型后,按名称还原:
GetStringUTFChars(Java jstring → Modified UTF-8 字节序列)、NewStringUTF(Modified UTF-8 → Java String)、CallVoidMethod/CallBooleanMethod(回调 Java)、GetJavaVM(进程内拿 JavaVM) - 是 Modified UTF-8,不是标准 UTF-8:两者在空字符编码(
0xC0 0x80)与增补字符(代理对)上不同——按标准 UTF-8 解析会得到错误字符串,按字节数做的strlen类操作也会对不上(字符数要用 JNI 自己的长度接口拿) - 还原目标:native 函数签名(
(JNIEnv*, jclass/jobject, 业务参数...))——第一个参数是 env、第二个是 jclass(静态)或 jobject(实例),业务参数从第三个起
- JNIEnv 是函数表指针:
-
注册方式(静态 JNI_OnLoad / 动态 RegisterNatives):
- 静态注册:函数名
Java_包名_类名_方法名(下划线转义),直接出现在导出表(步骤 2 可看到) - 动态注册:
JNI_OnLoad里调RegisterNatives(env, clazz, methods, count),methods是JNINativeMethod{name, signature, fnPtr}数组——三个字段都是指针,宽度随 ABI 变(64 位下各 8 字节;armeabi-v7a / x86 等 32 位 ABI 下各 4 字节,数组步长 12 字节)——函数地址不在导出表(见坑 2),反编译定位JNI_OnLoad后沿 RegisterNatives 第三参数数组逐项还原 - 机制要点(知识层):
JNIEnv*指向JNINativeInterface函数表(见坑 1),RegisterNatives位于标准JNIEnvfunction table index 215(UnregisterNatives216)——这是 JNI ABI 的固定布局;实际 byte offset = index × 目标指针宽度(64 位下 215×8=0x6B8)。Android Dalvik/ART、NDK 与标准jni.h不改变该 index - frida 观察运行时注册(spawn 目标 App):脚本模板与运行时校验策略(
JNIEnv*有效性 / hook 表检测 / ABI 传参分支)见 probes——易变的是 ABI 传参寄存器与 libart 内部结构,以目标环境实测为准 - 还原产物:
Java 方法名 → 签名 → native 函数地址对照表
- 静态注册:函数名
-
逻辑分析(re-ghidra 联动):
- Ghidra 导入 so → 载入 jni.h 类型(步骤 2)→ 对每个 native 函数 F5 反编译
- 无符号辅助时按 API 调用点反推:JNI API 的参数就是业务数据的入口(如
GetStringUTFChars的返回值是输入字符串) - 算法/校验/解密逻辑按 re-binary-core 方法论深挖;被混淆(OLLVM 变脸/字符串加密)→ 转 re-deobfuscate(坑 3)
- 静态注册的
Java_*函数之间常有跨调用(native 内部函数表/回调),追踪函数指针来源(re-binary-core R6 思路)
-
与 Java 层交互(native 调用点定位):
- Java 侧(re-apk / jadx):
System.loadLibrary("target")与native方法声明所在的类——Java_包名_类名_*命名即从这里来 - jadx 里对每个
native方法找调用点(谁在什么业务路径上触发它),与步骤 3 的对照表对齐,形成Java 调用点 ↔ native 函数映射 - 动态验证:frida
Java.perform里直接调用 native 方法(Java.use("com.x.Cls").method(...))观察参数与返回;或用 xhook / PLT hook 思路(re-frida 的Interceptor.attach(Process.getModuleByName(...).findExportByName(...)))观察 native 内部对外部库(libc / 系统库)的调用链 - 闭环:Java 触发点 → 参数来源 → native 处理逻辑 → 输出回 Java 层
- Java 侧(re-apk / jadx):
跨域联合
- re-mobile:本技能是其工作流第 4 步(原生库)的专项子技能——网关识别到含
.so目标后固定调度 - re-frida:动态观察 RegisterNatives / hook native 函数 / Java↔native 交互验证(spawn 抓 JNI_OnLoad)
- re-format-elf:导出表 / 动态段 / 符号解析(readelf 三表),动态注册函数定位的前提
- re-ghidra:so 反编译工作台 + jni.h 类型载入
- re-apk:Java 侧静态(jadx 找 native 声明与调用点);加固识别后转脱壳域
- re-deobfuscate:OLLVM / 字符串加密的 native 代码还原
- re-mobile-pack / re-anti-analysis:加固 so 壳壳先脱壳再分析(坑 5)
- re-android-crypto:加密体系审计(Keystore/Cipher/第三方加密库语义)已独立承接——本技能聚焦 JNI/native 逻辑;.so 内加密库 API 的加密语义转 re-android-crypto
- re-analyze:被 triage「移动 App 分析」路径调用(re-mobile → 原生库 → 本技能)
- re-analyze/platform-tips:动态插桩受控环境最高原则
常见坑与陷阱
- JNIEnv 是函数表指针(不是直接调用):现象——反编译里 JNI 函数调用点看起来像"从结构体偏移取出函数指针再调用",参数对不上,或按普通函数分析
GetStringUTFChars直接当字符串函数用错;原因——JNIEnv指向JNINativeInterface函数表,所有 JNI API 都是表槽中的函数指针,C 写法(*env)->fn(env, ...);对策——Ghidra/IDA 载入 jni.h 类型(Data Type Manager 导入),JNIEnv声明为JNINativeInterface**,反编译自动还原成env->GetStringUTFChars(env, str)形式;没有类型库时手工按JNINativeInterface槽位索引建结构体 - 动态注册函数地址不在导出表:现象——
readelf --dyn-syms/ strings 里找不到Java_*或业务函数名,IDA 里全是地址没名字;原因——动态注册时函数是静态/局部符号,运行时才由RegisterNatives把地址与 Java 方法绑定;对策——JNI_OnLoad是可选的库加载钩子、非必有导出:存在则反编译看RegisterNatives第三参数数组逐项还原(name/signature/fnPtr 各为一个指针:64 位 8 字节、32 位 4 字节);不存在(或导出表无它)时从RegisterNatives的其他调用点、JNI 函数表 xref、字符串定位,或 frida spawn 后 hook RegisterNatives(步骤 3 脚本)直接拿运行时注册表 - 混淆 native(OLLVM):现象——反编译全是控制流平坦化(switch 调度器)、字符串全加密、函数巨大难读;原因——游戏/加固厂商用 OLLVM(变脸、bcf、sub)或商业混淆(VMP 类)保护 native 代码;对策——先确认混淆类型(平坦化 vs 指令虚拟化),平坦化按 re-deobfuscate 还原(状态变量 + 情况块),字符串加密定位解密函数后脚本批量解密;仍不行就 frida 动态拿运行时明文(hook 解密函数读内存)
- 多 ABI 架构差异:现象——按 arm64 分析的偏移/指令套到 armeabi-v7a 全错,或模拟器 x86_64 上行为与真机不同;原因——APK 每个 ABI 一份 so,编译优化/指令集/调用约定不同(arm64 用 x0-x7 传参,arm32 有 thumb 指令,x86_64 用 rdi/rsi...),部分 so 还会按 ABI 返回不同实现(如 arm64 真机 vs x86_64 模拟器分支);对策——
file/readelf -h先确认目标 ABI,分析以真机 ABI(arm64-v8a)为准,x86_64 结果仅参考;JNI 类型宽度跨 ABI 一致(jlong=64 位、jint=32 位)但 C 层long宽度不同,注意反编译里的类型标注 - 加固 so(壳壳):现象——静态分析 so 只见一小段 stub / 壳代码,
JNI_OnLoad反编译是脱壳流程;原因——so 被加固(厂商加壳 / 商用壳),真实逻辑运行时才解密到内存;对策——先识别加固(re-apk 加固识别 + 熵值),静态脱壳按 re-mobile-pack,或运行时 re-memdump 提内存中已解密的 so 再分析;脱壳产物 sha256 存档后回到步骤 1 复跑
经验库
实战经验全集见 experience。