咪咕视频m3u8地址解析及ddCalcu参数加密逆向
概述
本文主要讲述咪咕视频m3u8地址的解析以及使用Wasm对视频的m3u8地址进行加密得到ddCalcu参数的方法。
使用视频ID获取未加密的视频URL
对咪咕视频进行抓包发现,通过接口
https://webapi.miguvideo.com/gateway/playurl/v3/play/playurl?contId=926412678&rateType=3&xh265=true&chip=mgwww&channelId=0132_10010001005
可以获取到视频的m3u8地址,接口的参数contId
指定视频ID,这里是926412678,rateType
指定视频分辨率,2为标清540P,3为高清720P,4为超清1080P(但超清一般只有前6分钟的试看时间)。
访问此接口需要使用GET方法,并设定以下请求头:
Appcode: miguvideo_default_www
Appid: miguvideo
Channel: H5
x-up-client-channel-id: 0132_10010001005
接口返回JSON,JSON的body
->urlInfo
->url
键值就是视频的m3u8地址
http://gslbmgspvod.miguvideo.com/depository_nas03/asset/zhengshi/5105/544/165/5105544165/media/5105544165_5270812321_56.mp4.m3u8?msisdn=20240811111225909a4fdfb1964b7b802c2c47241b024c&mdspid=&spid=800033&netType=0&sid=5700895378&pid=2028597139×tamp=20240811111225&Channel_ID=0132_10010001005&ProgramID=926412678&ParentNodeID=-99&assertID=5700895378&client_ip=183.6.24.190&SecurityKey=20240811111225&promotionId=&mvid=5105544165&mcid=1003&mpid=120000510768&playurlVersion=WX-A1-7.7.2.5-RELEASE&userid=&jmhm=&videocodec=h264&bean=mgspwww&puData=41a05276e61f2220e3a57b9eddf625a8
若直接访问获取到的m3u8地址并不能正常得到m3u8文件,需要对其进行加密最终得到加密后的m3u8地址
http://gslbmgspvod.miguvideo.com/depository_nas03/asset/zhengshi/5105/544/165/5105544165/media/5105544165_5270812321_56.mp4.m3u8?msisdn=20240811195035d062a3247070440fa88a380accb49b6a&mdspid=&spid=800033&netType=0&sid=5700895378&pid=2028597139×tamp=20240811195035&Channel_ID=0132_10010001005&ProgramID=926412678&ParentNodeID=-99&assertID=5700895378&client_ip=183.6.24.190&SecurityKey=20240811195035&promotionId=&mvid=5105544165&mcid=1003&mpid=120000510768&playurlVersion=WX-A1-7.7.2.5-RELEASE&userid=&jmhm=&videocodec=h264&bean=mgspwww&puData=021d5e2e680350a1d51a47bdb1cec090&ddCalcu=0092z01ycdwe5zce12bed6b87043a5105ad1&sv=10000&ct=www
加密后的URL主要是多了ddCalcu
参数,接下来就要知道网站是如何进行加密的。
视频URL的加密方法
既然ddCalcu
是加密得到的参数,首先想到的是直接搜索这个关键词,发现在pcPlayer.js
文件中能找到
打个断点调试,刷新网页发现这行代码没有被执行,看来这个并不是用于加密的代码。
换个思路,找到加密后的m3u8地址的网络请求,查看这个请求的调用栈并进行断点调试,最后在pcPlayer.js
中误打误撞找到了用于加密的代码
这段代码中的var n = r._getEncrypt(l)
十分可疑,很可能就是加密函数,打断点逐行调试这段代码,逐个查看这里的变量,结果发现在执行_getEncrypt
函数前,这里的变量t
就是未加密的URL
当执行_getEncrypt
函数以及i = r.UTF8ToString(n)
这行代码后,i
变量就得到了加密后的URL(带有ddCalcu参数)
到这里基本就可以确定这就是用于加密URL的代码了。
继续查看_getEncrypt
函数实现,发现这是个wasm函数,wasm文件名是pickproof1000.wasm
,这就对了,pickproof就是防盗的意思,这也说明URL是使用wasm进行加密的。
既然URL是用wasm进行加密的,那么在加密前就必须先加载wasm模块,只要找到加载wasm模块的代码,从加载开始一步步调试,抽丝剥茧,应该就不难找到URL加密的方法了。
直接在pcPlayer.js
中搜索WebAssembly.instantiate
找到WebAssembly.instantiateStreaming(e, t).then(n, ...
,这是用于创建wasm实例的函数,直接断点调试
这里wasm在实例化时导入了10个js函数,这些函数虽然参数列表不同,但逐个查看发现均返回0或无返回值,应该是用于混淆代码的。
此外,在wasm实例化后又立即将wasm的Module和Instance对象穿给函数n
函数n
又将wasm实例对象传给了函数r
函数r
中,变量t
是wasm实例,wasm实例导出的对象赋值给了e.asm
,变量y
是从wasm实例中导出的对象k
,结合pickproof1000.wasm
的代码(memory $k (;0;) (export "k") 256 256)
可知导出的对象k
实际上是wasm实例中一段初始大小和最大大小均为256页的线性内存,由于wasm每页内存大小固定为64KB,所以导出的内存大小为16777216字节,紧接着非本地变量b
和e.HEAPU8
被赋值为可以读写此内存的Uint8Array数组。
function r(t, r) { // t是WebAssembly.Instance对象var n, i, o = t.exports;e.asm = o, // wasm实例导出的对象y = e.asm.k, // wasm导出的对象k,查看wasm源码可知其实是wasm内存n = y.buffer,e.HEAPU8 = b = new Uint8Array(n) // 得到可读写wasm内存的Uint8Array数组// 省略一些代码
}
接着就开始执行加密代码了,首先是l = r._malloc(4 * t.length + 1)
,这里的变量t
是未加密的URL,_malloc
函数是wasm实例中导出的p
函数,这行代码的作用是在wasm内存中申请特定大小的空间,然后返回申请得到空间的起始地址,即变量l
,这里得到的地址是5266520。
// 加密代码
l && r._free(l),
l = r._malloc(4 * t.length + 1), // 申请空闲的wasm内存,返回起始地址
r.stringToUTF8(t, l, 4 * length + 1); // 将未加密的URL写入刚刚申请的空闲内存
var n = r._getEncrypt(l) // , i = r.UTF8ToString(n);
随后执行r.stringToUTF8(t, l, 4 * t.length + 1)
,这行代码的作用是将未加密的URL的ASCII码逐一写入刚刚申请得到的wasm实例的内存,其写入的起始地址是前面通过_malloc
申请内存得到的起始地址,而这具体读写wasm内存的操作正是通过读写前面在r
函数中赋值给变量b
的Uint8Array数组实现的。
然后就是var n = r._getEncrypt(l)
,_getEncrypt
实际上是wasm实例中导出的m
函数,向加密函数传入wasm实例内存中未加密URL的起始地址,用于指定URL在内存中的位置,得到完成加密后的URL的起始地址n
,这里得到的是5272888,至于其具体的算法是如何实现的不用关心。
最后是i = r.UTF8ToString(n)
,向UTF8ToString
函数传入完成加密后的URL在wasm实例内存中的起始地址,该函数能通过给出的起始地址提取出加密后的URL,然后赋值给变量i
。
另外,这里加密代码的r
对象就是前面在r
函数中的非本地变量e
,r
对象的HEAPU8
就是r
函数中的e.HEAPU8
,也就是可以用于读写wasm实例内存的Uint8Array数组。
在内存查看器中查看可以看到wasm实例的内存,定位到加密URL的起始地址可以直接看到加密后的URL。
总结,URL加密的过程是这样的,在加载完wasm模块后进行一些变量的初始化,接着调用_malloc
申请用于存放未加密URL的wasm内存,得到空闲内存的起始地址,再通过stringToUTF8
函数将未加密的URL逐字符写入申请得到的空闲内存,然后调用_getEncrypt
对wasm内存中未加密的URL进行加密,得到完成加密后的URL再wasm内存中的地址,最后通过UTF8ToString
获取加密后的URL地址。
其实不使用_malloc
申请wasm内存,直接将空闲内存的起始地址设为0,将URL存放在内存的最开头也是可行的。
代码实现
javascript实现
const importObj = {a: {a: (a,b,c)=>{},b: (a)=>{return 0}, c: ()=>{},d: (a,b,c,d)=>{return 0},e: (a)=>{return 0},f: (a,b,c,d,e)=>{return 0},g: (a,b)=>{return 0},h: (a,b)=>{return 0},i: (a)=>{return 0},j: (a,b,c,d,e)=>{return 0}}
}
// Load wasm module and get wasm instance.
const wasmURL = "https://www.miguvideo.com/mgs/player/prd/v_20240727173237_cfa34b34/dist/pickproof1000.wasm"
const wasm = await WebAssembly.instantiateStreaming(fetch(wasmURL), importObj).then(w => w.instance)// Get wasm memory.
const memory = wasm.exports.k
const memoryView = new Uint8Array(memory.buffer)// Get encryption function.
const getEncrypt = wasm.exports.mfunction encrypt(url) {// Write the origin URL into the wasm memory.let i;for (i = 0; i < url.length; ++i) {memoryView[i] = url.charCodeAt(i)}memoryView[i] = 0// Get the beginning index of encrypted URL in wasm memory.let start = getEncrypt(0)// Read the encrypted URL from wasm memory.encryptedURL = ""for (let i = start; memoryView[i] != 0; ++i) {encryptedURL += String.fromCharCode(memoryView[i])}return encryptedURL
}var vid = 926412678
var rateType = 3
var apiURL = `https://webapi.miguvideo.com/gateway/playurl/v3/play/playurl?contId=${vid}&rateType=${rateType}&xh265=true&chip=mgwww&channelId=0132_10010001005`
var resp = await fetch(apiURL, {headers: {"Appcode": "miguvideo_default_www","Appid": "miguvideo","Channel": "H5","x-up-client-channel-id": "0132_10010001005",}
})var videoURL = (await resp.json()).body.urlInfo.url
var encryptedURL = encrypt(videoURL)
console.log(encryptedURL)
python实现
需要先安装以下三个包
requests
wasmer
wasmer_compiler_cranelift
测试代码:
import requests
from wasmer import Store, Function, Module, Instancedef a(a: int, b: int, c: int): pass
def b(a: int) -> int: return 0
def c(): pass
def d(a: int, b: int, c: int, d: int) -> int: return 0
e = b
def f(a: int, b: int, c: int, d: int, e: int) -> int: return 0
def g(a: int, b: int) -> int: return 0
h = g
i = e
j = fstore = Store()
import_obj = {'a': {'a': Function(store, a),'b': Function(store, b),'c': Function(store, c),'d': Function(store, d),'e': Function(store, e),'f': Function(store, f),'g': Function(store, g),'h': Function(store, h),'i': Function(store, i),'j': Function(store, j),},
}wasm_url = 'https://www.miguvideo.com/mgs/player/prd/v_20240727173237_cfa34b34/dist/pickproof1000.wasm'
wasm_binary = requests.get(wasm_url).content
module = Module(store, wasm_binary)
wasm = Instance(module, import_obj)
memory = wasm.exports.k
memory_view = memory.uint8_view()def encrypt(url: str):for i,c in enumerate(url.encode()):memory_view[i] = cstart = wasm.exports.m(0)encrypted_url = ''i = startwhile memory_view[i]:encrypted_url += chr(memory_view[i])i += 1return encrypted_urldef get_url(vid, rate_type=3):channel_id = '0132_10010001005'headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4343.0 Safari/537.36 Edg/89.0.727.0','Appcode': 'miguvideo_default_www','Appid': 'miguvideo','Channel': 'H5','x-up-client-channel-id': channel_id,}url = f'https://webapi.miguvideo.com/gateway/playurl/v3/play/playurl?contId={vid}&rateType={rate_type}&xh265=true&chip=mgwww&channelId={channel_id}'resp = requests.get(url, headers=headers)video_url = resp.json()['body']['urlInfo']['url']return video_urldef main():video_id = 926412678video_url = get_url(video_id)encrypted_url = encrypt(video_url)print(encrypted_url)if __name__ == '__main__':main()