executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
re-android-nativeAndroid 原生库 JNI 逆向:so 提取、JNI 注册还原、Native 逻辑分析。 触发词:JNI、.so、Android native、xhook、IDA so
Independent GitHub snapshot · Automated classification · Original documentation governs
目标出现时先判形态,命中即转对应技能(自动调度分流):
目标
├── 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
lib/*.so 原生库——JNI 接口还原、注册方式(静态/动态)、native 逻辑分析System.loadLibrary + native 声明,要定位与还原 native 实现静态分析(readelf / Ghidra 导入 .so)免沙箱;动态(frida / xhook)按 [[re-analyze/platform-tips]] 最高原则在受控环境执行。所有工具先验证再使用。
File > Import File 直接导入 ELF .so(arm64/arm/x86_64 都支持),Data Type Manager 载入 jni.h 类型(见 [[re-ghidra]] 工具准备)Option > Load new type 载入 jni.hJNI_OnLoad 或 Java_* 导出函数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 路径)JNINativeInterface(函数表)与 JNIEnv(函数表指针)定义apt install binutils / dnf install binutils / pacman -S binutilsbrew install binutils(greadelf)readelf --versionpip install frida-tools;设备 frida-server 推送安装见 [[re-frida]] 工具准备frida-ps -U 能列出设备进程apt install adb(Debian/Ubuntu)/ dnf install android-tools / pacman -S android-toolsbrew install android-platform-tools;Windows: 官方 platform-tools zipadb 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 # 依赖库与动态段
arm64-v8a(真机主力)、armeabi-v7a(32 位)、x86_64/x86(模拟器)——多 ABI 差异见坑 4adb pull /data/app/.../lib/arm64-v8a /tmp/lib/(需 root 或 debuggable)readelf -d NEEDED)、是否 strip(readelf -s 符号数量)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* env 实际指向 JNINativeInterface 函数表(jni.h 定义),函数调用是 (*env)->GetStringUTFChars(env, ...) 形式的表槽访问——反编译里看到"从结构体偏移取函数指针再调用"就是 JNI API,见坑 1GetStringUTFChars(Java jstring → Modified UTF-8 字节序列)、NewStringUTF(Modified UTF-8 → Java String)、CallVoidMethod/CallBooleanMethod(回调 Java)、GetJavaVM(进程内拿 JavaVM)0xC0 0x80)与增补字符(代理对)上不同——按标准 UTF-8 解析会得到错误字符串,按字节数做的 strlen 类操作也会对不上(字符数要用 JNI 自己的长度接口拿)(JNIEnv*, jclass/jobject, 业务参数...))——第一个参数是 env、第二个是 jclass(静态)或 jobject(实例),业务参数从第三个起注册方式(静态 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 不改变该 indexJNIEnv* 有效性 / hook 表检测 / ABI 传参分支)见 [[probes]]——易变的是 ABI 传参寄存器与 libart 内部结构,以目标环境实测为准
Java 方法名 → 签名 → native 函数地址 对照表逻辑分析([[re-ghidra]] 联动):
GetStringUTFChars 的返回值是输入字符串)Java_* 函数之间常有跨调用(native 内部函数表/回调),追踪函数指针来源([[re-binary-core]] R6 思路)与 Java 层交互(native 调用点定位):
System.loadLibrary("target") 与 native 方法声明所在的类——Java_包名_类名_* 命名即从这里来native 方法找调用点(谁在什么业务路径上触发它),与步骤 3 的对照表对齐,形成 Java 调用点 ↔ native 函数 映射Java.perform 里直接调用 native 方法(Java.use("com.x.Cls").method(...))观察参数与返回;或用 xhook / PLT hook 思路([[re-frida]] 的 Interceptor.attach(Process.getModuleByName(...).findExportByName(...)))观察 native 内部对外部库(libc / 系统库)的调用链.so 目标后固定调度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 脚本)直接拿运行时注册表file/readelf -h 先确认目标 ABI,分析以真机 ABI(arm64-v8a)为准,x86_64 结果仅参考;JNI 类型宽度跨 ABI 一致(jlong=64 位、jint=32 位)但 C 层 long 宽度不同,注意反编译里的类型标注JNI_OnLoad 反编译是脱壳流程;原因——so 被加固(厂商加壳 / 商用壳),真实逻辑运行时才解密到内存;对策——先识别加固([[re-apk]] 加固识别 + 熵值),静态脱壳按 [[re-mobile-pack]],或运行时 [[re-memdump]] 提内存中已解密的 so 再分析;脱壳产物 sha256 存档后回到步骤 1 复跑实战经验全集见 [[experience]]。