你造保险柜,我直接搬走:视频号WASM加密被“降维打击”的分析于防范

你造保险柜,我直接搬走:视频号WASM加密被“降维打击”的分析于防范

之前在 朋友说视频号下载了全是乱码,我看了前八个字节说:没坏 聊到视频号文件的破解,今天从视频号角度,设想下防范升级的措施。

视频号的加密策略是这样的:

它用Isaac64,一种密码学伪随机数生成器,生成一串密钥流。然后对视频文件最开头的128KB,精确到131072字节,做XOR异或混淆。128KB之后的所有数据,原封不动,一个字节都不改。

每个MP4都长这样
每个MP4都长这样

就这?

对,就这。

我第一次看到这个方案的时候,反应是???就加密128KB?后面几十兆的视频数据全是明文,这不是形同虚设吗?

但在仔细一想,我服了。

这里有个关键背景。视频号为了在手机上实现秒开,它的MP4文件一定是Web优化过的,也就是Fast Start格式。Fast Start的意思是,moov这棵元数据树被挪到了文件最前面,紧跟在ftyp之后。

也就是说,文件最开头的128KB,刚好完整覆盖了ftyp和整个moov树。

moov是什么?是整棵播放导航树。每一帧视频在文件的什么位置、多大、什么时间戳,全存在moov里。没有moov,播放器就不知道去哪里找每一帧。

视频号只加密128KB,就等于把整棵导航树砍掉了。

后面的mdat,几十兆的H.264或HEVC原始帧,全是明文又怎样?你拿到了文件,VLC打不开,QuickTime打不开,系统相册打不开,任何常规播放器都打不开。因为它们全都不认识这个文件了,不知道每一帧在哪里,不知道视频有多长,不知道从哪里开始解码。

用128KB的加密代价,废掉了所有第三方播放器。

四两拨千斤。

我当时就觉得这个设计太精妙。它完全不在乎你能不能看到原始视频字节,它只在乎你能不能找到那些字节。就像一本书,目录页被涂黑了,正文一页没少,但你翻不到你想看的那一章。

而且对微信自己来说,解密成本极低。播放的时候,只需要在内存里把前128KB异或还原,拼上后面现成的明文数据,直接喂给系统解码器就行。手机端几乎零额外功耗。

这个设计聪明在哪?聪明在它精确地找到了那个「四两」的位置。不是加密整个文件,不是加密视频流,而是只加密那个让所有播放器都能工作的「目录页」。

回到主线。这个设计虽然聪明,但有一个致命的破绽。

还记得开头说的那8个字节吗?

每个MP4文件,前8个字节永远不变。4字节size,加4字节ftyp。66 74 79 70,就是ftyp的ASCII码。

在密码学里,这叫已知明文。

说到已知明文,有个特别经典的历史案例。二战的时候,英国人破解德军的Enigma密码机,最关键的一步就是利用crib,也就是已知明文。德军每天早上发天气预报,天气预报的开头永远是WETTER,德语的天气。英国人知道这个,就拿着密文和已知的明文去反推当天的密码设置。

ftyp就是MP4世界的WETTER。

视频号对文件头部做XOR混淆,等于把自己的命门递到了逆向工程师手里。因为不管你的密钥流多复杂,不管你的Isaac64算法多精妙,逆向工程师根本不需要去理解算法本身。

他们只需要做一件事。

拿密钥流去异或密文的前8个字节。如果输出结果里出现了ftyp这4个ASCII字符,就说明密钥流对了。算法攻破了。

这8个字节构成了一个完美的校验器。你不需要看懂视频编解码,不需要理解加密算法,不需要调试任何东西。ftyp出现就是对了,没出现就是错了。一个天然的Oracle。

我在cn-sec上看到一篇逆向记录,作者的分析过程特别直白。他对比了加密文件和正常MP4的文件头,正常MP4开头是00 00 00 20 66 74 79 70,加密后变成了一堆乱码。确认加密。然后从那里开始追解密函数。

Evil0ctal的项目里更直接,解密完成后第一步就是校验第4到第8个字节是不是ftyp,确认解密成功。

你看,格式规范送给逆向工程师的免费crib,视频号一个都没躲掉。

但这里有个更有意思的点。

视频号的加密核心是一个WASM模块,wasm_video_decode.wasm。Isaac64算法就跑在这个WASM里。你以为逆向工程师会去反编译这个WASM,一行行读那些底层的指令,还原算法状态机,然后用Python重写一套?

没有。

几乎没有人这么干。

我一开始还想着,这WASM反编译一下不就行了?后来了解到WASM反编译出来的控制流图像迷宫一样,我立刻放弃了这个念头。愚钝如我,硬刚算法的投入产出比太低了,人家专业搞逆向的当然更不会这么干。

他们把WASM当黑盒用。

不需要懂原理,只需要会调用
不需要懂原理,只需要会调用

这个思路特别像…你不需要知道微波炉怎么工作的,你只需要知道怎么通电、按哪个按钮,饭就热好了。

具体操作大概两种路子。

第一种,环境模拟。WASM文件和一段JS胶水代码一起工作。攻击者把这两个文件整个下载下来,在Node.js里用JSDOM伪造一个假环境,骗过WASM的环境检测,然后直接调用暴露出来的解密函数,传入decode_key,WASM就乖乖吐出密钥流。

第二种,更暴力,RPC注入。如果WASM对环境校验特别严,本地模拟搞不定,那就直接寄生在真实的微信客户端上。往PC版微信注入一段脚本,在真实的JS上下文里架一个微型WebSocket服务器。外部的下载器抓到加密视频后,通过WebSocket把参数发给注入的脚本,脚本调用正在运行的真实WASM解密函数,拿到明文后再传回来。

这种降维打击让防守方所有的环境校验全部失效。因为调用确实发生在一个绝对真实的合法环境里。

我看到这段的时候,一时间无语凝噎。

你花了那么大精力做WASM混淆,做环境校验,做算法封装,结果人家根本不看你里面写了什么,直接把你当API调。

这就像你给保险柜装了一把最精密的锁,结果小偷不撬锁,直接把整个保险柜搬走了。

锁匠再厉害,也防不住人家把你当工具人。

顺着这个思路,我就在想,如果我是视频号的工程师,我会怎么改进?

想了几个方向,跟你们聊聊。

第一个,也是最直接的,干掉那个已知明文的命门。ftyp之所以是crib,是因为它永远在文件开头第0个字节。那我在加密之前,先在文件最前面塞一段随机长度的垃圾数据,1到1024字节随机,然后再一起做XOR。微信自己的播放器知道怎么跳过这些垃圾(比如把随机长度编码在decode_key里下发),但攻击者拿到密文后,就不知道ftyp在第几个偏移量上了。校验器直接报废。

第二个,给WASM加上下文绑定。现在WASM只接收decode_key作为输入,太容易模拟了。如果WASM同时读取当前JS运行时的调用堆栈,或者只有用户真实交互才会产生的数据(比如鼠标轨迹哈希、微信内部内存指针),在Node.js里调用的时候这些参数就不匹配,解密直接输出脏数据。

第三个,不暴露明文密钥。现在攻击者调用WASM,WASM返回密钥流或解密后的数据,JS层面能直接拿到。如果改成WASM的输出直接灌入底层解码器的内存里,JS层面完全拿不到返回的数据,就像Widevine DRM那套逻辑一样,攻击者就算调用了WASM也拿不到密钥流。

第四个,动态轮换。每次发版,或者每隔24小时,通过后端的WASM混淆编译器生成一个全新的wasm文件,控制流完全不同,函数名全部打乱,连加密常数都变异。攻击者昨天刚写好自动化脚本,今天微信热更新了WASM,脚本瞬间失效。

第五个,一次性Token。现在解密可能过度依赖视频本身的固有属性。如果后端下发的用于生成Isaac64种子的Token,跟当前Session ID和时间戳绑定,只有5分钟有效期,攻击者用爬虫抓到旧Token传给WASM也解不了密。逼着他们必须实时抓包实时解密,批量盗版的效率大幅下降。

这五个方向,看着挺全面。但我跟你说个更有意思的事。

这五个改进,没有一个改变了视频文件本身。

文件必须是一模一样的
文件必须是一模一样的

不同客户端,旧版安卓微信和新版iOS微信,下载到的依然是完全相同的那个加密视频文件。

为什么?因为CDN。

如果每个客户端版本、每个用户拿到的加密文件都不一样,CDN的缓存命中率会瞬间跌到0。服务器带宽成本暴增百倍。所以无论用户是谁、用什么版本,CDN下发的加密MP4必须是同一份。

这五个改进,全是在「钥匙」和「锁匠」上做文章,没动「锁芯」。

WASM高频轮换,是给锁匠换衣服。版本A和版本B的WASM内部代码完全不同,但输入相同参数算出来的密钥一样,新老客户端都能解开同一个文件。

环境上下文绑定,是查锁匠的身份证。只在WASM运行的瞬间检查环境,视频文件本身没变。

Session Token绑定,是一次性提货券。服务器验证你是合法用户后给你发Token,WASM用Token算出钥匙,解开的还是那个所有人都一样的文件。

这个设计背后的逻辑,其实是一个很经典的博弈。

你要让CDN高效分发,文件就必须通用。你要让文件通用,就不能把安全绑死在文件上。那安全绑在哪?绑在钥匙的获取上,绑在锁匠的身份验证上。

这让我想到一个东西。

广播加密。

信号是公共的,钥匙是私有的
信号是公共的,钥匙是私有的

数字电视的信号是同一个频率广播出去的,所有人收到的信号一模一样。安全不在信号上,在你的智能卡上。你的智能卡能解密,你就能看;没有卡,你收到信号也是一堆噪声。

视频号干的是一模一样的事。加密视频是广播信号,decode_key是智能卡,WASM是卡里的解密芯片。

这个架构的精妙之处在于,它把「内容分发效率」和「版权保护」解耦了。CDN只管高效分发同一份文件,安全层只管控制谁能拿到钥匙。两层各干各的,互不干扰。

但这个架构的脆弱之处也在这里。一旦钥匙的获取机制被攻破,所有人都完了。因为文件是同一份,一把钥匙开所有的锁。。。

视频号现在的方案,钥匙的获取机制确实被攻破了。WASM被当黑盒调用,decode_key能从API响应里抓到,ftyp给了完美的校验器。开源项目都做出来了,在线解密工具都上线了。

那五个改进方向,本质上都是在加固钥匙的获取机制。随机填充干掉校验器,上下文绑定防黑盒调用,不暴露密钥防数据外泄,动态轮换防自动化脚本,Token绑定防批量抓取。

每一个都在抬高攻击者的成本,但没有一个能彻底堵死。因为只要文件是通用的,只要WASM最终要输出解密能力,就总存在被调用的可能。

这是「通用文件 + 私有钥匙」架构的宿命。

你可以在钥匙上层层加锁,但你永远无法阻止一个有足够动机的攻击者去想办法拿到钥匙。

说真的,我觉得这个事特别有意思的地方不在于谁赢谁输。而在于这个设计本身展现出来的那种工程美学。

128KB,四两拨千斤,精确地找到了那个最小加密面。

反常规,不动mdat保moov的惯例,反着来,动moov保mdat,因为它不需要讨好任何第三方播放器。

WASM黑盒,用算法封装换工程隔离,虽然被当API调了,但思路是对的。

CDN和DRM解耦,文件通用钥匙私有,把分发效率和版权保护分到两层各干各的。

每一步都是权衡。每一步都在「安全性」和「性能/成本」之间找那个最优的平衡点。

虽然最后被破了,但被破的方式也很体面。不是设计有漏洞,是架构的固有局限。就像再好的保险柜也防不住搬走整个柜子的人。

我有时候觉得,做安全的人和做逆向的人,其实是同一枚硬币的两面。一个在想办法堵,一个在想办法绕。双方都在对方的思路里学到东西。视频号的工程师选择了最小成本最大效果的加密面,逆向工程师选择了最小成本最大效果的攻击面。两边都在做同一件事,找那个四两拨千斤的支点。

只不过一个找到了128KB,一个找到了8个字节。

还记得开头那8个字节吗。

66 74 79 70。ftyp。

它不是视频号设计的漏洞,它是MP4格式规范写死的保证。每一个MP4文件,从诞生起就带着这8个字节的胎记。视频号的所有聪明设计,最终都败在这8个字节上。

这大概就是规范的代价。你选择了一个通用格式,就继承了它所有的确定性。而确定性,在安全领域,永远是攻击者最好的朋友。

二战时的WETTER是这样,今天的ftyp也是这样。

格式不变,crib不死。

/ 注意,本项目仅在 SyncMein 的500人小群分享。

/ 谢谢你看完了我的文章,我们下次再见吧。

/ 作者:明察 / 投稿或爆料,请联系邮箱:mingcha@gqmg.com

– END –

© 版权声明
THE END
喜欢就支持一下吧
点赞15赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容