微信视频号视频加密逆向

前言

大家好啊,我又回来写博客了。由于大四的选了几门超级难的课,导致没多少时间写博客了。(以上50%为真)

现在是2023年11月30日凌晨1点半,我来写点博客。

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

什么,加密,这能忍?

准备工作

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

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

最后实现效果如图下

Screenshot_20231130_014835

逆向

Javascript初步分析

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

Screenshot_20231130_015711

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

1
2
3
4
aynakeya @ ThinkStation]:~/workspace/weixinshipin
01:59:21 $ xxd -l 32 v2.bin
00000000: 75a2 b80f 5db2 528b af76 c5f0 9407 a7e9 u...].R..v......
00000010: 4c31 99a8 60ef a5de c64e ce1e 3ab1 6e74 L1..`....N..:.nt

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

1
2
00000000: 0000 0020 6674 7970 6973 6f6d 0000 0200  ... ftypisom....
00000010: 6973 6f6d 6973 6f32 6176 6331 6d70 3431 isomiso2avc1mp41

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

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

Screenshot_20231130_020505

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

Screenshot_20231130_020822

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

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

Screenshot_20231130_021050

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

Screenshot_20231130_021720

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

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

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

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

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

Screenshot_20231130_022300

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

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

Screenshot_20231130_022452

Screenshot_20231130_022810

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

WebAssembly 进一步分析

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

1
2
3
4
5
./path/to/wasm2c wasm_video_decode.wasm -o wasm_video_decode.c
cp /path/to/wasm-rt-impl.c .
cp /path/to/wasm-rt-impl.h .
cp /path/to/wasm-rt.h .
gcc -c wasm_video_decode.c -o wasm_video_decode.o

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

Screenshot_20231130_024028

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

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

Screenshot_20231130_024332

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

Screenshot_20231130_024924

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

  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最早出现的地方就行了。

Screenshot_20231130_030303

Screenshot_20231130_030340

Screenshot_20231130_030849

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

注入WeixinJSBridge

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

Screenshot_20231130_031821

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

(function () {
function wrapper(name,origin) {
console.log(`injecting ${name}`);
return function() {
let cmdName = arguments[0];
console.log(`${name}("${cmdName}", ...) => args: `);
console.log(arguments[1])
if (arguments.length == 3) {
let original_callback = arguments[2];
arguments[2] = async function () {
console.log(`${name}("${cmdName}", ...) => callback result (length: ${arguments.length}):`);
if (arguments.length == 1) {
console.log(arguments[0]);
}else {
console.log(arguments);
}
return await original_callback.apply(this, arguments);
}
}
let result = origin.apply(this,arguments);
return result;
}
}
window.WeixinJSBridge.invoke = wrapper("WeixinJSBridge.invoke",window.WeixinJSBridge.invoke);
})()

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

Screenshot_20231130_010450

Screenshot_20231130_010436

总结

  1. 通过FinderGetCommentDetail获取到视频的decode_key(就是seed),urltitle等信息
  2. 通过seed生成decryptor_array
  3. 通过url下载加密后的视频文件,把视频的加密段数据和decryptor_array做异或运算即可。

如何实现一个视频下载器

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

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

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

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

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

写在后面

回顾这次的逆向工程过程,我们可以看到WebAssembly在现代网络安全和逆向工程领域扮演着越来越重要的角色。随着WebAssembly的普及,JavaScript逆向逐渐演变为WebAssembly逆向。这不仅提高了代码的执行效率,同时也给逆向工程带来了更多的挑战和机遇。

Relevant Links

  1. 微信 v3.9.8.15
  2. wasm_video_decode.wasm v1.2.46
  3. worker_release.js v1.2.46
  4. wasm_video_decode.js v1.2.46
  5. wasm_video_decode_fallback.js v1.2.46

免责声明

本博客中提供的信息和过程仅供学习和研究目的使用。博主不鼓励、不支持并强烈反对任何形式的非法行为,包括但不限于未经授权的数据访问、破解或逆向工程。博客内容的使用应遵守相关法律法规以及道德规范。

读者在使用本博客中的信息时,应自行承担相应的风险和责任。博主不对由于使用、引用或依赖本博客中信息而产生的任何形式的损害或损失负责。此外,博主对于博客内容的准确性、完整性或适用性不作任何明示或暗示的保证。

请读者在使用本博客中的技术和信息时,始终保持合法、负责任的态度,尊重知识产权和隐私权。如果您不确定您的行为是否合法,或者您的行为可能会侵犯他人的权利,请在行动前咨询专业法律意见。