Google Play 内购 CC Max 漏洞深度拆解 — 一个 Bundle.putInt 就能白嫖 $250/月的 Claude Max
0x00 背景:$20 的 Pro,$250 的 Max,中间差了一个 hook
Claude 的 Android 客户端走 Google Play 内购(Google Play Billing Library)处理订阅。价格梯度大概是这样:
| 方案 | 月费 |
|---|---|
| Pro | $20 |
| Max 5x | $125 |
| Max 20x | $250 |
正常流程:你是 Pro 用户,想升级到 Max,Google Play 会按剩余天数计算差价,立刻从你的信用卡上扣。
但如果我告诉你,有个参数决定了 Google Play 对这笔”升级”收不收钱呢?
而且这个参数,就是 Android Bundle 里一个普普通通的 putInt 调用?
0x01 prorationMode:Google Play 里最被低估的攻击面
Google Play Billing Library 在处理订阅升/降级时,有个参数叫 prorationMode(比例分配模式),定义在 BillingFlowParams.SubscriptionUpdateParams 里。它决定了用户切换订阅方案时的扣费逻辑:
| 值 | 常量名 | 行为 |
|---|---|---|
| 0 | UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY | 未知 |
| 1 | WITH_TIME_PRORATION | 按剩余时间折算差价,立刻扣费 |
| 2 | CHARGE_PRORATED_PRICE | 按比例收取差价 |
| 3 | WITHOUT_PRORATION | 立刻升级,本周期不收费,下个周期按新价扣 |
| 5 | CHARGE_FULL_PRICE | 立刻全额扣费 |
| 6 | DEFERRED | 延迟到下个周期才生效 |
重点关注 3 — WITHOUT_PRORATION:Google 官方文档里写得很清楚,这个模式的设计意图是”让用户立刻获得高级功能,但推迟扣费到下个计费周期”。本来是给某些业务场景用的(比如试用期升级),但如果被滥用……
Claude 的客户端代码里,升级订阅时用的是模式 1(WITH_TIME_PRORATION),也就是正常的按比例扣差价。它通过 Bundle.putInt("prorationMode", 1) 把这个值传给 Google Play 的计费 Activity。
这个 Bundle.putInt 调用发生在客户端侧。客户端侧的东西,你懂的。
0x02 攻击链:Frida 一条龙
整个攻击链其实非常简洁,所有操作都在模拟器上完成。用到的工具:
- MuMu 模拟器(x86_64,因为要跑 Frida server 的 x86 版本)
- KernelSU(MuMu 自带,用来获取 root)
- Frida 17.16.0(动态插桩框架)
- 特定版本的 Claude APK(v1.260330.27,在 Uptodown 可以下到历史版本)
- 一个能扣 $20 的美区 Google 账号
- 家宽 IP(避免 Google 风控拦截)
步骤拆解:
第一步:搭环境
MuMu 模拟器装好后,用它自带的 Google 安装器装上 Google 套件 + Play 商店。打开 KernelSU Manager 给 Shell 授权 root。这步没啥技术含量,MuMu 对 Google 服务兼容性比 LDPlayer 好不少。
第二步:准备 Claude APK
这里有个前置问题——你不能直接从 Play 商店装 Claude。
现在的 Android 应用普遍使用 App Bundle(AAB)格式发布,Play 商店下发的是 split APK:base.apk 加一堆按设备拆分的 config splits(ABI、密度、语言)。x86_64 模拟器拿到的 splits 跟 ARM 真机不一样,而且 split APK 不能直接 adb install 侧载。
所以需要一个经过修补、移除了 split 限制的完整 APK。实测可用的版本是 v1.260330.27,从 Uptodown 等第三方市场可以找到历史版本的完整包。关键词:“已修补 split 限制”——就是把 App Bundle 重新打包成单个 APK,绕过 Play 商店的分发限制。
还有一个连锁问题:修补过的 APK 签名跟 Google Play 原版不一致(相当于 debug 签名),Google OAuth 登录会直接拒绝——点 “Continue with Google” 没反应或者报错。解决办法很简单:用邮箱登录(Continue with email),输入 Claude 账号的邮箱和密码,完全不走 OAuth。
第三步:部署 Frida
adb push frida-server-17.16.0-android-x86_64 /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
adb shell su -c "setenforce 0"
adb shell su -c "/data/local/tmp/frida-server -D &"
MuMu 有个坑:它的 ADB 端口不是标准 5555,而是 5557(多开的话 5559、5561 递增)。另外 MuMu 用 houdini 做 ARM 翻译,所以 Frida 必须用 attach 模式,不能用 spawn 模式(-f 参数),否则 houdini 翻译层直接 SIGSEGV 崩掉。
第四步:注入 Hook
核心的 hook 脚本其实就干了一件事——拦截 Bundle.putInt,把 key 为 prorationMode 的值从 1 改成 3:
var Bundle = Java.use("android.os.Bundle");
Bundle.putInt.overload("java.lang.String", "int").implementation = function (key, value) {
if (key === "prorationMode") {
console.log("[*] 拦截 prorationMode: " + value + " → 3 [WITHOUT_PRORATION]");
return this.putInt(key, 3); // 就这一行,价值 $230/月
}
return this.putInt(key, value);
};
注意启动方式——必须先手动打开 Claude,再用 -H 连接(而不是 -U),因为 MuMu 的 USB 层跟标准 Android 不一样。而且 Frida 的进程名参数必须用显示名 Claude,不能用包名 com.anthropic.claude——MuMu 上 -H 模式下 Frida 按包名是找不到进程的,只认 app_process 里注册的显示名:
adb forward tcp:27042 tcp:27042
frida -H 127.0.0.1:27042 Claude -l hook-replacement-mode.js
# 注意:用 com.anthropic.claude 会报 "unable to find process"
这里还有个细节:脚本额外拦截了 SentryNdk.loadNativeLibraries 和 SentryAndroidNdk 的初始化方法,因为 Sentry 的 native crash 上报模块在 houdini 翻译层下也会崩。不拦截的话 App 直接闪退。
除了核心的 prorationMode 拦截,脚本还 hook 了 PurchaseReceipt 的构造函数,打印 purchase_token 和 organization_id。这个 org_id 是 Anthropic 后端用来标识组织/用户的唯一 ID(格式类似 6a085918-8383-44e5-9538-c4c81c6e122b),在规模化操作时用于追踪哪个 Claude 账号绑定了哪笔 Google Play 订阅——一个 Google 账号只能给一个 Claude 账号开通订阅,org_id 就是这个映射关系的 key。
第五步:走一遍正常支付
hook 注入成功后(控制台会显示 Frida Hook 注入成功!,App 里也会弹 Toast),正常操作:
- 用邮箱登录 Claude 账号(不能用 Google OAuth,原因见上)
- 在模拟器里绑定支付卡(需要能扣 $20 的真实/虚拟信用卡)
- 通过 Google Play 订阅 Pro($20/月)—— 这笔钱是真扣的
第六步:触发升级
Pro 订阅成功后,直接在 App 里点”升级到 Max”。这时候 hook 就发挥作用了:
控制台输出:
[*] 拦截 prorationMode: 1 [WITH_TIME_PRORATION] → 3 [WITHOUT_PRORATION]
同时 PurchaseReceipt 的 hook 会打印出新订阅的 org_id,确认升级已经走通了 Anthropic 后端。
Google Play 的计费页面会显示:
- 产品:Claude Max
- 价格:$250.00/month(日区显示 JP¥42,500/month)
- 开始日期:2026年8月20日(注意,是下个月!)
- “我们将于 2026年8月20日向您收取第一笔费用”
看到了吗?因为 prorationMode 被改成了 WITHOUT_PRORATION,Google Play 认为”当前不收费,下个计费周期再扣”。点击订阅,立刻升级到 Max,卡上不会多扣一分钱。
第七步:善后
升级成功后,去 payments.google.com 把这个订阅的支付方式换成余额钱包(Google Play 余额一般是 0)或者一张空的虚拟卡,然后解绑原来的支付卡。
下个月续费的时候,余额不够或者虚拟卡扣费失败,订阅自然过期。但这一个月的 Max 你已经用上了。
$20 的成本,享受 $250 的服务。利润率 1150%。
有一个规模化的硬约束:一个 Google 账号只能给一个 Claude 账号开通订阅。想批量撸就得不断囤 Google 号——这也是为什么这条灰产链的瓶颈不在技术,而在号源。
0x03 为什么这个 bug 能成立
从技术层面分析,这个漏洞能成立需要几个条件同时满足:
1. prorationMode 由客户端决定
这是最根本的问题。Google Play Billing Library 的设计是把 prorationMode 作为客户端参数传递给 Google Play 的计费系统。Google Play 本身不会校验”这个 App 有没有资格用 WITHOUT_PRORATION 模式”——它无条件信任客户端传来的值。
这是一个经典的信任边界错误:把安全关键参数的决定权放在了不可信的客户端侧。
2. 服务端验证缺位
Claude 的后端在收到 Google Play 的订阅回调时,主要验证的是”这个 purchase token 是否有效”和”订阅是否 active”。它不会去检查 prorationMode 是什么——因为这个信息在服务端回调里根本就没有。Google Play 的 purchases.subscriptions API 返回的数据里不包含当初用的是哪个 proration mode。
3. 历史版本 APK 的可用性
新版 Claude 可能已经做了混淆或者改变了参数传递方式,但旧版本在 Uptodown 等第三方市场永远可以下到。只要 Google Play 的 Billing API 版本还兼容,旧 APK 照样能完成订阅操作。
0x04 不用 hook 的路子:速刷
值得一提的是,prorationMode hook 并不是唯一的利用方式。圈子里还流传一种叫”速刷”的打法——不需要 Frida,不需要 hook,纯靠旧版本客户端的某些行为差异来打时间差。
具体原理不在本文展开(毕竟还没被修),但核心思路类似:利用 Google Play Billing Library 旧版本在处理订阅变更时的窗口期,在扣费确认到达之前完成订阅切换。跟 prorationMode hook 相比,速刷的门槛更低(不需要 root、不需要 Frida),但成功率也更低,而且强依赖特定的历史版本 APK。
这两种方法本质上攻击的是同一个面:Google Play 对客户端行为的过度信任。
0x05 修复状态与后续
截至 2026 年 7 月,prorationMode hook 这个特定的攻击路径已被修复。从修复思路上猜测,可能的措施包括:
- 客户端侧:新版 Claude APK 不再使用 Bundle 明文传递 prorationMode,可能改用了加密或者服务端控制
- 服务端侧:在处理升级回调时增加了额外验证(比如检查
linkedPurchaseToken的订阅金额是否合理) - Google Play 侧:Google 可能限制了 WITHOUT_PRORATION 模式的使用场景(需要应用显式在 Google Play Console 配置)
但说实话,Google Play Billing 的这个攻击面远没有被堵死。prorationMode 只是其中一个参数,BillingFlowParams 里还有其他可操控的字段。只要”客户端构建支付参数 → 传给 Google Play → Google Play 无条件执行”这个模型不变,同类问题就会持续出现。
据了解,目前仍有针对其他参数或其他 App 的类似攻击手法在流通。Google Play 内购系统的安全模型,说白了就是建立在”客户端可信”这个脆弱假设上的。
0x06 技术启示
如果你是开发者,从这个漏洞可以学到:
- 永远不要信任客户端传来的支付参数。即使是通过 Google Play 这种”可信”中间人,底层的
BillingFlowParams仍然是客户端构造的。 - 服务端必须独立验证订阅状态。不要只检查 token 有效性,还要验证订阅的实际扣费金额(通过
purchases.subscriptions的priceAmountMicros字段)与预期是否一致。 - Pin APK 版本。如果你依赖客户端逻辑做安全控制,服务端应该检查客户端版本号,拒绝过旧版本的请求。
如果你是安全研究者,这个 case 是一个很好的”API 信任边界”研究样本。Google Play Billing Library 把自己定位成”安全的支付中间件”,但它实际上信任了太多来自客户端的输入。类似 Frida hook Bundle.putInt 这种低成本的攻击手段,在整个 Android 内购生态里适用面很广。