技术文档
Android Native ELF 加固推荐方案(JNI .so)
Android APK / AAB 内 JNI .so 的符号处理、ABI 与 16 KB 对齐、重打包重签名、JNI 加载验证与测试矩阵。
最后更新于 2025-06-06预计阅读 7 分钟
1. 适用范围与定位
本文针对 Android APK / AAB 中的 native 库 和 Android Native ELF 可执行文件,即:
lib/arm64-v8a/libxxx.so
lib/armeabi-v7a/libxxx.so
lib/x86_64/libxxx.so
典型来源:
- C / C++ 核心算法、音视频处理、图像处理
- 游戏引擎(Unity IL2CPP、Unreal、Cocos)
- 加密、风控、反作弊 SDK
- 第三方
.so(FFmpeg、OpenSSL、SQLCipher 等)
不适用于:
- Dex / 资源 / 整体 APK 加固:见 Android 应用加固推荐方案(APK / AAB)
- Linux 服务端
.so:见 Linux ELF 加固推荐方案 - 纯 Java / Kotlin 逻辑
当前客户端的 Native ELF 能力可用于评估
.so保护,但 Android 的加载器、包结构、对齐和签名有额外约束,必须重新打包、重签名并在真机验证。
2. 与整体 Android 加固的关系
Android 应用通常有两层保护:
| 层 | 目标 | 方案 |
|---|---|---|
| Java / Kotlin 层 | Dex 反编译、改包、注入 | Dex 加密、反调试、反注入、Root / 模拟器检测 |
| Native 层 | .so 逆向、Hook、算法还原 | 符号移除、函数混淆、函数虚拟化、完整性校验 |
两层应组合使用,但不要重复处理同一份代码。只保护 .so 时,攻击者仍可修改 Java 层调用逻辑;只保护 Dex 时,核心算法仍可从 .so 中还原。
3. 保护选项与档位
3.1 可选能力
| 能力 | 作用 | 主要代价 | 建议 |
|---|---|---|---|
| 移除符号 | 删除导出符号与部分名称信息 | 崩溃栈可读性下降 | 发布版开 |
| 函数混淆 | 打乱控制流 | 单函数小幅开销 | 核心函数开 |
| 函数虚拟化 | 转换为自定义虚拟指令 | 被虚拟化的函数慢数倍到数十倍 | 少量核心函数 |
| 导入保护 | 保护动态链接符号 | 与 dlopen / dlsym 冲突 | 视场景 |
| 压缩保护 | 压缩 native 库 | 启动解压 / 校验变慢,包越大越慢,可能影响 APK 对齐与加载 | 谨慎评估 |
| 内存校验 | 检测运行时修改 | 少量运行时开销 | 视场景 |
| 反调试 | 检测调试器 / 注入 | 与合法调试、部分设备冲突 | 发布版开 |
性能提醒
- 函数虚拟化会让被选中的
.so函数运行速度慢数倍到数十倍;游戏每帧逻辑、音视频回调、高频 JNI 调用不要虚拟化。- 压缩保护与文件校验会增加 App 冷启动时间,
.so/ APK 越大越慢;extractNativeLibs=false时还会影响加载与对齐。- 先用原始包测冷启动、首帧、
JNI_OnLoad初始化耗时,再决定保护范围;大体积游戏 / 音视频应用重点测低端机。
extractNativeLibs=false的 APK 会把.so以未压缩、页对齐方式存放。修改.so后必须重新打包并正确对齐,否则可能出现dlopen失败或安装后无法加载。
3.2 基础档:兼容性优先
| 选项 | 设置 |
|---|---|
| 移除符号 | 开 |
| 函数保护 | 5–10 个核心函数混淆 |
| 函数虚拟化 | 关 |
| 导入保护 | 关 |
| 压缩保护 | 关 |
目标:先确认 .so 替换与重打包流程稳定,再增加强度。
3.3 标准档:商业发布
| 选项 | 设置 |
|---|---|
| 移除符号 | 开 |
| 函数保护 | 授权、协议签名、核心算法虚拟化;业务逻辑混淆 |
| 函数虚拟化 | 5–15 个核心函数 |
| 导入保护 | 开(验证 JNI 与插件加载) |
| 压缩保护 | 视 APK 结构决定 |
3.4 强化档:高价值目标
| 选项 | 设置 |
|---|---|
| 移除符号 | 开 |
| 函数保护 | 10–30 个核心函数虚拟化,其余敏感函数混淆 |
| 反调试 / 内存校验 | 开,异常环境降级 |
| 完整性校验 | .so 哈希 + 服务端挑战 |
| 应用层 | Dex 加密 + 反注入 + Root / 模拟器检测 |
4. JNI 符号与加载机制
4.1 必须保留的符号
JNI_OnLoad:向 VM 注册 native 方法。Java_<包名>_<类名>_<方法名>:静态注册的 JNI 方法。- 通过
RegisterNatives动态注册时,被注册的函数本身可以隐藏,但注册逻辑必须保留。 - 对外暴露的 C API(供其他
.so链接)需要保留导出。
建议在编译期使用:
__attribute__((visibility("hidden")))
只导出必要符号,减少攻击面,也减少保护后符号解析失败的概率。
4.2 加载方式
System.loadLibrary("xxx"):由 Android 运行时按 ABI 选择libxxx.so。System.load("/absolute/path/libxxx.so"):验证绝对路径与权限。dlopen/dlsym:验证符号名与命名空间限制。- Android 7+ 对私有平台库的链接有限制,避免依赖非 NDK 公开库。
- 对插件化 / 动态加载框架,验证
.so释放路径与 SELinux 权限。
4.3 常见崩溃点
JNI_OnLoad被虚拟化或混淆后注册失败。- 导出符号被 strip 掉,导致
UnsatisfiedLinkError。 .so与 ABI 目录不匹配,64 位设备加载了 32 位库。- 依赖
.so的加载顺序变化,导致符号未定义。 - 16 KB 页大小设备上段对齐不正确。
5. 打包、ABI 与重签名
5.1 处理流程
构建 APK / AAB
↓
解包,取出 lib/<abi>/*.so
↓
按 Native ELF 方案保护
↓
替换回原目录(保持文件名与 ABI)
↓
重新打包
↓
zipalign
↓
apksigner(原证书)
↓
真机回归
5.2 ABI 覆盖
| ABI | 说明 |
|---|---|
arm64-v8a | 现代设备必测,Google Play 要求 64 位 |
armeabi-v7a | 仍支持 32 位设备时保留 |
x86_64 | 模拟器、Chromebook、部分平板 |
- 每个 ABI 目录都要处理,不能只替换
arm64-v8a。 - 使用
unzip -l app.apk | grep lib/确认目录结构。 - 使用
file libxxx.so或readelf -h确认架构与保护前一致。
5.3 16 KB 页大小
面向 Android 15+ 与 16 KB 页大小设备时:
- 确认
.so的 LOAD 段对齐满足 16 KB 要求。 - 使用 Android 官方工具检查 ELF 对齐。
- 保护后重新打包,不要直接复用旧的未对齐产物。
- 在支持 16 KB 页大小的真机或模拟器上验证加载。
5.4 重签名
zipalign -p -f 4 app-protected-unsigned.apk app-protected-aligned.apk
apksigner sign \
--ks release.jks \
--ks-key-alias release \
--out app-protected.apk \
app-protected-aligned.apk
apksigner verify --print-certs app-protected.apk
- 必须使用原签名证书,否则无法覆盖安装。
- AAB 不能直接安装,使用
bundletool生成 APK 后测试。 - Google Play 使用上传密钥重新签名,确认加固流程不影响上传签名。
6. 构建流水线
Gradle assembleRelease / bundleRelease
↓
原始包冒烟测试
↓
提取并保护 .so(所有 ABI)
↓
替换、重新打包
↓
zipalign + apksigner
↓
真机回归 + 性能测试
↓
分发 / 上架
建议:
- 把
.so保护放在签名之前,Dex 保护之后或之前均可,但要避免重复处理。 - 每次构建记录输入 / 输出
.so的 SHA-256 和 ABI。 - 保留未 strip 的
.so与符号文件,用于 tombstone 还原。 - 对游戏 / 音视频应用,重点测试帧率、延迟和内存。
7. 验证清单
- 原始 APK / AAB 可正常安装与启动
- 所有 ABI 的
.so均已处理 -
JNI_OnLoad、静态 / 动态注册方法正常 -
System.loadLibrary、dlopen、dlsym正常 - 依赖
.so加载顺序正常 - 覆盖安装、升级安装正常
- 签名校验通过,可上架 Google Play
- 16 KB 页大小设备加载正常
- 崩溃 tombstone 可通过未 strip 符号还原
- 最大体积
.so/ APK 的冷启动已单独测试 - 被虚拟化函数的帧率 / 延迟已对比基线
- 冷启动、核心功能、帧率 / 延迟符合预期
8. 常见问题
8.1 启动时报 UnsatisfiedLinkError
- 检查
.so是否放入了正确的 ABI 目录。 - 检查
JNI_OnLoad与Java_*导出符号是否被移除。 - 检查依赖库是否缺失或加载顺序错误。
- 检查 APK 是否重新对齐、签名是否有效。
8.2 保护后 dlopen 失败
- 检查
dlopen的路径、命名空间和 SELinux 权限。 - 检查导入符号是否被导入保护破坏。
- 用
readelf -d对比保护前后的NEEDED与RPATH。 - 在
adb logcat中查看 linker 的具体错误。
8.3 只有部分设备崩溃
- 优先检查 ABI 与 CPU 架构。
- 检查 16 KB 页大小设备。
- 检查厂商定制系统的加载限制与安全策略。
- 收集
tombstone并用未 strip 的.so还原。
8.4 游戏 / 音视频性能下降
- 减少虚拟化函数数量,尤其是每帧调用的函数。
- 改用混淆,或只保护非热路径。
- 对引擎核心循环单独评估,不要全量虚拟化。
- 在低端真机上测试,不只看旗舰机。
8.5 能否只保护 .so,不改 Dex
可以,但保护不完整。Java / Kotlin 层仍可被修改,建议同时使用 Dex 加密、反调试和反注入。整体方案见 Android 应用加固推荐方案(APK / AAB)。
8.6 AAB 如何测试
使用 bundletool build-apks 生成 APKS,再 install-apks 到设备。不要直接安装 .aab。
9. 相关文档
这份文档有帮助吗?欢迎反馈文档问题或补充场景。提交文档反馈