好友
阅读权限 10
听众
最后登录 1970-1-1
本帖最后由 Bot95 于 2026-10-2 18:59 编辑
【前言】
椒盐音乐是作者 Moriafly 开发的纯本地音乐播放器,Pro 版走 Google Play 内购解锁。
分析过程中发现两点比较有意思:一是 Pro 权益判定完全落在客户端本地状态(MMKV)上,
没有服务端强校验;二是它内置了一套名为 "Sky" 的反篡改防护,检测维度比较全面,
值得拆一拆。本文仅做技术分析,不提供任何成品。
【实验环境】
- 设备:Android 真机 / 模拟器(Android 15)
- 逆向工具:MT Manager(MCP 自动化)、Jadx、Apktool
- 分析方式:静态分析 + MCP 自动化检索(dex 字符串/方法/字段交叉引用)
【App 基本信息】
- 应用名:Salt Player(椒盐音乐)
- 包名:com.salt.music
- 版本:12.3.2(versionCode 2026090601)
- minSdk / targetSdk:23 / 36
- 签名方案:V1 + V2
- 加固情况:无壳(R8 混淆 + 自定义字符串加密)
- UI 框架:Jetpack Compose
- 关键 so:无 native 层参与校验逻辑,纯 Java/Kotlin 层
【混淆与保护手法观察】
1. **命名桶伪装**:R8 重命名时把大量业务类映射进了 androidx.media3 这个第三方
命名空间下,很容易被误判成 ExoPlayer 媒体库代码,实际是核心业务逻辑。
2. **字符串编译期加密**:所有敏感字符串(SKU、接口地址、渠道标识、邮箱等)在编译期
被替换为 long 常量,运行时统一经一个解密函数还原(形如 `X.ތ(long)`),
静态检索看不到明文,需顺着调用点理解语义。
3. **Lambda 池合并**:大量单逻辑被合并进共用的 synthetic lambda 类,通过构造参数
(case id)在方法内用 packed-switch 分发。定位具体功能必须结合 `<clinit>` 中
Lazy 包装时传入的 case 号,不能只看类的表象语义。
【权益模型分析】
Pro 通过 Google Play Billing 解锁,商品定义为枚举,含两个条目:
- **SaltPlayerPro**:主程序 Pro 权益(对应一个正式 SKU + 一个 debug SKU)
- **AudiophilePack**:Pro 之上的独立专业拓展包(USB 独占输出、DAC 控制、
一项名为 Morvanium 的智能音频过渡分析功能)
除内购外还有多渠道分发(Google Play / 官网直售 / 联名发行),
渠道判定由一个独立单例完成,官网渠道通过 MMKV 存储用户 token,
配合服务端下单接口完成购买。
购买链路大致为:Billing 管理器 → queryPurchases 回调(遍历 Purchase 的
productIds 匹配 SKU)→ 结果回调命中则置位本地状态并落盘 MMKV。
**关键结论一**:权益判定最终只依赖客户端本地的一个布尔状态(MMKV 持久化),
未购买时不会主动写 false,只吃初始化默认值。这意味着状态位的读写入口就是分析切入点,
客户端侧存在修改空间(服务端无法强校验这一层)。
【Sky 反篡改防护拆解】
这是本文的重点。该防护在启动首屏路由阶段即触发,表现为进入即弹拦截页,
页面含一个向作者报告的邮箱入口和一个"立刻上报并退出"的按钮。
检测链路集中在 Sky 相关类及其关联的懒加载 lambda,结果以布尔值注入首屏路由,
由路由决定渲染拦截页还是正常主界面。检测维度包括:
1. **assets 特征文件扫描**
遍历 assets 目录取全部文件名,与内置黑名单比对,识别常见的破解 /patch
特征文件(如 SignatureKiller、Hook_so、lspatch、KillByNP 等)。
这类检测针对的是"二次打包后再注入 patch 工具"的场景。
2. **核心签名多重校验(最关键,也是重打包必然踩雷的点)**
经一个独立的签名管理类做多层校验,包含:
- 签名 hashCode 与内置常量比对
- 包名 / 渠道哈希比对
- ApplicationInfo 的 debug flag 检测
- 第三方崩溃上报 SDK 的包名 MD5 校验
- 自定义 ClassLoader 动态加载校验
重打包后签名必然改变,上述任一项比对失败即判定篡改。
3. **宿主 Application 完整性**
校验 Application 的父类是否仍为原生父类,防止被 hook 框架替换。
4. **签名块存在性检测**
检测 APK 内是否存在 v2/v3 签名块被剥离的痕迹(SignatureKiller 式改包手法的
对抗检测)。
【遇到的坑(重点踩坑记录)】
**坑 1:检测布尔值的极性容易搞反,方向错了越改越糟。**
这是本次分析最大的弯路。该防护有两处布尔检测(assets 扫描结果 + 核心签名校验),
最终注入首屏路由决定渲染拦截页还是主界面。首次分析时我把 assets 扫描的返回值
改成了 false,结果不仅没绕过,反而被路由逻辑推向了拦截分支——因为在消费端
的分支语义里,false 恰恰对应"需要拦截"的路径。
**教训:这类开关不能凭直觉判断 true/false 哪个是"安全",必须顺着消费方的
if/else 分支把两种取值各走一遍,确认哪个值走正常流程,再动手改。**
**坑 2:认准"伪混淆",别被类名表象带跑。**
业务类藏在 androidx.media3 下,第一反应容易当成媒体库代码而跳过。
识别办法:看有没有业务特征字符串、有没有被 Settings/Billing 链路引用。
**坑 3:Lambda 池里定位功能要跟 case 号。**
共用 lambda 类里有几十个分支,靠构造参数分发。只看类的表象方法名
(比如看到一个全是 Compose 渲染代码的类)会误判其用途,必须回到
`<clinit>` 看 Lazy 包装时传的到底是哪个 case。
**坑 4:静态加密字符串让搜索失效。**
直接搜关键字符串搜不到,必须结合调用链和上下文反推语义,比如顺着
"购买结果回调 → 状态置位方法 → MMKV 写入"这条链一路跟下去。
【复现要点小结】
1. 从资源文案和购买提示切入,确认是本地权益而非服务端校验
2. 顺着 Billing 回调找到权益状态位的 setter/getter
3. 拦截页从特征文案反查渲染方法,再顺着路由消费方定位检测结果注入点
4. 检测判定常有多层(assets 扫描 + 签名多重比对 + hook 检测),
客户端侧真正卡住重打包的是签名比对那几层
5. 改任何布尔开关前,先把 true/false 两种取值在消费端各推演一遍
【核心结论】
- 权益判定:纯客户端本地状态,无服务端强校验,客户端侧存在修改空间
- 反篡改:assets 特征扫描 + 签名多重比对 + Application 完整性 + 签名块检测,
设计较完整,对二次打包和注入式 patch 都有针对性拦截
- 整体看,该 App 的加固思路值得学习:权益与反篡改解耦(权益弱、反篡改强),
把主要防护压力放在反篡改而非权益校验上
【参考】
- 静态分析工具链:MT Manager (MCP)、Jadx、Apktool
- 交流与勘误欢迎论坛内讨论
【免责声明】
本文及所涉分析仅限用于学习和研究目的,不得用于商业或非法用途,
否则一切后果由使用者自负。请勿将本文内容用于制作、传播破解成品或注册机。
分析涉及的样本请在研究结束后及时删除。
喜欢请支持正版软件。[/md]
免费评分
查看全部评分
发帖前要善用【论坛搜索 】 功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。