Skip to main content

re-android-native

Android 原生库 JNI 逆向:so 提取、JNI 注册还原、Native 逻辑分析。 触发词:JNI、.so、Android native、xhook、IDA so

来源信息

仓库
dslsdzc/rev-skills
最近来源活动
2026年10月5日 09:35
检测到的 SKILL.md 语言
中文
星标
125
分支
18

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
3 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
re-android-native
description
Android 原生库 JNI 逆向:so 提取、JNI 注册还原、Native 逻辑分析。 触发词:JNI、.so、Android native、xhook、IDA so
capabilities
["jni-analysis"]
# 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,或命令行: ```sh 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]]),供报告引用。 1. **so 提取与架构识别**: ```sh 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 -d` NEEDED)、是否 strip(`readelf -s` 符号数量) 2. **JNI 接口还原(JNIEnv 函数表)**: ```sh 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(实例),业务参数从第三个起 3. **注册方式(静态 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` 位于标准 `JNIEnv` function table index **215**(`UnregisterNatives` 216)——这是 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 函数地址` 对照表 4. **逻辑分析([[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 思路) 5. **与 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 层 ## 跨域联合 - [[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]]。
在 GitHub 查看