我 review 过的上线安全清单,十有八九是满屏的勾,看着挺踏实。但检查清单全绿,和包能否拦住破解是两码事。见过很多游戏,上线前检查没问题,结果上线没几天,重打包的破解版照样在各个群里传,急得运营像热锅上的蚂蚁,拉着各方开会讨论办法...
这件事的问题在于,勾打的是“做没做这件事”,不是“做没做到位”,这两件事差得远。所以这篇不打算梳理游戏上线前该做哪些项,那些谁都知道,我想把每一项从“做没做”改成“做到位”。
加固强度与渠道包
加固别只信那句“加固完成”。包传上去、平台回个“加固完成”,清单这项就打勾了。
加固是分等级的。有的方案静态层做得狠,符号清得干净、字符串全加密、.text 只剩个空壳;有的静态一般,可运行时的反调试、反 hook 检测堆得很全。我见过 .text 只剩十几字节的纯空壳方案,也见过反过来的:运行时检测很全,导出符号却全是明文,SDK 接口直接暴露。你勾选了“加固”,但你验证过他的强度吗?
更容易翻车的是渠道包。几十个渠道,主包加了、渠道包漏加,这种情况并不少见,泄一个出去,别人拿到的是集成好 SDK 的完整包,重打包比啃主包省事多了。验收别只看平台报告,抽个包拖 IDA 扫一眼符号和明文字符串;加固在重签名之后做,每个渠道包分开加。

签名、完整性、pinning 校验
签名、完整性、pinning 这三行,清单上通常分开写,但验收时要盯同一个问题:关键判断是不是都留在了 C# / Java 托管层。objection 一句 android sslpinning disable,或者甩个 frida-multiple-unpinning,专挑 checkServerTrusted 或 native 的 SSL_CTX_set_custom_verify 这类点,让它无脑返回通过。攻击的从来不是校验逻辑,是那个能被劫持的校验函数。逻辑再对,一个 bool 端回托管层,也是在给 Hook 留口子。三项的验收因此是同一条:下沉 native,比对在 C++ 里做完,别把结果端回去。
// native 直接调 Android API 取签名,比 C# 通过 AndroidJavaObject 调难 hook 得多
jclass pmClass = env->FindClass("android/content/pm/PackageManager");
jmethodID getPackageInfo = env->GetMethodID(pmClass, "getPackageInfo", ...);
// 取到后在 native 层直接比对,不把 bool 返回给托管层
再补两个易漏项。校验时机别只在启动时,启动校一次,绕过一次就永久通过。最好在登录、支付这些节点随机插。还有包体大小异常检测,可以防一种很阴的绕过:破解包里嵌一份完整正版 APK,启动校验时 hook 掉签名获取,让它去读嵌套的正版包返回正版签名,校验过了,实际跑的却是破解内容。这种包特征就是包体明显偏大,登录时上报客户端包体,明显偏大的打标记,能兜住一部分。
协议与重放
pinning 只能解决一部分抓包问题,协议验收还得继续往后看。真正把领奖接口刷穿的是重放:玩家自己的客户端就是个合法 TLS 端点,一个成功的请求原样再发一百遍,TLS 帮不上忙,因为在协议眼里这就是合法 client 发的合法请求。领奖接口刷奖励、支付回调刷到账,大多是这么来的。我查过一个项目,领奖接口三天被重放一万两千次。

验收看防重放三件套落地没:客户端带 ts + nonce、对 path + body + ts + nonce 签名,服务端超 ±60s 丢、nonce 用 SETNX 查重、签名对不上丢。secretKey 下沉 native 只是拖时间,它就躺在客户端里,SO 一逆密钥全有。顺带查个版本坑:targetSdk < 24 的话,用户装的 CA 默认被信任,network_security_config.xml 那道门等于自己开着。
服务端判定
跟奖励沾边的判定,一律以服务端为准。不管是时间、次数还是最后的结算,客户端报的一概别信。这里有个反直觉的点:时间作弊别去检测 hook,直接把收益端掉。为什么反而对?因为加速器放大的本来就是客户端自己数的那段时长,只要这段时长到服务端不算数,放大它就没了意义。跟 hook 拼检测是场军备竞赛,把收益端掉却是一次做对、长期有效。
// 服务端结算离线收益,客户端报的时长只当参考
long clientClaimed = req.offlineSeconds; // 客户端自己数的,可能被放大
long serverMeasured = now - player.lastSeenAtServer; // 服务端按自己的钟算
long settled = Math.Min(clientClaimed, serverMeasured);// 只认较小的那个
GrantIdleReward(settled);
清单上写“客户端做了时间校验”基本等于没做,要写的是“奖励判定是否走服务端”。
反调试与检测处理
反调试这项最容易应付了事。加了 Frida 或调试器检测,“反 Hook”打上勾就算过了。可检测到之后呢?闪退还是上报,误报成本能不能接受?反调试本身是军备竞赛,真正决定它有没有用的,是这个信号有没有接进上报和风控。埋了点,日志却落不到风控,风控也没在盯高频调用和越界数值,那点就白埋了。所以这项不看检测加没加,看的是检测结果有没有先上报、上报有没有真通到在跑的风控。

验收对照表
按照上面的思路,我整理了一张对照表。左边检查项,中间“只做到这步基本等于没做”,右边验收看什么:
| 检查项 | 只做到这步 = 没做 | 验收要点 |
|---|---|---|
| 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) |
| 反调试处理 | 只做检测 | 检测结果先上报;闪不闪退结合误报成本定 |
| 风控落地 | 埋点没接上 | 确认日志落到风控,风控在盯高频调用 / 越界数值 |
这张表想说的不是“你要检查哪些项”,而是让你清楚:怎么拉高你的门槛,你的门槛越高,对方绕它需要付出的成本就越高,当成本压过他破解的收益,这套防护才算真正起作用。