我研究了一下微信视频号下载视频的方式。我在 github 上发现一个挺好的解决方法 (WeChatVideoDownloader),原理是利用代理获取视频地址。但是最近出了点问题,微信对视频号做了一点更新,对视频进行了一点小小的加密,导致下载下来的视频无法直接打开。

什么,加密,这能忍?

准备工作

在正式开始逆向之前,我们首先需要能够在微信视频号中打开开发者工具,由于微信默认肯定是不会启用的,所以我们要对微信的某个动态链接库进行小小的修改。

总之就是找到 xweb-enable-inspect 这个启动选项,修改 branch 指令,这个启动选项所在的分支变成永远执行就行了。

最后实现效果如图下

逆向

Javascript 初步分析

首先随便打开一个视频,我们可以看到很多请求。其中带有 stodownload 的就是下载的视频文件,但这些视频链接下载下来的内容是加密的。

先看一下加密前的视频文件头,我们可以明显发现,它的文件头格式并不正确。

对比之下,一个正常的 mp4 文件的文件头应该如下所示:

那么确认了文件被加密。那么我们要从哪里开始呢。因为解密必然是文件下载完成后才解密的。所以解密的函数或者过程很有可能就在文件下载完成后。

查看请求是从哪行代码发起的,我们可以追踪到 worker_release.js 中的 g.send()

这个时候,写过 Javascript XMLRequest 的人可能就很熟悉这个了,在完成所有 callback 设置之后,发送请求用的就是.send(),所以往上翻,我们可以找到如下的返回值处理。

这里我们可以发现解密函数就是函数 M,参数分别为数据和 startIndex (也就是文件的第几个 byte)

函数 M 非常的简单易懂,把数据和 decryptor_array 进行异或即可。如果当前的 startIdx 大于 decryptor_array 的长度,则不进行异或,不改变原有数据。

如果我们在这个函数 M 的地方打个断点,我们可以发现这个 decryptor_array 的长度实际上是一个常量 2^17 = 131072 (一直都是这个长度)

从这里我们可以推断出,decryptor_array 的长度是有限的。

我们从 decryptor_array 的恒定长度可以推断出,视频加密只作用于文件的前 131072 字节。这样的加密策略似乎合理 —— 如果需要对整个视频数据进行加密和解密,那么播放视频时消耗的资源可能会显著增加。

(虽然 DRM 好像就是全文加密的,我也不太了解就是了)

另外,我们还发现,对于同一视频,decryptor_array 是一致的。不同的视频文件则对应不同的 decryptor_array。这表明 decryptor_array 是通过某种特定的方法生成或获取的。

经过搜索,我们了解到 decryptor_array 的赋值仅在 wasm_isaac_generate 函数中进行。

而 wasm_isaac_generate 函数在代码中只被一个地方调用,即 wasm_video_decode.js。

在 wasm_video_decode.js 中,wasm_isaac_generate 作为一个汇编函数,可以在 WebAssembly 中通过_emscripten_asm_const_int 接口被调用。

那么接下来,就要开始逆向可爱的的 wasm 了

WebAssembly 进一步分析

下载 wasm_video_decode.wasm 后,我们使用 wabt 工具将其转换为.o 文件,以便在反编译软件中进行分析。

完成这些步骤后,我们得到一个二进制文件 wasm_video_decode.o。将此文件拖入反编译软件,搜索_emscripten_asm_const_int 的调用。我们发现 wasm_isaac_generate 在函数 f378 处被调用。

进一步通过断点和调用栈的检查,我们发现 worker_release.js 中的 decryptor.generate() 最终触发了 wasm_isaac_generate 的调用。

仔细分析揭示出 decryptor 也是 WebAssembly 环境中的一个对象,即 WxIsaac64。

经过研究,我们了解到 Isaac64 实际上是一个随机数生成算法。

因此,我们可以合理推测:

1.decryptor 使用视频对应的 seed 进行初始化。

2.JavaScript 调用 decryptor.generate(),指示 wasm 在其内存中生成 2^17 即 131072 个随机数。

3.wasm 生成随机数后,通过 wasm_isaac_generate 将这些随机数写回 JavaScript,赋值给 decryptor_array。

现在,我们知道了 decryptor_array 的来源,接下来的问题是确定初始化 Isaac64 算法的 seed 的来源。

Tracebackbackbackbackback

接下来就是不停的打断点,看 call stack, 直到找到 seed 最早出现的地方就行了。

简单来说呢就是顺序就是从 FinderGetCommentDetail(objectid)->objectDesc.media.decodeKey->seed。

注入 WeixinJSBridge

那么 FinderGetCommentDetail 又是通过什么获取到信息的呢。继续追踪调用。可以发现 FinderGetCommentDetail 最后使用了 window.WeixinJSBridge.invoke 来获取数据。

window.WeixinJSBridge ???那接下來就要逆向微信的通信协议了。我才懒得逆向这玩意。

立刻启动后备隐藏能源,发动注入模式

总之结果很好,获得了需要的所有数据

总结

通过 FinderGetCommentDetail 获取到视频的 decode_key(就是 seed),url,title 等信息

通过 seed 生成 decryptor_array

通过 url 下载加密后的视频文件,把视频的加密段数据和 decryptor_array 做异或运算即可。

如何实现一个视频下载器

由于获取 seed 需要逆向微信协议,我不想在逆向这个协议上花费太多时间。

既然 WechatVideoDownloader 已经使用代理获取视频地址,我们可以进一步使用中间人攻击来获取视频链接及对应的 decode_key。

只需将注入 WeixinJSBridge.invoke 的代码插入到某个 JS 文件中,当微信客户端请求视频链接时,就把获取到的视频链接发送到本地服务器。

这样不仅解决了 seed 和链接的问题,连视频标题也能获取到。

最后,下载完视频后,通过 seed 生成解密序列并对视频进行解密。