「结算单和货币发放日志对不上」— 某个周一早晨的核对
月初核对结算的运营同事发现了异常:游戏服务器发放的付费货币,远多于商店后台记录的支付笔数。差额不是几笔,而是几百笔。
按账号拆开来看,规律就浮现了。有的账号一天「购买」同一件商品四十多次,商店却一条记录也没有。还有的账号支付后立刻退款,货币却原封不动地留着。
这时最常见的反应是「是不是商店支付被攻破了」。这种可能性很低。Google 和 Apple 的支付处理本身是稳固的。攻击者盯上的不是支付,而是游戏判定支付已完成、把货币交出去的那一刻。
支付破解盯的是发放,不是支付
内购大致按以下方式进行。
- 用户在商店完成支付
- 商店把购买凭证(收据、令牌)返回给客户端
- 游戏核对该凭证并发放货币
第 1 步和第 2 步属于商店的范围,不容易下手。问题出在第 3 步。凭证由谁核对、在哪里核对,决定了这一步是稳固还是形同虚设。
如果游戏只在客户端内部核对凭证,那么核对的代码也在攻击者够得着的地方。支付绕过工具做的正是这件事——它们不去欺骗商店,而是告诉游戏「支付已经完成了」。
篡改进来的四道门
线上环境中反复观测到的类型有四种。
1. 只在客户端校验
最常见,破坏也最大。拦截支付响应改成成功,或者固定校验函数的返回值,就能直接通过。商店那边不会留下任何记录。
2. 收据重放(replay)
把一次正常购买得到的收据反复提交。服务端确实做了校验,但如果不检查这张收据是否已经用过,同一份凭证就能反复领取货币。这是结算与发放量拉开差距的典型原因。
3. 退款之后仍然保留的权益
支付、领取货币、申请退款。商店那边钱退回去了,游戏里的货币却还在。反复操作,等于在免费印货币。
4. 把待定状态当作成功
购买存在尚未确定的状态。如果把它当成功处理,即使支付最终没有完成,货币也已经发出去了。
四者有一个共同点:没有一种是攻破了支付。 它们利用的都是游戏这一侧校验流程的缺口。
校验生命周期 — 发放之前必须完成的事
把「收据校验」当成一次性的核对,就会漏掉上面的第 2 和第 3 种。校验不是购买时刻的一个事件,而是一直延续到购买之后的流程。
| 阶段 | 要做的事 | 遗漏则 |
|---|---|---|
| 传递 | 客户端把收到的凭证交给服务端 | 只剩客户端校验,暴露于第 1 种 |
| 校验 | 服务端通过商店 API 确认凭证真伪 | 伪造凭证得以通过 |
| 查重 | 比对是否为已处理过的凭证 | 暴露于重放(第 2 种) |
| 发放 | 仅在确定状态下写入货币 | 待定状态下发放(第 4 种) |
| 确认 | 向商店告知处理完成 | 自动退款造成流水损失 |
| 后续 | 接收退款与取消,回收权益 | 暴露于退款滥用(第 3 种) |
六个阶段都必须发生在服务端。客户端的职责到「传递凭证」为止。
Google Play:需要在服务端确认的事项
Google 在Play 结算安全指南中明确推荐后端校验。文档给出的要点如下。
- 把客户端拿到的
purchaseToken发送到服务端,并记录所有令牌值。 - 该令牌全局唯一,可安全地用作主键。先比对是否为此前收到过的令牌。
- 通过 Google Play Developer API 的
Purchases.products:get、Purchases.subscriptionsv2:get确认真实购买。 - 用
obfuscatedAccountId或obfuscatedProfileId同时检查账号维度的资格与滥用情况。 - 仅在购买状态为确定(PURCHASED)时发放,待定状态不发放。
- 对无效购买调用
Orders:refund并设置revoke=true,显式回收。
这里有一个容易忽略的运营陷阱:购买在三天内未被确认(acknowledge)会被自动退款。 校验逻辑毫无问题、却因为漏掉确认调用而漏掉流水的情况,现实中确实存在。
App Store:是否还在沿用旧方式
Apple 这边要确认的内容有所不同,因为校验方式本身变了。
以前是把收据数据发送到 Apple 服务器的 verifyReceipt 端点进行确认。这一方式已成为遗留方案,当前的标准是通过App Store Server API查询交易信息。该 API 返回的交易信息带有 JWS 签名,服务端可以用 Apple 的公开证书验证签名。
如果项目里直接沿用了旧文章或几年前的示例代码,值得确认一下现在究竟在用哪种方式。具体步骤和最新状态请务必以官方文档为准——支付相关的 API 变化比本文快得多。
发放前的检查清单
其中不适用的项,就是此刻敞开的门。
校验流程
- 收据与令牌的校验在服务端进行。
- 通过商店 API 确认购买真实存在。
- 保存已处理的令牌,并在发放前比对是否重复。
- 仅在购买状态确定时发放,待定状态不发放。
购买之后
- 发放后向商店发送确认处理。
- 接收退款与取消通知,回收货币或权益。
- 发现无效购买时显式回收。
账号与运营
- 购买与账号绑定记录,可发现同一账号的异常重复购买。
- 定期核对结算金额与发放量。
- 同时判断发出支付请求的客户端是否被篡改。
OZero Security 负责的范围与不负责的范围
在这个主题上,先说清楚我们不做什么更合适。
不负责的范围
- 我们不代为校验收据与购买令牌。 调用商店 API 确认购买的真伪与状态,是你们服务端的工作。我们无法取代那个位置,也不会声称可以取代。
- 防止重复处理、退款回收、结算核对同样属于服务端与运营的职责。
负责的范围
- 判断发出校验请求的客户端本身是否可信。识别可执行文件的二进制篡改、调试器附加与挂钩,以及模拟器、Root 等非可信运行环境,提高绕过难度。
- 检测 MOD APK 重打包与重签名篡改,并根据项目策略进行响应(日志、回调或退出)。修改支付逻辑后重新分发的构建就属于这一类。
- 接入 Pro Add-on 后,收集可疑客户端的遥测数据,支持在后台按运营策略进行处置。
两者有先后顺序。服务端校验在先。 如果服务端没有认真核对凭证,客户端加固得再牢也没有意义。反过来,在服务端校验到位之后,发出请求的客户端是不是被篡改的构建,就成了下一个问题。
结论:要守的是发放,不是支付
把内购安全理解成「别让支付被攻破」,方向就偏了。商店的支付已经足够稳固。真正要守的是游戏如何核实「支付已完成」这个说法。
如果结算与发放量正在拉开差距,原因通常是上述六个阶段中有一个空着。其中查重与退款回收最容易在初次搭建时被落下,而且损失是悄悄累积的。
下一步
- 确认防护范围 —— 在产品页查看各平台的支持规格,以及各方案(Standard / Pro)的防护范围。
- 快速开始接入 —— 通过 Package Manager 的 My Assets(Asset Store 购买)或 .unitypackage(门户下载)接入现有项目。
- 技术咨询 —— 如果遇到支付相关的篡改案例或线上运营问题,欢迎联系我们。
先检查服务端校验,再看发出请求的客户端。