视频加载失败

Google Play 内购 CC Max 漏洞深度拆解 — 一个 Bundle.putInt 就能白嫖 $250/月的 Claude Max

2962 字
15 分钟
Google Play 内购 CC Max 漏洞深度拆解 — 一个 Bundle.putInt 就能白嫖 $250/月的 Claude Max
微信二维码

联系方式 & 交流群

  • 微信: gzs-47

进微信群请联系博主,各位觉得文章对你有帮助的话可否打赏一些呀~


⚠️ 免责声明:本文仅供安全研究与技术讨论。本文描述的漏洞已于 2026 年 7 月被修复。任何利用类似手段对正式服务进行未授权操作属违法行为,后果自负。

部分思路来自 cctest.ai 作者,在此致谢。


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 里。它决定了用户切换订阅方案时的扣费逻辑:

常量名行为
0UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY未知
1WITH_TIME_PRORATION按剩余时间折算差价,立刻扣费
2CHARGE_PRORATED_PRICE按比例收取差价
3WITHOUT_PRORATION立刻升级,本周期不收费,下个周期按新价扣
5CHARGE_FULL_PRICE立刻全额扣费
6DEFERRED延迟到下个周期才生效

重点关注 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

Terminal window
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 里注册的显示名:

Terminal window
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.loadNativeLibrariesSentryAndroidNdk 的初始化方法,因为 Sentry 的 native crash 上报模块在 houdini 翻译层下也会崩。不拦截的话 App 直接闪退。

除了核心的 prorationMode 拦截,脚本还 hook 了 PurchaseReceipt 的构造函数,打印 purchase_tokenorganization_id。这个 org_id 是 Anthropic 后端用来标识组织/用户的唯一 ID(格式类似 6a085918-8383-44e5-9538-c4c81c6e122b),在规模化操作时用于追踪哪个 Claude 账号绑定了哪笔 Google Play 订阅——一个 Google 账号只能给一个 Claude 账号开通订阅,org_id 就是这个映射关系的 key。

第五步:走一遍正常支付

hook 注入成功后(控制台会显示 Frida Hook 注入成功!,App 里也会弹 Toast),正常操作:

  1. 用邮箱登录 Claude 账号(不能用 Google OAuth,原因见上)
  2. 在模拟器里绑定支付卡(需要能扣 $20 的真实/虚拟信用卡)
  3. 通过 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 这个特定的攻击路径已被修复。从修复思路上猜测,可能的措施包括:

  1. 客户端侧:新版 Claude APK 不再使用 Bundle 明文传递 prorationMode,可能改用了加密或者服务端控制
  2. 服务端侧:在处理升级回调时增加了额外验证(比如检查 linkedPurchaseToken 的订阅金额是否合理)
  3. Google Play 侧:Google 可能限制了 WITHOUT_PRORATION 模式的使用场景(需要应用显式在 Google Play Console 配置)

但说实话,Google Play Billing 的这个攻击面远没有被堵死prorationMode 只是其中一个参数,BillingFlowParams 里还有其他可操控的字段。只要”客户端构建支付参数 → 传给 Google Play → Google Play 无条件执行”这个模型不变,同类问题就会持续出现。

据了解,目前仍有针对其他参数或其他 App 的类似攻击手法在流通。Google Play 内购系统的安全模型,说白了就是建立在”客户端可信”这个脆弱假设上的。

0x06 技术启示#

如果你是开发者,从这个漏洞可以学到:

  1. 永远不要信任客户端传来的支付参数。即使是通过 Google Play 这种”可信”中间人,底层的 BillingFlowParams 仍然是客户端构造的。
  2. 服务端必须独立验证订阅状态。不要只检查 token 有效性,还要验证订阅的实际扣费金额(通过 purchases.subscriptionspriceAmountMicros 字段)与预期是否一致。
  3. Pin APK 版本。如果你依赖客户端逻辑做安全控制,服务端应该检查客户端版本号,拒绝过旧版本的请求。

如果你是安全研究者,这个 case 是一个很好的”API 信任边界”研究样本。Google Play Billing Library 把自己定位成”安全的支付中间件”,但它实际上信任了太多来自客户端的输入。类似 Frida hook Bundle.putInt 这种低成本的攻击手段,在整个 Android 内购生态里适用面很广。


本文记录的是一个已修复的安全问题,旨在帮助开发者理解 Google Play 内购系统的安全边界。

如果你对 Android 安全、支付系统攻防感兴趣,欢迎关注博客,后续会持续更新更多分析。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

相关文章智能推荐
1
GPT Plus 订阅转移全解析 - 从 Google Play Token 到一键开通的完整技术链路
安全研究深度剖析 GPT Plus Android 订阅架构:Google Play purchase token 如何通过 RevenueCat 中间层实现跨账号转移,从原理到 ADB 提取、MITM 拦截、API 调用的完整实战指南。
2
焚决 Claude — 一个油猴脚本如何撬开 Anthropic 的支付大门
漏洞分析深入拆解传说中的「焚决」——通过 Tampermonkey 油猴脚本劫持 checkout_capabilities API 响应,前端注入 cassia 支付流,配合随机德国 IBAN 实现 Claude Max/Pro 零元订阅。完整技术分析:Fetch/XHR 双通道拦截、SEPA CORE 直接借记清算机制、MOD 97-10 校验算法局限性、客户端信任边界漏洞的通用攻击框架与防御方案。
3
GPT Plus 订阅漏洞深度分析 - Google Play Billing 鉴权缺失导致 0 元订阅
漏洞分析技术拆解:通过 API 响应注入和 offerToken 内存替换两种方式,利用 Google Play Billing 与 RevenueCat 回调鉴权缺失实现 GPT Plus/Pro 零元订阅。
4
Windsurf 开源反代工具:59 个 AI 模型一站式接入
教程有人把 Windsurf AI 编程 IDE 的后端扒出来,做成了 OpenAI 兼容的 API 代理。零 npm 依赖,支持多账号池轮转,5 分钟完成部署。
5
Stripe 协议支付自动化深度拆解:从 HAR 抓包到纯 API 全链路实现
技术分析完整拆解 Stripe 协议支付的技术实现:从浏览器 HAR 抓包逆向 Stripe Checkout Session 全流程,到纯 API 无浏览器实现 ConfirmationToken 创建、PaymentIntent 确认、3DS 验证挑战,再到 TLS 指纹对抗、代理池架构和反欺诈绕过。附 5 套不同技术栈的完整实现源码下载。
随机文章随机推荐
Profile Image of the Author
47工作室
47工作室:记录技术实践与安全研究。
公告
欢迎来到 47工作室!微信:gzs-47
分类
标签
最新动态
站点统计
文章
39
分类
16
标签
165
总字数
121,285
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
站点 vunknown
文章许可
CC BY-NC-SA 4.0