Android 原生库(JNI/.so)逆向

SkillDev tools

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

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Android 原生库(JNI/.so)逆向 skill

What this skill tells your AI

The instructions your AI receives, as published by dslsdzc/rev-skills in .claude/skills/re-android-native/SKILL.md and read by ahel’s review.

入口判定(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_OnLoadJava_* 导出函数

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 binutilsgreadelf
  • 验证: 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 提取与架构识别

    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 函数表)

    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 类型后,按名称还原:GetStringUTFCharsJava jstring → Modified UTF-8 字节序列)、NewStringUTFModified 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)methodsJNINativeMethod{name, signature, fnPtr} 数组——三个字段都是指针,宽度随 ABI 变(64 位下各 8 字节;armeabi-v7a / x86 等 32 位 ABI 下各 4 字节,数组步长 12 字节)——函数地址不在导出表(见坑 2),反编译定位 JNI_OnLoad 后沿 RegisterNatives 第三参数数组逐项还原
    • 机制要点(知识层):JNIEnv* 指向 JNINativeInterface 函数表(见坑 1),RegisterNatives 是表中一个槽——槽号是易变参数(随 jni.h 声明序/NDK/ART 版本变化),不在核心流程硬编码
    • frida 观察运行时注册(spawn 目标 App):脚本模板与槽位探测策略(锚点定位 / runtime 校验 / ABI 分支)见 [[probes]]——易变数值一律以运行时探测为准
    • 还原产物: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]]。

Signals

GitHub stars
57
Forks
8
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
re-android-native
Source
github.com/dslsdzc/rev-skills