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