Android Native 库发布前检查:导出符号、敏感字符串与运行时保护

发版前检查 Native 库,不能只看构建配置里有没有开启 strip 或混淆。真正交付出去的文件,是否带上了预期的处理,还要从最终包里确认。

一次检查也不宜只留下"通过"两个字。把文件版本、导出清单和运行验证结果保存下来,下次升级工具链、修改 JNI 接口或调整保护配置时,才有内容可以对照。

下面按实际产物、符号、字符串和运行验证整理一份检查顺序。

确认检查的是最终包内的库

从准备发布的包中提取 so,覆盖随包交付的各个 ABI,并记录包版本、库路径及文件哈希。

在安装了 GNU 工具的环境中,可以先执行:

bash 复制代码
# 记录当前文件,避免后续混淆样本
sha256sum libgame.so

# 查看 ELF 基本信息和节区
readelf -h libgame.so
readelf -S -W libgame.so

这一步先确认对象,不急着判断保护强度。开发目录里处理过的库,与最终进入包里的库,不能仅凭文件名认定一致。

核对符号裁剪和公共接口

继续检查符号:

bash 复制代码
readelf --syms -W libgame.so | c++filt
readelf --dyn-syms -W libgame.so | c++filt

需要分开看调试信息、普通符号表和动态符号表。执行过 strip,不代表所有业务名称都会消失。动态符号还包含导入项,应结合定义状态、绑定类型和可见性判断哪些是公共接口。

对于只在库内部使用的业务函数,检查它是否也被导出。名称修饰经 c++filt 处理后,仍可能显露类名、函数名和参数信息。

发现多余导出后,可以回到构建环节收敛接口。比如使用 RegisterNatives() 注册方法、没有其他导出需求的 JNI 库,可以用 version script 只保留 JNI_OnLoad。

但按名称查找的 JNI 方法、其他共享库需要调用的接口,仍要保留。导出减少后,必须回归 JNI 注册、实际调用和跨库依赖,不能只比较符号数量。

检查日志是否透露业务位置

下一轮检查可读字符串:

bash 复制代码
strings -a -t x libgame.so | rg -i 'signature|reward|decrypt|\.json'

这里使用 GNU Binutils 和 ripgrep,关键词需结合项目调整。-t x 输出文件偏移,不能直接当作运行时地址。没有命中也不能证明没有暴露,字符串可能采用其他编码、被拆分,或在运行时生成。

优先挑出有业务含义的结果:校验失败日志、解密提示、奖励配置字段、内部资源路径。它们是否能把范围缩到少数函数,比一共提取出多少行更有参考价值。

对不需要的发布日志先做清理。敏感字符串加密后,再观察它何时恢复、是否长期缓存。文件里看不到明文,不能代替这一轮检查。

运行验证单独记录

静态检查能发现名称和逻辑线索,却无法可靠证明实际反调试、注入检测或防篡改效果。

运行验证至少应关注:相关条件出现后,检测是否触发,触发后采取了什么处置,以及正常使用是否受影响。只看到相关导入函数,不能说明防护实际生效。

混淆范围也要与性能回归一起检查。每帧计算、低频授权检查与资源加载,对开销的容忍度不同。应在可比较的设备和场景下,观察启动、热点执行和资源加载的变化。

每次发版保留一份对照记录

检查项 核对内容 留存结果
实际产物 最终包内各 ABI 的库是否覆盖 包版本、路径、文件哈希
符号导出 是否存在多余业务接口 符号输出、必要接口清单
调用兼容性 JNI 与跨库调用是否正常 回归场景和结果
字符串 日志、路径是否透露敏感流程 命中位置及处理记录
关键逻辑 核心函数是否容易定位和理解 入口线索、调用关系分析
运行时保护 检测是否触发,处置是否符合预期 测试条件、响应及异常记录
性能 启动、热点函数、加载是否出现明显开销 同场景加固前后结果
服务端校验 客户端修改能否影响奖励、付费权益等结果 校验规则与验证结果

这张表可以随版本保留,配置或工具链变化后,再针对受影响的项目复查。

so 文件能否被分析工具打开、符号少了多少,都只是检查中的一部分。更值得追踪的是,关键逻辑的定位成本是否发生变化,以及代码运行后是否仍有保护措施继续生效。