reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
143
vendor-new/rev-skills/.claude/skills/re-android-native/SKILL.md
Normal file
143
vendor-new/rev-skills/.claude/skills/re-android-native/SKILL.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
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]]。
|
||||
@@ -0,0 +1,11 @@
|
||||
# re-android-native 经验全集
|
||||
|
||||
> 条目含来源与日期;入库前 grep 去重。格式: **标题**:现象——…;原因——…;对策——…
|
||||
|
||||
## 2026-09-01 · 会话复盘
|
||||
|
||||
**JNIEnv 表槽定位法(hook 抓明文/注册表)**:现象——想抓 JNI 层字符串明文或动态注册表,却不知从哪 hook;原因——JNIEnv 是函数表指针(`[env]` = JNINativeInterface 表),各 API 是表槽(偏移 = 槽号×8),关键槽: `NewStringUTF`=0x538(槽167)、`RegisterNatives`=0x6b8(槽215);对策——`memory.pointer([env]).getLong(0)` 拿表基址,读槽值得实现地址,code hook 该地址:NewStringUTF 命中时 x1=明文、LR=调用点;RegisterNatives 命中时读 methods 数组(名称/签名/地址三指针)。调用点静态定位: 全库搜 `ldr xN, [xN, #0x538]`。来源:Android 商业应用 native 层分析实战
|
||||
|
||||
**注册表拿不到时的裸调用匹配(重建动态注册函数表)**:现象——动态注册的 native 方法,注册表(方法名+签名+地址)加密存储、strings 搜不到,运行时也不注册(模拟器无 ART);原因——注册表本身被加密,注册依赖真实运行时;对策——绕开注册表: ① 全量扫 `.text` 找函数入口(`stp x29,x30` 序言)② 叶子筛选(函数体内无内部 bl)③ 裸调用 `callFunction(addr, jniEnv, 0)`(每候选独立模拟器防状态污染)④ 按返回特征匹配: 0/1=boolean、小值=long/枚举、指针=String/回调 ⑤ 全局读取特征聚类(adrp 目标页)分组。已验证: 158 个候选零崩溃,14 个 boolean、2 个 long、8 个指针函数确认。来源:Android 商业应用 native 层分析实战
|
||||
|
||||
**懒解密/延迟注册的模拟边界**:现象——模拟器里 JNI_OnLoad 全路径执行、注册表槽 hook 零命中,以为漏了逻辑;原因——商业应用把字符串解密/方法注册推迟到 Java 调用链触发(类首次使用),模拟器无 dex 类加载能力,调用链无法构造;对策——先静态验证"启动路径确实不注册"(执行路径追踪+槽 hook),再判断为运行时触发;字符串/注册表的最终获取走真机 frida,模拟器能摸到哪就到哪,不硬啃。来源:Android 商业应用 native 层分析实战
|
||||
@@ -0,0 +1,52 @@
|
||||
# JNI 运行探针(Runtime Probes)
|
||||
|
||||
易变参数层:ABI 传参寄存器 / libart 内部结构 / runtime 实现细节——**不写进 SKILL.md 核心流程**(NDK / ART 版本变化可能影响实现),以运行时探测与校验为准。`JNIEnv` 函数表的 index 布局属固定 ABI(`RegisterNatives`=215),不在此列。
|
||||
|
||||
## RegisterNatives 槽位定位
|
||||
|
||||
`RegisterNatives` 位于标准 `JNIEnv` function table index **215**(`UnregisterNatives` 216)——JNI ABI 的固定布局,JDK 8/11/17/27、GraalVM 21、Android NDK 均一致(新增 helper 只追加在尾部,不改动已有 index)。实际 byte offset = index × 目标指针宽度:64 位下 215×8=0x6B8,32 位下 215×4=0x35C。
|
||||
|
||||
### 运行时锚点(校验用)
|
||||
|
||||
index 固定不代表运行时实现一定符合规范——以下锚点用于**校验**而非推导:
|
||||
|
||||
- 从 `JNIEnv*` 取函数表基址,读 index 215 槽值;按签名形态 `(JNIEnv*, jclass, JNINativeMethod*, jint)` 打桩调用确认
|
||||
- 交叉校验其他固定槽:`NewStringUTF`=167、`GetJavaVM`=219(语义稳定、调用频繁,适合作有效性锚点)
|
||||
- 槽值异常(为空、指向非 libart/系统库范围、多槽指向同一地址)→ 疑 hook/代理函数表或非标准实现,先还原再继续
|
||||
|
||||
### 备选:ART 内部函数 hook(版本绑定)
|
||||
|
||||
`art::JNI::RegisterNatives`(libart.so 导出符号,debuggable 进程可见)——符号名跨版本稳定,内部结构随 ART 版本变化,仅作交叉验证,不写死偏移。
|
||||
|
||||
## Frida 脚本模板
|
||||
|
||||
```js
|
||||
// hook JNI_OnLoad 后按固定 index 定位 RegisterNatives
|
||||
var REGISTER_NATIVES_INDEX = 215; // JNI ABI 固定槽位
|
||||
Interceptor.attach(Module.findGlobalExportByName("JNI_OnLoad"), { // Frida 17+:全局符号静态查找
|
||||
onEnter: function () {
|
||||
var env = this.context.x0; // arm64: JNIEnv* 在 x0(ABI 相关,见下)
|
||||
var table = env.readPointer(); // env[0] = JNINativeInterface 函数表
|
||||
var RegisterNatives = table.add(REGISTER_NATIVES_INDEX * Process.pointerSize).readPointer();
|
||||
if (RegisterNatives.isNull()) { console.log("槽位为空——疑非标准实现或 hook 表"); return; }
|
||||
Interceptor.attach(RegisterNatives, { onEnter: function (a) {
|
||||
console.log("RegisterNatives:", a[1], a[2].readPointer()); // clazz + methods 数组
|
||||
}});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**ABI 注记(易变)**:JNIEnv* 传参寄存器随 ABI——arm64 x0、arm32 r0、x86_64 rdi——以 `this.context` 当前架构为准,不写死。
|
||||
|
||||
## Runtime 校验清单
|
||||
|
||||
hook 生效后逐项校验,防误 hook:
|
||||
|
||||
1. **参数形态**:hook 到的方法被调用且参数形态符合 `(JNIEnv*, jclass, JNINativeMethod*, jint)`;形态不符 → 疑非标准实现或调用点误判
|
||||
2. **函数表有效性**:用 `NewStringUTF`(167) / `GetJavaVM`(219) 等固定槽交叉验证表基址正确、无 hook/代理函数表
|
||||
3. **多 ABI**:每个 ABI(arm64-v8a / armeabi-v7a / x86_64)独立确认——index 一致,但传参寄存器与指针宽度不同(byte offset 随宽度变化),脚本按 `Process.arch` 分支
|
||||
|
||||
## 使用注意
|
||||
|
||||
- 函数表 index 是固定 ABI,勿再「按 jni.h 声明序重新推导」;真正易变的是 ABI 传参寄存器、libart 内部结构与 runtime 实现细节,以目标环境实测为准
|
||||
- 核心流程(SKILL.md 步骤 3)只描述机制与策略,易变数值一律在本文件维护
|
||||
Reference in New Issue
Block a user