【软件逆向-微信视频号视频加密逆向】此文章归类为:软件逆向。
拿WxIsaac64(Isaac64的变种?)生成2^17个字节。然后和视频的前2^17字节做异或。
略去, 总之就是WeChatVideoDownloader不能用了。
原文链接:
在正式开始逆向之前,我们首先需要能够在微信视频号中打开开发者工具,由于微信默认肯定是不会启用的,所以我们要对微信的某个动态链接库进行小小的修改。
总之就是找到xweb-enable-inspect
这个启动选项,修改branch指令,这个启动选项所在的分支变成永远执行就行了。
最后实现效果如图下
首先随便打开一个视频,我们可以看到很多请求。其中带有stodownload
的就是下载的视频文件,但这些视频链接下载下来的内容是加密的。
先看一下加密前的视频文件头,我们可以明显发现,它的文件头格式并不正确。
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()
这个时候,写过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了
下载wasm_video_decode.wasm
后,我们使用工具将其转换为.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
处被调用。
进一步通过断点和调用栈的检查,我们发现worker_release.js
中的decryptor.generate()
最终触发了wasm_isaac_generate
的调用。
仔细分析揭示出decryptor也是WebAssembly环境中的一个对象,即WxIsaac64
。
经过研究,我们了解到Isaac64实际上是一个随机数生成算法。
因此,我们可以合理推测:
decryptor.generate()
,指示wasm在其内存中生成2^17即131072个随机数。wasm_isaac_generate
将这些随机数写回JavaScript,赋值给decryptor_array
。现在,我们知道了decryptor_array
的来源,接下来的问题是确定初始化Isaac64算法的seed的来源。
接下来就是不停的打断点,看call stack, 直到找到seed
最早出现的地方就行了。
简单来说呢就是顺序就是从FinderGetCommentDetail(objectid)
->objectDesc.media.decodeKey-
>seed
。
那么FinderGetCommentDetail
又是通过什么获取到信息的呢。继续追踪调用。可以发现FinderGetCommentDetail
最后使用了window.WeixinJSBridge.invoke
来获取数据。
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
|
(
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);
})()
|
FinderGetCommentDetail
获取到视频的decode_key
(就是seed
),url
,title
等信息decryptor_array
decryptor_array
做异或运算即可。总之为了让WechatVideoDownloader能够解密,我思考了以下的思路
由于获取seed
需要逆向微信协议,我不想在逆向这个协议上花费太多时间。
既然WechatVideoDownloader已经使用代理获取视频地址,我们可以进一步使用中间人攻击来获取视频链接及对应的decode_key
。
只需将注入WeixinJSBridge.invoke
的代码插入到某个JS文件中,当微信客户端请求视频链接时,就把获取到的视频链接发送到本地服务器。
这样不仅解决了seed和链接的问题,连视频标题也能获取到。
最后,下载完视频后,通过seed生成解密序列并对视频进行解密。
最后的劫持代码
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
(function () {
if
(window.wvds !
=
=
undefined) {
return
}
let receiver_url
=
"https://wechat.video.download"
;
function send_response_if_is_video(response) {
if
(response
=
=
undefined) {
return
;
}
if
(response[
"err_msg"
] !
=
"H5ExtTransfer:ok"
) {
return
;
}
let value
=
JSON.parse(response[
"jsapi_resp"
][
"resp_json"
]);
if
(value[
"object"
]
=
=
undefined || value[
"object"
][
"object_desc"
]
=
=
undefined || value[
"object"
][
"object_desc"
][
"media"
].length
=
=
0
) {
return
}
let media
=
value[
"object"
][
"object_desc"
][
"media"
][
0
];
let video_data
=
{
"decode_key"
: media[
"decode_key"
],
"url"
: media[
"url"
]
+
media[
"url_token"
],
"size"
: media[
"file_size"
],
"description"
: value[
"object"
][
"object_desc"
][
"description"
].trim(),
"uploader"
: value[
"object"
][
"nickname"
]
}
fetch(receiver_url, {
method:
'POST'
,
mode:
'no-cors'
,
body: JSON.stringify(video_data),
}).then((resp)
=
> {
console.log(`video data
for
${video_data[
"description"
]} sent`);
});
}
function wrapper(name,origin) {
console.log(`injecting ${name}`);
return
function() {
let cmdName
=
arguments[
0
];
if
(arguments.length
=
=
3
) {
let original_callback
=
arguments[
2
];
arguments[
2
]
=
async function () {
if
(arguments.length
=
=
1
) {
send_response_if_is_video(arguments[
0
]);
}
return
await original_callback.
apply
(this, arguments);
}
}
let result
=
origin.
apply
(this,arguments);
return
result;
}
}
console.log(`
-
-
-
-
-
-
-
Invoke WechatVideoDownloader Service
-
-
-
-
-
-
-
-
-
`);
window.WeixinJSBridge.invoke
=
wrapper(
"WeixinJSBridge.invoke"
,window.WeixinJSBridge.invoke);
window.wvds
=
true;
})()
|
proof of work
CHATGPT生成的。
更多【软件逆向-微信视频号视频加密逆向】相关视频教程:www.yxfzedu.com