ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AssetBundle热更新安全自查:从CDN清单到本地缓存的防篡改链路

AssetBundle热更新安全自查:从CDN清单到本地缓存的防篡改链路 AssetBundle 热更新大家都会做但真正敢拍胸脯说“我的热更链路是安全”的团队真不多。我见过太多项目包体往 CDN 一传、清单一发、客户端一拉就以为万事大吉了。结果线上出了资源被篡改的事故排查起来一头雾水从 CDN 清单一路追到本地缓存处处都可能有问题。这篇内容就是我基于实际排查经验整理的热更新安全自查思路围绕“CDN 清单 → 资源下载 → 本地缓存”这条完整链路把每个环节的安全隐患、校验手段、排查方法掰开揉碎了讲清楚。如果你正在做 Unity 项目热更或者被线上更新问题折腾过这篇应该能帮你少踩不少坑。1. 热更新安全的全局视角先想清楚你在防谁1.1 热更资源到底会被谁盯上搞安全的人常说一句话先确定威胁模型再谈防御方案。热更新安全也一样你得先想明白你的 AssetBundle 资源到底会面临哪些威胁。第一种最常见的威胁是普通玩家出于好奇或者想“简化游戏”去修改资源。他们可能用现成工具解包 AssetBundle改个贴图、改个数值、改个文案然后想办法让客户端加载自己的版本。这类人技术门槛不高但数量多而且经常在各种社区分享成果影响面扩散得很快。第二种是真正的作弊者。他们会专注修改战斗核心资源比如技能描述、伤害数值表、CD 时间、敌人属性这些配置类 AssetBundle。这类修改一旦成功就等于给外挂提供了最稳定的数据源游戏平衡直接被破坏严重的话还会影响内购和排行系统。第三种容易被忽略的威胁是 CDN 或更新链路本身被污染。这个不一定是有人故意攻击有时候是 CDN 缓存配置错误、回源被劫持、后台误操作更新了错误版本结果玩家拿到的资源和服务器预期的完全不一致。这种问题往往影响的是全量玩家一旦发生就是灾难性事故。明确了这三类威胁你才能理解为什么不能只做“下载成功后就不管了”的校验。热更新安全不是某一个点的校验而是一条链路上每一环都要有对应的检测和兜底措施。1.2 一条 AssetBundle 热更链路上有哪些关键环节大多数 Unity 项目的热更链路大致可以拆成这么几段客户端启动时先从本地或远程获取热更清单Manifest清单里记录了每个 AssetBundle 的版本号、文件大小、哈希值、下载地址客户端根据清单对比本地已有的资源决定哪些需要下载下载过程中通过 CDN 拉取 AssetBundle 文件文件落盘到本地缓存目录运行时按需加载这些 AssetBundle看起来流程很清晰但每段都存在被攻击或出错的空间。远程清单可以被替换CDN 上的文件可以被篡改本地缓存目录可以被注入加载阶段可以绕过校验。所以排查和加固的时候必须把这几段拆开来看不能笼统地觉得“反正校验一下哈希就行”。用个生活化一点的类比你网购一个贵重物品商家发货时给了你一个防伪码快递途中可能被人掉包你收货时如果不验证防伪码就签收那拿到假货的概率就全靠运气了。热更新链路也是这样清单就是防伪码CDN 就是快递链路本地缓存就是你的收货地址每一环都得对得上才行。1.3 版本一致性是安全的地基排查热更新问题时我第一个会查的往往不是加密和签名而是版本一致性。这个点经常被忽略但很多诡异问题都源于版本错配。比如线上跑的是 1.2.0 版本的客户端服务器清单已经更新到 1.3.0 的资源列表了但 CDN 回源时缓存策略出了问题返给客户端的还是旧版清单。这时候客户端可能下载到不完整的资源也可能下载下来一堆本地一校验就失败的 AssetBundle。另一种情况是客户端本地缓存的资源版本和清单不匹配。比如玩家上次更新到一半退出了旧资源还在新清单已经下发客户端一对比发现本地有文件但哈希对不上就会反复下载。如果这时候下载还失败就会陷入更新死循环。所以做安全排查的时候第一步永远是确认全链路的版本视图是一致的服务器版本、CDN 缓存版本、客户端内置版本、本地缓存版本这四个视角必须对齐。对不齐后面无论做多少校验都是徒劳。2. 守住源头CDN 名单与下发环节的加固要点2.1 清单文件是整个安全体系最关键的锚点在热更新链路里Manifest 清单文件承担了一个非常核心的作用它是客户端判断所有 AssetBundle 真伪的唯一依据。你下载的每个 Bundle 哈希是多少、该放在哪个路径、应该加载哪个版本全部以清单为准。正因为清单这么重要攻击者往往第一个盯上的就是它。如果清单本身可以被篡改那后面所有校验都是废纸——攻击者把清单里的哈希改成他自己构造的恶意 Bundle 的哈希客户端还以为是官方资源照样下载照样加载。所以加固的第一步就是要保证清单文件本身的完整性和真实性。最基础的做法是为清单文件生成独立的校验值比如把清单的 SHA-256 哈希下发到客户端做二次比对。更进一步的做法是对清单文件做签名客户端内置公钥每次加载清单时先验签验签通过才信任清单内容。这里有个很经验的点很多人会为了省事把清单校验和打包成一个热更资源一起下发这等于让攻击者一起改了。正确的做法是清单校验值要么编译进客户端要么通过一个独立于热更链路的接口下发而且要做好防重放的保护避免攻击者抓包之后把旧版本清单反复注入。2.2 CDN 链接配置防别人盗刷你的更新流量热更新安全里CDN 侧的配置经常被忽略因为它看起来只是“下载个文件”不像本地缓存那样能直接被人改文件。但 CDN 配置一旦出问题同样会造成严重的安全事故。先说链接级别的问题。很多团队会直接使用一个不带任何约束的 CDN 地址比如https://yourcdn.com/hotupdate/bundles/{version}/xxx.bundle。这种地址如果有人拿到就可以直接扫描你的 CDN 目录把你的所有资源批量下载走。这不仅是盗刷流量的问题更是给攻击者提供了完整的资源分析样本。比较靠谱的做法是给热更资源加上签名鉴权比如用 CDN 的时效性签名有时也叫鉴权 Key 或 URL 签名在生成下载地址时带上时间戳和签名串CDN 节点校验签名在有效期内才放行。这样即使地址泄露过期后也无法继续下载。同时配合 Referer 校验、IP 黑白名单之类的访问控制能过滤掉很大一部分自动化扫描流量。另外一个容易被忽视的点是 CDN 缓存策略。如果热更资源的缓存失效时间设得太长CDN 节点可能会在服务器端已经更新文件的情况下继续向玩家分发旧文件。这个时候如果你在客户端校验的是新清单里的哈希就会本地下载失败或者哈希不匹配。所以热更资源的 Cache-Control 建议设成较短的缓存时间清单文件更是建议设为不缓存或每次回源校验。2.3 更新明细下发接口也要纳入审计范围热更清单除了做成文件放到 CDN 之外很多时候还会通过一个更新检查接口比如登录后的版本检查接口来下发版本号和更新通知。这个接口如果没做防护一样会变成攻击面。具体来说攻击者可能模拟这个接口给客户端下发一个伪造的版本号和新资源地址诱导客户端去下载恶意服务器上的资源。要防住这个接口必须走 HTTPS并且要做签名或者令牌校验保证下发内容确实来自你的服务器。接口返回的版本号、资源地址、清单哈希都应该有服务端签名。我自己排查过的一个案例就是客户端更新用的检查接口没签名测试过程中抓包改了返回的清单下载地址客户端就真去拉取了一个本地局域网里的恶意 Bundle而且因为后续校验也没做加载成功后很多操作就不可控了。这个案例给我的教训是接口和文件链路都要一起纳入安全设计不能只盯着文件本身的哈希。3. 下载链路的双重校验哈希只是及格线签名才是安全线3.1 先搞清楚哈希校验到底能防什么不能防什么大多数普通项目会做的热更新校验就是下载完 AssetBundle 后计算一下文件的哈希值然后跟清单里的记录对比一下一致就通过不一致就重新下载。这个做法防得住“下载过程中文件损坏”和“CDN 意外返回错误文件”这两类问题但防不住人。为什么因为哈希本身是明文记录的。攻击者拿到了你的清单完全可以构造一个恶意 AssetBundle然后把这个恶意文件的哈希值也写进清单里。客户端去校验哈希发现一致照样加载。所以我说哈希校验只是及格线它保证的是传输完整性不是内容真实性。要让内容真实性有保障必须引入签名机制。用非对称加密私钥在服务器侧将每个 AssetBundle 的哈希值做签名或者将整个清单文件做签名客户端用内置的公钥去验签。攻击者没有私钥就算他改了资源文件的内容也伪造不出对应的签名客户端一验签就会发现问题。3.2 下载完之后是不是就万事大吉了我见过不少项目把校验写在下载完成后之后就进入加载流程了。这里有个细节很多人没注意校验完之后到真正加载 AssetBundle 之间还有一段时间间隔和一个文件读取的过程。如果攻击者卡在“校验完但还没加载”的窗口期替换掉本地缓存文件那么你加载时用的确实是被替换后的文件。所以严谨的做法是在加载前再做一次校验而且要从加载目标路径读取文件后计算哈希再和签名后的哈希做比对。有条件的项目可以边加载边校验或者将文件内容直接读到内存里在内存中计算哈希并校验这样能在很大程度上避免“校验通过但加载了别的文件”的问题。另一个容易踩的坑是校验算法选择。MD5 和 SHA-1 都不建议用了碰撞攻击的风险是真实存在的。最低配置建议使用 SHA-256配合 RSA 或 ECDSA 签名。ECDSA 的签名长度更短计算性能也更好在移动端上体验更友好。3.3 一个可参考的双重校验加载逻辑C# 伪代码下面这段代码是我在自己项目里整理出来的一个简化版双重校验流程核心就是在下载后和加载前分别做一次校验保证资源完整性。public class HotUpdateVerifier { // 这里简化处理实际项目中公钥应存放在客户端安全区 private static readonly byte[] PublicKey YourEmbeddedPublicKey; // 校验下载完成的AB文件 public static bool VerifyBundleFile(string filePath, string expectedHash, byte[] signature) { // 计算文件哈希 byte[] fileHash; using (var stream File.OpenRead(filePath)) { using (var sha256 SHA256.Create()) { fileHash sha256.ComputeHash(stream); } } // 对比哈希 string fileHashStr Convert.ToHexString(fileHash); bool hashMatch string.Equals(fileHashStr, expectedHash, StringComparison.OrdinalIgnoreCase); // 验证签名清单中该文件的哈希签名 bool signedMatch VerifySignature(expectedHash, signature); return hashMatch signedMatch; } // 加载前再次验证 public static AssetBundle LoadVerifiedBundle(string cachedPath, string expectedHash, byte[] signature) { if (!VerifyBundleFile(cachedPath, expectedHash, signature)) { Debug.LogError($[HotUpdate] 资源校验失败: {cachedPath}); // 进入清理、重下或者兜底逻辑 return null; } return AssetBundle.LoadFromFile(cachedPath); } }注意一个细节清单里存放的签名是“对哈希的签名”不是对文件整体签名。因为文件体积大直接签名整个文件会在移动端产生较大的性能开销而签哈希已经足够保证安全性了。你只要保证清单本身可信那么清单里的哈希签名就决定了资源内容的可信度。3.4 校验失败的兜底流程设计校验失败了怎么办很多人第一反应是重新下载但如果你的 CDN 上本身就是被篡改的文件那重新下载多少次结果都一样。所以失败处理不能只做“重试一次”而是要分场景处理。我的习惯是先区分是“校验值不匹配”还是“下载失败”。“下载失败”可以直接重试“校验值不匹配”则需要先比对 CDN 端的文件哈希看是不是 CDN 缓存异常如果 CDN 端也不对那就要配合运维和服务端排查是发布版本的问题还是被篡改的问题了。同时客户端要保留错误日志记录设备型号、系统版本、加载路径、下载地址、期望哈希和实际哈希。这些日志在异常排查时非常关键能帮你快速判断是个别玩家设备上的缓存污染还是全量玩家 CDN 拉取异常。4. 本地缓存攻防落盘之后危险才刚刚开始4.1 缓存目录选择能不能随便定AssetBundle 下载后通常要落到本地缓存目录很多项目图省事直接放在Application.persistentDataPath下面然后按 Bundle 名字铺开一堆文件。这个做法安全性很一般因为 persistentDataPath 在各个平台上都是公开可写的应用沙箱目录攻击者如果能拿到设备文件系统权限就可以直接对你的缓存文件动手动脚。稍微安全一点的做法是把缓存目录放在应用自己的私有沙箱中并且对目录做一些权限限制。iOS 和 Android 上系统本身提供了沙箱机制应用之间的文件访问是被隔离的。但这只是基础不能把它当成绝对防线。Android 上如果玩家有 root 权限沙箱形同虚设iOS 上非越狱设备相对安全但越狱设备同样能突破沙箱。在路径规范上还有一个小细节不要直接把 Bundle 的远程路径或者 URL 拼接成本地路径防止目录穿越。比如攻击者伪造了一个资源路径包含../../如果你没有做过滤就可能把文件写到沙箱之外的目录这不仅影响安全性还可能导致系统崩溃。4.2 本地缓存加密到底救不了什么很多团队为了防本地资源被改会考虑对缓存文件做加密。这想法没错但要清楚加密能解决什么问题不能解决什么问题。加密能解决的是玩家直接通过文件管理器看到你的 AssetBundle 内容拿去解包分析资源结构。如果你的资源里包含敏感的数据表、美术素材、关卡配置加密至少能提高解包的成本。加密解决不了的是运行时的内存注入和加载后修改。攻击者完全可以不读你的缓存文件而是在游戏运行过程中通过内存修改器把资源数据改掉。加密保护的是静态文件不保护动态内存。所以正确的心态是把加密当作“提高门槛”的手段而不是“绝对防御”的保证。配合 IL2CPP、代码混淆、运行时校验这些手段一起上整体安全性会好很多。我实际项目里用的是对称加密AES加密缓存文件密钥通过设备绑定信息做派生不直接硬编码。虽然技术上不能防 root 设备上的完全分析但已经足以挡住一大票“解包党”了。要真追求极致安全得配合服务端行为校验来做本地加密只是其中一环。4.3 缓存文件的脏数据治理与过期淘汰热更新最烦的 bug 之一就是本地缓存里残留着旧版本的 AssetBundle。游戏版本更新后旧缓存文件可能和新版本的清单哈希对不上但客户端做差异对比时往往只看“文件存在且路径一致”不看版本就会误判为“已是最新”加载时又加载了旧文件导致各种诡异的行为异常。所以本地缓存建议在更新时进行清理或者组别管理。一种实现方式是每个热更版本一个缓存目录更新时切换目录并清理旧目录另一种方式是为每个缓存文件记录版本标签校验时先比对版本标签再比对哈希。第二种方式实现成本稍高但在大版本迭代频繁的项目里非常有必要。我自己项目里是采用“目录版本号”的方式即persistentDataPath/{version}/bundles/切换版本时直接换目录并触发旧目录清理。这看起来简单但极大地减少了脏缓存引发的错误。而且清理旧目录对安全也有帮助——旧版本可能存在已知的攻击面和新漏洞及时清理能减少旧文件被利用的机会。4.4 加载阶段的绕过风险与注入防护再往后走就是你真正调用 AssetBundle 加载的地方了。这个阶段的攻防容易被忽视但其实很关键。如果你在加载时用了相对路径或者允许从任意本地路径加载 Bundle攻击者可能会把恶意 Bundle 放在某个能访问的位置并伪造一个加载路径让游戏加载它。防这种问题最直接的办法是只允许从你指定的缓存目录和清单注册的路径加载加载路径必须经过白名单校验。Unity 的AssetBundle.LoadFromFile支持路径加载也支持LoadFromMemory。如果是用内存加载攻击者还可以通过注入 DLL 之类的办法替换内存内容这已经超出了常规热更新安全的范畴更多要靠运行时防护和反外挂方案解决。所以本地缓存再严也只能覆盖到加载前的静态数据层面。5. 实战排查实录从 CDN 清单到本地缓存的一次完整复盘5.1 排查工具与关键埋点设计下面我分享一个比较典型的排查过程场景是线上反馈“某版本部分玩家更新后加载资源报错校验失败”。面对这种问题我习惯用一套相对固定的排查流程第一步是确认埋点是否完整。排查热更新链路建议至少记录这么几类日志客户端拿到清单的时间点和清单版本清单中记录的每个 Bundle 的期望哈希CDN 下载完成时本地计算的实际文件哈希加载前校验时的本地文件大小和哈希校验失败时的错误码和失败路径设备本地缓存的目录路径和文件数量如果这些日志都有排查起来会非常有方向感。如果缺埋点我会先补上埋点再让玩家提供复现信息。这一步不能省盲猜链路问题太容易误判了。5.2 三步定位法从云端到客户端的逐层对照我总结了一个“三步定位法”可以快速定位资源更新异常发生在哪一层第一步直接到 CDN 拉一次目标 Bundle计算它的哈希值对比清单里记录的期望哈希。这里我会用 curl 拉文件然后本地计算哈希。如果 CDN 的哈希和期望不一致说明问题出在发布侧或 CDN 缓存侧。curl -o test_001.bundle https://yourcdn.com/hotupdate/bundles/1.3.0/001.bundle shasum -a 256 test_001.bundle第二步到客户端本地找到对应的缓存文件计算实际哈希对比 CDN 拉取到的哈希。如果客户端本地和 CDN 哈希一致但加载还是报错说明问题可能在加载逻辑或清单路径映射上。如果本地哈希和 CDN 不一致说明本地缓存已经被污染或者下载过程出了问题。第三步检查客户端实际加载时用的路径和清单记录的路径是否一致。这一步最常见的坑是旧版本客户端用了旧的路径规则但新 CDN 文件路径变了本地缓存里新文件和旧文件混在一起加载时定位到老路径自然校验不过。5.3 一份常见问题速查表我把经常遇到的热更异常问题整理成了一张速查表排查时对着看效率很高现象可能原因排查方向与解决建议校验总是失败重下也不行CDN 文件被污染或不正确对比 CDN 文件哈希和服务端发布哈希刷新 CDN 缓存后重下只有部分玩家更新失败当地本地网络节点缓存脏数据检查 CDN 节点缓存策略设置短缓存时间客户端做本地缓存清理下载成功但加载报错本地缓存有旧版本残留检查加载路径是否指向旧版本目录启用按版本分目录策略清单能拿到但 Bundle 下载被拦截CDN 鉴权配置没生效或终端网络环境检查 CDN 链接签名时效确认 TLS 与终端网络代理是否拦截校验失败日志显示本地文件大小为 0下载中断或磁盘空间不足检查磁盘空间下载过程增加断点续传和重试机制玩家用修改工具改本地文件后闪退本地缓存被恶意篡改确认校验逻辑覆盖加载前启用签名校验配合行为校验5.4 一次具体问题的处理过程记录之前项目上遇到过这么一次问题版本 1.3.0 上线后大概 5% 的玩家反馈进游戏加载资源很慢经常白屏而且在 Wi-Fi 和 4G 网络下表现不一样。一开始以为是 CDN 节点问题但查了下 CDN 日志发现请求量并不异常。顺着链路排查后发现了问题这个版本调整了更新检查接口的返回内容新下发的清单指定了一个新的 CDN 路径前缀但老版本客户端内置了一个旧的路径前缀。新玩家落在干净环境里没问题老玩家本地缓存还留着旧路径的文件更新逻辑却把新旧路径混在了一起造成部分资源永远下载不齐。这个问题本质上不是被篡改而是版本演进时的兼容性问题。但从排查过程看和“从 CDN 清单到本地缓存”的思路完全一致先确认清单下发是否正确再确认 CDN 资源是否可达最后确认本地缓存是否干净。如果这三级校验都有日志兜底定位起来不会花多少时间。6. 我在实际排查中积累的一些经验心得排查过几轮热更安全问题之后我最直观的感受是安全排查往往不是在找“哪里有黑客”而是在找“哪里信任了不该信任的东西”。比如你信任 CDN 一定返回正确文件结果 CDN 缓存坏了你信任清单一定没有被改结果清单接口没签名你信任本地缓存一定和清单一致结果旧版本残留了脏数据。每一层信任关系都是一个潜在的攻击面或故障点。热更新安全的本质就是把每一层信任都变成验证。所以我现在做新项目会在设计阶段就定下热更链路的校验基线和日志埋点。具体来说有三件事第一清单必须签名且公钥不进热更资源第二资源文件下载后和加载前都做哈希校验第三本地缓存做版本目录隔离任何更新操作都记录日志。这三点做完至少能应对 90% 的常见热更安全问题和大部分资损事故。如果你正在排查一个热更问题我的建议是不要上来就怀疑别人篡改。先老老实实跑一遍“CDN 哈希 → 本地哈希 → 加载路径”的对照流程大多数问题都会自动现形。等你把这条链路所有验证点都完善了热更安全也就不再是一个让人心里发虚的话题了。
返回列表