咪咕视频m3u8地址解析及ddCalcu参数加密逆向

咪咕视频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&timestamp=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&timestamp=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字节,紧接着非本地变量be.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函数中的非本地变量er对象的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.m

function 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, Instance


def 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 = f

store = 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] = c
    start = wasm.exports.m(0)
    encrypted_url = ''
    i = start
    while memory_view[i]:
        encrypted_url += chr(memory_view[i])
        i += 1
    return encrypted_url


def 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_url


def main():
    video_id = 926412678
    video_url = get_url(video_id)
    encrypted_url = encrypt(video_url)
    print(encrypted_url)


if __name__ == '__main__':
    main()
posted @ 2024-08-12 13:43  KiraDiana  阅读(584)  评论(0编辑  收藏  举报