payment

流水没变,货币却在变多 — 内购收据校验的缺口

「结算单和货币发放日志对不上」— 某个周一早晨的核对

月初核对结算的运营同事发现了异常:游戏服务器发放的付费货币,远多于商店后台记录的支付笔数。差额不是几笔,而是几百笔。

按账号拆开来看,规律就浮现了。有的账号一天「购买」同一件商品四十多次,商店却一条记录也没有。还有的账号支付后立刻退款,货币却原封不动地留着。

这时最常见的反应是「是不是商店支付被攻破了」。这种可能性很低。Google 和 Apple 的支付处理本身是稳固的。攻击者盯上的不是支付,而是游戏判定支付已完成、把货币交出去的那一刻。

支付破解盯的是发放,不是支付

内购大致按以下方式进行。

  1. 用户在商店完成支付
  2. 商店把购买凭证(收据、令牌)返回给客户端
  3. 游戏核对该凭证并发放货币

第 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 后,收集可疑客户端的遥测数据,支持在后台按运营策略进行处置。

两者有先后顺序。服务端校验在先。 如果服务端没有认真核对凭证,客户端加固得再牢也没有意义。反过来,在服务端校验到位之后,发出请求的客户端是不是被篡改的构建,就成了下一个问题。

结论:要守的是发放,不是支付

把内购安全理解成「别让支付被攻破」,方向就偏了。商店的支付已经足够稳固。真正要守的是游戏如何核实「支付已完成」这个说法。

如果结算与发放量正在拉开差距,原因通常是上述六个阶段中有一个空着。其中查重与退款回收最容易在初次搭建时被落下,而且损失是悄悄累积的。

下一步

  1. 确认防护范围 —— 在产品页查看各平台的支持规格,以及各方案(Standard / Pro)的防护范围。
  2. 快速开始接入 —— 通过 Package Manager 的 My Assets(Asset Store 购买)或 .unitypackage(门户下载)接入现有项目。
  3. 技术咨询 —— 如果遇到支付相关的篡改案例或线上运营问题,欢迎联系我们。

先检查服务端校验,再看发出请求的客户端。

从可信的客户端开始 — OZero Security 的 Native 完整性校验。

相关阅读