前言
在之前上一篇相关文章中的开头讲到,托管平台的加密已经升级到了v12的版本,现在已经升级到了v13版本了。
v13相对v12版本来讲,真正的存在了es层的加密。想要完整解密被v13加密的pts分片,必须分离到es层处理,而并非可以好像v12一样进行绕过,这个版本迭代的变化也是明显可以预见的。
此视频托管平台目前已有的加密包含网页端的v10、V11、V1102、V1103、V1104、V12、V13以及安卓端的native前缀加密版本,其中部分所使用的的加密大差不差,本篇文章分析的则是最新的也是加密流程长达7次的v13版本。
测试使用的网站为:aHR0cHM6Ly93d3cueWlpaHV1LmNvbS92XzI4MjQ3Ni5odG1s (无需登录,访问直接可以播放)
第一层加密
打开f12,访问网站然后播放视频,可以看到一个xxxxxxx.json的请求
1.jpg (39.18 KB, 下载次数: 0)
下载附件
2024-4-17 10:43 上传
那就设置一个【.json】的XHR断点,然后刷新网页
2.jpg (9.77 KB, 下载次数: 0)
下载附件
2024-4-17 10:43 上传
成功在请求前断下,接着在下面请求结束的地方下一个断点
3.jpg (52.37 KB, 下载次数: 0)
下载附件
2024-4-17 10:43 上传
再次断下后发现,确实是前面的响应内容
4.jpg (79.97 KB, 下载次数: 0)
下载附件
2024-4-17 10:43 上传
接着单步执行继续往下走,会进入到【e.success】这个函数中
5.jpg (58.74 KB, 下载次数: 0)
下载附件
2024-4-17 10:43 上传
进入函数直接可以看到一个名字叫做【decryptVideoJson】的函数,从名字上来猜测,很有可能就是解密这一段数据的,接着单步执行,果然进入到这个函数
6.jpg (35.14 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
接着进入到函数内,发现逻辑并不长,按照变量名来看,首先是对vid取了一个md5
7.jpg (33.17 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
返回值是【5728409e316d2014411337cacfa625ed】,然后常规对照一下是不是标准的md5,发现没有问题
8.jpg (180.51 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
然后分别取了md5结果的前16位和16到32位,接着进入到【Qt.m.c】函数,进入函数发现有aes相关的字符串,所以猜测可能是一个aes加密,并且出现了【initialation vector】,那么可以说明e变量就是iv,t变量就是key。对应的就是前16位作为key,后16位作为iv来解密
9.jpg (47.05 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
按照对应逻辑编写python代码
import json
import base64
from Crypto.Hash import MD5
from Crypto.Cipher import AES
from Crypto.Util import Padding
def decryptVideoJson(vid, body):
key_iv = MD5.new(vid.encode()).hexdigest()
cryptor = AES.new(key=key_iv[:16].encode(), mode=AES.MODE_CBC, iv=key_iv[16:].encode())
return json.loads(base64.b64decode(Padding.unpad(cryptor.decrypt(bytes.fromhex(body)), AES.block_size)).decode())
使用代码可以成功出结果,里面就存在分片列表以及seed_const等关键参数
第二层加密
第一层解密出来后,发现并没有传统的m3u8后缀文件,浏览器也没有请求m3u8文件,而是请求了一个pdx的后缀文件,这个pdx的url也在前面的解密结果里面,但是响应也是加密了
10.jpg (70.81 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
但是这是一个fetch请求,没法设置XHR断点,那么就先看调用堆栈,点击进第二个
11.jpg (85.25 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
看到这是一个标准的异步请求,并且在case 15处有类似响应的处理,那么就在这里下一个断点,同时可以清除前面设置的XHR断点了
12.jpg (78.43 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
刷新后发现,出现意外了,这次没有断下来。才发现原来代码不在js中,而是在blob里面
13.jpg (92.84 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
那么说明代码可能运行在worker里面或者js的url有随机数之类的,首先要找到代码是在哪个js里面
搜索发现代码来自于polyv-wasm-player.js,发现并没有随机数,那么直接用浏览器的替换功能来注入debugger语句,保存刷新
14.jpg (84.36 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
这次就没有意外断下来了,单步调试发现下一步走的是【case 21】
15.jpg (111.07 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
那么就在case 21下一行下断点,然后继续运行脚本,再次断下后单步执行,就会进入到onmessage里面
16.jpg (61.91 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
这里貌似看到了pdx的关键词,继续单步执行,会进入到【rt(n.body, String(2 t), r)】函数里面,n.body就是密文,String(2 t)就是前面得到的seed_const,r就是参数长度
17.jpg (37.14 KB, 下载次数: 0)
下载附件
2024-4-17 10:44 上传
首先是根据n来确定一个固定的字符串以及一个固定的数组,ze函数就是一个标准md5,Qt.m.c根据前面的经验可以知道是一个aes加密
按照对应逻辑编写python代码
import base64
from Crypto.Hash import MD5
from Crypto.Cipher import AES
from Crypto.Util import Padding
def decryptPdx(seed_const, body, n):
r = "NTQ1ZjhmY2QtMzk3OS00NWZhLTkxNjktYzk3NTlhNDNhNTQ4#"
i = [1, 1, 2, 3, 5, 8, 13, 21, 34, 21, 13, 8, 5, 3, 2, 1]
if 2 == n:
r = "OWtjN9xcDcc2cwXKxECpRgKw7piD4RwCdfOUlyNHFdSV0gHi="
i = [13, 22, 8, 12, 7, 6, 13, 1, 50, 11, 12, 8, 5, 16, 4, 1]
key = MD5.new((r + str(seed_const)).encode()).hexdigest()[1:17]
cryptor = AES.new(key=key.encode(), mode=AES.MODE_CBC, iv=bytes(i))
return Padding.unpad(cryptor.decrypt(base64.b64decode(body.encode())), AES.block_size).decode()
使用代码可以成功出结果,里面就是分片的列表,但是分片的后缀不是ts,而是pts,到底是仅仅改了名字,还是另有玄机?下一篇再来分析