
1. 热更新链路全景一份AssetBundle从CDN到本机要过几道门聊Unity热更新安全排查先得把链路拉直了看。很多人一上来就盯着AssetBundle文件本身其实真正出事的地方往往在链路的两头——CDN清单和本地缓存。我见过不少项目AB包加密做得花里胡哨结果清单文件裸奔在HTTP上任意人都能改版本号、换下载地址整个热更新体系跟没设防一样。先把热更新的完整路径梳理一遍客户端启动后第一步请求CDN上的版本清单Manifest拿到当前线上所有AssetBundle的文件名、哈希值、版本号和下载地址第二步拿这份清单跟本地已缓存的记录做比对找出需要新增、更新或删除的文件第三步按清单里的URL逐个下载AssetBundle到本地缓存目录第四步加载AB并实例化资源。这四步环环相扣任何一环被突破后面的环节都跟着遭殃。在这条链路上有几个关键信任边界值得单独拎出来说清单端的信任边界客户端默认“CDN返回的清单是可信的”这是第一个信任假设。如果清单本身没做签名校验那么后续的一切校验都可以被绕过——攻击者直接改掉清单里的哈希值和URL让客户端去下载他构造好的AB包就行。传输层的信任边界下载AB时默认“HTTP响应内容就是服务器真正发出的内容”。在明文HTTP下中间人完全可以替换响应体即使是HTTPS如果客户端没做证书校验也一样能被抓包工具中间人。本地缓存的信任边界加载AB时默认“缓存目录里的文件没被人动过”。这个假设在Android上尤其危险因为外部存储目录/sdcard/Android/data等的访问权限比应用沙箱内部松得多很多机型上其他应用甚至用户本人都可以直接读写。我把这些信任边界画成了一张排查地图后面所有章节都按这条链路推进。搞清楚每个环节默认信任了什么才能知道该在哪儿加固。2. CDN清单被篡改我排查时最先盯上的环节2.1 清单文件里最容易被动手脚的核心字段版本清单在Unity热更新工程里通常是JSON或自定义二进制格式无论什么格式核心字段基本逃不出这几类文件ID或相对路径、MD5或CRC哈希、文件大小、版本号、下载URL。排查看的就是字段之间的依赖关系是否严密。我自己排查过的项目里最典型的薄弱点是“哈希字段存在但没被客户端校验”或者“哈希字段只参与增量比对不参与下载后校验”。什么意思呢客户端拉取清单后用哈希值判断本地文件是否需要更新这个环节确实用了哈希但文件下载完成后有些实现图省事直接按清单里的URL加载AB压根不重新算一遍本地文件的哈希去对账。这样一来哈希的作用退化成“比对版本号”攻击者只要同时改掉清单里的哈希和构造好对应的AB文件客户端就会乖乖加载被替换的内容。另一个容易忽略的字段是URL。很多团队把CDN完整URL直接写死在清单里客户端拿到URL就下载。如果清单没签名攻击者把URL换成自己的服务器地址同时把哈希改成自己文件的哈希整个下载流程就成了“从攻击者服务器拉取任意AB包”。2.2 用抓包工具完整复现“清单被串改”的链路排查清单安全性的第一步是实际做一次抓包验证别只在代码里翻逻辑。我习惯用Charles或Fiddler这类代理工具把客户端流量引过来模拟一次完整的热更新请求。操作步骤大致是这样手机关闭代理或走直连先正常触发一次热更新抓到正常的清单请求URL和响应体留作基线。开启代理工具的断点功能Breakpoint拦截清单请求的响应。手动修改响应体里的某个AB包哈希值同时把下载URL替换成本地起的一个HTTP服务地址我用Python的http.server起过临时服务方便验证。放行响应观察客户端行为。正常情况下如果客户端健壮应该检测到清单校验失败并中断热更新流程。但如果像我排查的那个项目一样客户端直接按新URL去下载那问题就实锤了——清单环节没有任何签名和可信校验。这里有个细节要提醒抓包工具在Android 7.0以上系统抓HTTPS流量需要在手机上安装并信任代理证书且应用本身要允许用户证书networkSecurityConfig里配置。如果目标应用强制禁用了用户证书那就没法用代理抓HTTPS需要改用基于VirtualApp的双开注入或Frida hook等方案。这一块如果项目里没有专门的安全测试同事建议尽早和客户端同学确认一下应用的网络安全配置别到时候连排查都做不了。2.3 签名缺失与弱校验的代码识别信号在代码层面识别清单校验强度有一套快速判断方法。翻到拉取清单的代码问三个问题清单本身有没有数字签名常见做法是对整个清单内容做RSA或HMAC签名客户端内置公钥或密钥进行验签。如果清单里压根没有签名字段、客户端也没有验签逻辑直接判定为高风险。验签失败后是继续还是终止有些实现验签了但失败时只是打印一行warning后续流程照跑。这种“软失败”比没验签还坑因为排查时可能看到有验签代码就掉以轻心。我建议在代码评审时明确要求验签失败必须终止本次热更新流程并且要计数告警。签名密钥是否内置在客户端里如果用的是对称密钥做HMAC密钥藏在代码或AB包里反编译后就能拿到。虽然任何客户端内置密钥都能被逆向但起码要有难度门槛。RSA方案里公钥被逆向也无所谓私钥在服务端安全性明显高一档。3. AssetBundle完整性校验哈希比对里那些“等于没校验”的写法3.1 哈希算法与比对时机的工程选型AssetBundle下载后的完整性校验表面上是“算一遍哈希对一下清单”但工程实装时的差异非常大。哈希算法的选择上MD5已经不适合做安全校验碰撞攻击早就可行了至少要用SHA-256。CDN链路里如果不考虑性能极端敏感的场景SHA-256对几MB到几十MB的AB包计算耗时完全可接受——现代手机上算一个20MB文件的SHA-256也就几十毫秒级别跟下载时间比可以忽略不计。比对时机的选择才是重灾区。我见过三类做法下载完立即比对文件写入磁盘后马上算哈希跟清单对账失败则删除重下。这是最标准做法。加载前比对每次要加载这个AB时才算哈希好处是能防止“首次校验通过、后续文件被替换”的情况坏处是加载路径多了计算开销。只在版本比对比对就是我前面说的“用哈希做增量更新判断”下载后不再校验。这种等于没有完整性校验。我倾向的做法是“下载后立即校验 加载前对关键AB再做一次校验”。第二个校验可以用更轻的方式比如记录第一次校验的修改时间或文件长度加载前快速比对不必完整算哈希。游戏里频繁加载场景对耗时敏感完整哈希每次加载都算不太现实这种折中方案更落地。3.2 三类“假校验”代码的识别特征排查存量项目时识别假校验比想到校验方案更重要。特征是代码层面一眼能看出来的第一种只比对文件大小。清单里有哈希但下载后校验逻辑只检查file.Length expectedLength。这种校验形同虚设攻击者随便构造一个同大小的文件就能过。第二种比对部分字节。比如只取文件前1KB算哈希或者只比首尾若干字节。对AB包这种二进制格式攻击者只要保证被替换文件的首尾字节与原始一致中间内容随便改。Unity的AB包头部有固定格式尾部有对齐数据攻击者完全可以构造出首尾一致但中间payload完全不同的AB。第三种校验范围选错。有些AB包是LZ4压缩的有些是不压缩的原始格式还有AB包在传输时可能经过了一层外层加密或自定义打包。如果校验哈希时用的是“解密前”或“解包前”的数据跟清单里的哈希对不上就会导致校验永远失败最后只能被迫跳过校验。这会倒逼开发者把校验逻辑悄悄改成“只警告不阻断”等于自废武功。3.3 动态篡改AB内容的实际检测思路除了静态的哈希机制还有一些动态场景值得加一层防护。比如Android真机上外部工具可以直接修改沙箱内文件Root过的设备又比如有些外挂框架会在AB加载时hookAssetBundle.LoadFromFile在内存里动态替换AB数据。静态哈希对这类动态篡改是无效的因为它发生在哈希校验完成之后。针对内存层篡改常规应急方案是定期对AssetBundle.LoadFromFile等关键API做完整性检查比如hook检测、方法调用栈检查看调用者里有没有可疑的注入库。这个做起来复杂度不低一般项目未必需要。我的建议是分优先级先把链路基础和静态校验做扎实动态篡改防护放到量级足够大、确实被攻击者盯上之后再加不要上来就搞一套复杂到团队维护不了的东西。4. 本地缓存区权限、一致性、回滚攻击三座大山4.1 缓存目录权限设置与沙箱边界本地缓存的排查第一件事就是看文件落在什么位置。Unity引擎在Android上习惯用Application.persistentDataPath作为默认持久化目录这个目录在Android上通常对应/data/data/包名/files属于应用私有目录其他普通应用无权访问。但如果项目里手动指定了缓存路径比如为了“方便排查”把AB包放到了/sdcard/Android/data/包名/甚至/sdcard/Download/下那权限边界就完全打开了。尤其要警惕/sdcard/这种公共存储。在Android 11以前公共存储目录的读写权限管理宽松任何应用只要申请了存储权限就能枚举和改写其他应用放在公共目录里的文件。热更新AB包如果直接落到公共存储攻击者的替换成本极低——不需要root不需要特殊漏洞普通文件管理器就能操作。iOS这边的沙箱相对严格Documents和Library/Caches目录都有ACL保护但越狱环境以及通过macOS上的备份工具iMazing等仍能导出沙箱文件。这更依赖“文件加密 校验兜底”两道防线纯粹的目录权限在iOS上并不能解决所有问题。我的排查标准是Android上缓存目录只允许Application.persistentDataPath或filesDir这类应用私有目录iOS上资产放Library/Application Support或Caches并做好苹果备份排除逻辑备份文件被拖出来后未加密的AB等于直接泄露。4.2 缓存与远端清单的版本一致性对账本地缓存里常见的第二个隐患是脏缓存不清理。Unity的Caching系统有自己的校验机制但自定义热更新框架里缓存与远端版本的一致性往往靠客户端自己维护。最典型的问题场景用户上次更新到版本2本地缓存了版本2的AB服务器随后发布版本3用户下载完版本3的部分AB后中断了网络本地出现版本2和版本3混存的局面。如果本地清单文件也处于“更新了一半”的状态下次启动时加载逻辑可能把版本2的资源误当成版本3来用。针对这种脏缓存问题务必要在两个维度做一致性控制清单版本号与文件版本号的绑定本地缓存记录里每条都要带上下发的清单版本号加载前先确认自己属于当前生效清单的版本不匹配就清理。原子化替换更新流程设计成“新版本文件先下载到临时目录全部完成后再切换”而不是边下边替换。这样即使下载中断旧版本文件还是完整的不会出现半新半旧。我在代码评审时看到过一个反模式更新逻辑把AB文件直接覆盖到正式缓存目录一边下载一边更新“已下载完成列表”。中途断网后已下载文件和列表对不上下次启动按列表加载时会导致崩溃。这类问题一般压测阶段暴露不出来真在线上出现时用户只能通过重装App解决体验非常糟糕。4.3 版本回滚攻击的原理与防护手段版本回滚攻击可能是本地缓存环节最隐蔽的问题。攻击原理是攻击者人为把客户端的本地版本记录改小或者把缓存文件替换成老旧版本然后诱导客户端重新走一遍热更新流程。这听起来似乎只是多下载点数据但如果服务器上没有“拒绝旧版本客户端”的逻辑攻击者可以用这种方式把客户端回退到存在已知漏洞的旧版本再利用漏洞进行下一步操作。防护手段的核心是“服务端记录客户端的最低允许版本”。客户端上报自己的热更新版本号服务器发现版本低于阈值时强制客户端走全量更新或者直接提示升级App。这个逻辑必须放在服务端客户端自己校验是没意义的——攻击者可以同时伪造客户端上报数据。本地缓存的安全性也有直接关系如果攻击者无法轻易篡改缓存目录那么版本回滚攻击的操作成本会高很多因为得先绕过本地校验才能伪造版本号。Android上把缓存放对目录、做好完整性校验回滚攻击的入口就已经堵住了大半。5. 完整排查实战从抓包到修复的一套流程5.1 搭建最小化的排查环境真刀真枪排查时环境搭得好不好直接决定效率。我每次做热更新安全排查都会准备一套固定组合一台已开启USB调试的Android设备最好是能root的备用机方便看目录、改权限、模拟攻击。本机起一个HTTP服务用来模拟恶意CDN端点。Python一行命令就能搞定python3 -m http.server 8080把构造好的AB包放在这个目录下。一个抓包工具手机端配好代理。我之前在Charles和Fiddler之间换过多次现在习惯用Charles因为断点修改响应体的交互体验更顺手。一个能随时修改客户端热更新配置的入口比如在App里内置一个内部调试面板或者直接改本地配置文件。没有调试入口的话每次验证都要重打包效率太低。这些工具合在一起可以模拟出链路里最核心的几个攻击场景清单被篡改、传输被替换、本地缓存被手动修改。整套环境搭好大约需要半小时但对后端验证非常值。5.2 分步执行的安全排查操作我按链路顺序走每个环节都有明确的通过/不通过标准。第一步验证清单请求与响应。正常触发一次热更新抓取清单请求。看几点请求是否走HTTPS响应体里是否包含签名字段客户端代码里是否有验签逻辑验证失败的行为是什么。这一步的通过标准是清单使用HTTPS传输、带签名、验签失败直接终止更新流程。第二步篡改清单测试。用代理工具断点修改清单里的哈希值和URL指向本地恶意服务。客户端如果出现“接受修改后的URL并下载文件”的行为直接记为高风险问题。第三步篡改AB文件测试。保持清单不变只修改下载返回的AB文件内容比如替换成另一个合法AB或改几个字节制造损坏观察客户端是否会加载异常文件。通过标准是下载后哈希校验能识别出文件不一致并删除重下或提示失败。第四步本地缓存篡改测试。用root权限直接改掉本地缓存目录里的AB文件或修改本地版本描述文件。然后触发热更新或直接启动游戏看客户端能否检测到缓存异常并自动修复。通过标准是检测到文件与清单记录不一致后能重新拉取正确版本。整个排查流程走完基本能把“清单-传输-缓存”三个层面的主要漏洞暴露出来。我实际跑过的项目里四步全部通过的几乎没有通常能在第一步或第二步就发现1-2个实质性问题有的问题在代码里看半天都看不出来一抓包马上现形。5.3 结合项目记录我踩过的具体问题举一个我印象最深的案例有个合作项目做的是MMO类型的游戏热更新框架是团队早期自己搭的。排查到第二步时发现客户端拉取清单用的是UnityWebRequest但这哥们没配证书校验直接在URL里写死了一个http地址。清单明文传输CDN在海外回源还不稳定于是他们加了一套“本地清单兜底”——如果远端拉取失败就先用上一次缓存的清单。听起来很合理对吧问题出在兜底逻辑上本地缓存的清单文件本身没有签名、没有有效期、没有来源标记攻击者只要篡改本地清单的URL字段客户端下次启动时就会自动去攻击者服务器拉取AB。修复方案其实不难给远端和本地清单都加上同一套签名机制验签失败不做兜底而是终止更新并清掉异常清单文件。就这么简单一个问题在线上藏了很久不抓包真的发现不了。6. 修复方案落地的先后顺序与上线前的自测清单6.1 按风险等级确定加固优先级排查之后面临的问题往往是一堆缺陷一起暴露不可能也没必要一次性全部修复。我习惯按风险等级排序用最少的改动先把最大的洞堵上。排序标准就三条被利用的难度、被利用后的影响范围、修复成本。我推荐的落地顺序是先堵清单验证缺口给清单加签名RSA或HMAC客户端验签失败直接终止热更新流程。这一步修复成本不高效果却是最明显的——一旦清单可信了后续URL和哈希值的可信度才立得住。再补传输链路把热更新请求全部切到HTTPS并在客户端做好证书校验至少校验证书链。这一步对国内多数CDN厂商来说没有额外成本只是配置问题。然后完善下载后校验确保每个AB文件下载后都执行SHA-256比对失败重下。最后治理本地缓存清理缓存目录权限、确保版本原子切换、增加服务端最低版本限制。有的团队做反了一上来就折腾AB包加密、加壳、加固把精力和预算花在最容易被绕过的层面反而忽略了清单和缓存这两个真正能左右全局的地方。这种“大炮打蚊子”式的加固实际效果并不好。6.2 加固过程中容易踩到的二次事故修复过程本身也会埋新坑我在好几个项目里都见过类似问题。加签名时最常见的坑是密钥管理混乱。RSA私钥直接放在代码仓库里或者多个团队共用一个密钥泄露了都不知道。建议私钥单独管理只有服务端团队能访问客户端只内置公钥。如果早期用的是HMAC对称方案至少要做到不同环境测试/正式用不同密钥避免一个测试环境密钥泄露导致正式环境遭殃。加HTTPS时也常有项目踩坑CDN服务商的证书配置不全或客户端的证书校验代码写得太死例如固定校验某个特定证书的指纹导致证书轮换时线上直接挂掉。我的建议是客户端校验证书链的有效性即可不要固定单一公钥指纹除非团队有非常成熟的证书轮换流程。切换哈希算法时要注意拉取“增量更新”功能的兼容性。如果老版本客户端已经上线新版本清单里的哈希从MD5切成SHA-256需要保证老版本客户端要么能兼容、要么会被强制升级不然可能出现“老客户端每次都全量更新”的线上问题。6.3 上线前自测清单与长期监控建议修复完之后我习惯把排查过程中的所有测试用例沉淀成一份自测清单每次热更新版本发布前都跑一遍。清单长这样清单签名缺失 / 篡改时客户端是否终止更新并告警。清单正确、AB下载正常时更新流程是否顺利走完。AB下载后内容被篡改时客户端是否检测到哈希不匹配并重新下载。本地缓存文件被手动替换时客户端能否自愈。本地版本被改小模拟回滚攻击时服务端是否拒绝或发起强制更新。弱网 / 断网场景下是否存在半更新状态污染缓存目录的问题。老版本客户端在服务器发布新清单后是否出现异常更新行为。长期监控方面我建议在热更新链路的关键节点埋点清单拉取失败次数、验签失败次数、AB下载校验失败次数、本地缓存自愈次数。这些指标平时看着不起眼一旦有玩家反馈“黑屏”“白屏”“一直闪退”这些数据就是你判断是否遭遇恶意攻击的第一手线索。我个人体会是热更新的安全排查不是一次性工作而是一个需要反复验证的持续过程。游戏版本迭代快CDN策略变动多一套方案用半年可能就出现新的盲区。每迭代一个版本把上面这套流程走一遍成本并不高但能避免很多线上翻车事故。