上线安全清单最坑的地方,是它只回答“做没做”,不回答“做没做到位”。全绿的清单能让人踏实,但踏实和安全是两码事,我见过太多检查全过、上线没几天就被破解或刷接口的项目。
这篇不铺原理,直接给一份能照着查的清单:每一项标清“做到什么程度约等于没做”和“验收要看什么”。代码以 Unity(C#)和 Android native(C++)举例,其他引擎思路通用。
一、总检查清单(建议收藏)
| 检查项 | 只做到这步 = 没做 | 验收要点 |
|---|---|---|
| SO / dex 加固 | 平台跑一遍回个"完成" | 抽包拖 IDA 看符号表 / 明文字符串;区分静态强度与运行时检测 |
| 渠道包加固 | 主包加了就算数 | 重签名之后做,每个渠道包分别加固,别统一偷懒 |
| 签名校验 | 写在 C# / Java 层 | 下沉 native,比对在 C++ 完成,不返回 bool |
| 完整性校验 | 只在启动时校一次 | 关键文件记 hash;登录 / 支付 / 关卡随机插入校验 |
| 包体大小检测 | 没做 | 登录上报客户端包体,异常偏大打标记(防嵌套正版包 + hook 签名) |
| pinning | 挂在托管层 handler | native 校验;叠 Frida / 调试器检测 + 上报 |
| 防重放 | 只上了 HTTPS | ts+nonce+签名,服务端 SETNX 查重 + ±60s 窗口 |
secretKey 存放 | 明文在 C# 层 | 下沉 native(仅拖时间,不是终点) |
targetSdk | 压在 24 以下 | ≥ 24,否则默认信任用户 CA,network_security_config.xml 漏风 |
| 时间 / 次数权威 | 走客户端时钟 | 奖励判定走服务端时间 / 次数;离线收益取 Min(client, server) |
| 反调试处理 | 只做检测 | 检测结果先上报;闪不闪退结合误报成本定 |
| 风控落地 | 埋点没接上 | 确认日志落到风控,风控在盯高频调用 / 越界数值 |
下面把几个最容易"打了勾但没做到位"的项拆开说。
二、加固:验收要看强度,不是看"完成"
平台回个"加固完成"就打勾,是最常见的自欺。加固分等级,验收时分两个维度确认:
- 静态强度:抽一个加固后的包拖进 IDA,看符号表清没清干净、字符串有没有加密、
.text段是不是空壳。 - 运行时防护:反调试、反 Hook、模拟器检测全不全。
两个维度是分开的,一个强不代表另一个强。另外一定检查:渠道包有没有漏加固。几十个渠道,主包加了、某个渠道包忘了太常见,而渠道包泄露后别人拿到的是集成好 SDK 的完整包,重打包门槛更低。加固要在重签名之后做,每个渠道包分别加。
三、签名 / 完整性 / pinning:三项同一个坑
这三项验收就盯一件事:校验判断是不是留在了 C# / Java 托管层。留在托管层,一句 objection 的 android sslpinning disable 或一个 frida-multiple-unpinning 脚本,专挑返回 bool 的校验函数让它无脑返回 true 就废了。
核心一句:攻击的不是你的校验逻辑,是那个能被劫持的校验函数。验收标准是能下沉 native 就下沉,比对在 C++ 做完,别把 bool 端回托管层:
// native 直接调 Android API 取签名,比 C# 通过 AndroidJavaObject 调难 hook 得多
jclass pmClass = env->FindClass("android/content/pm/PackageManager");
jmethodID getPackageInfo = env->GetMethodID(pmClass, "getPackageInfo", ...);
// 取到后在 native 层直接比对,不把 bool 返回给托管层
两个补充项:校验别只在启动时跑(绕一次就永久通过,要在登录/支付随机插);加一道包体大小检测(防破解包里嵌正版 APK、Hook 签名获取的绕过,特征是包体明显偏大)。
四、防重放:HTTPS 不够,要的是"请求一次性"
高风险接口先列出来,这几类必查防重放:
- 登录态 / Token 刷新
- 充值回调
- 活动奖励领取
- 排行榜提交
- 战斗结算
- 道具合成 / 消耗
为什么 HTTPS 不够:玩家客户端本身就是合法 TLS 端点,把一个成功请求原样再发一百遍,HTTPS 帮不上,在协议眼里就是合法请求。防重放至少要这几个字段和校验:
- 客户端:每个请求带
ts(时间戳)+nonce(一次性随机串),对path + body + ts + nonce签名; - 服务端三道关:
ts超 ±60s 窗口丢、nonce用 RedisSETNX查重丢、签名对不上丢。
注意边界:签名密钥躺在客户端里,secretKey 下沉 native 只是拖时间,SO 逆出来密钥就有了。所以最终判断还得回服务端。
五、服务端判定:所有防护的最后一道
原则一句话:跟奖励沾边的判定(时间、次数、结算),一律以服务端为准,客户端报的只当参考。
一个反直觉但实用的做法:查时间作弊(加速器)别去检测 hook,直接把收益端掉。因为加速器放大的本就是客户端自己数的那段时长,只要它到服务端不算数,放大就没意义。结算离线收益时取客户端和服务端两者的较小值:
long clientClaimed = req.offlineSeconds; // 客户端自己数的,可能被放大
long serverMeasured = now - player.lastSeenAtServer; // 服务端按自己的钟算
long settled = Math.Min(clientClaimed, serverMeasured);// 只认较小的那个
GrantIdleReward(settled);
六、一个通用的验收判断法
清单再全,最终每一项都用同一个标准过一遍:绕过它,对方要付多少成本,够不够压过他破解拿到的收益。 压得过就有用,压不过勾打得再满也只是自我安慰。客户端天然不可信这个前提改不了,加固、校验、协议本质都是往上垒成本,不是为了绝对挡死,是让动手的人觉得不划算。
完整版(含全部验收项和 FAQ)在字节暗面主站,想要清单原文可以搜「字节暗面 游戏上线安全验收」。