某视频的无限debuger和内存暴增的破解并Python获取m3u8及下载

查看 140|回复 10
作者:billsmiless   
某视频的无限debuger和内存暴增的破解并Python获取m3u8及下载
1、前言
最近300块淘了个老主机(i5\8G\垃圾显卡),作为家庭影音服务器,装了个Ubuntu系统,装了开源免费的Jellyfin影音系统,一顿折腾之后,卡在了片源的问题上,Ubuntu的BT都比较慢,而且垃圾显卡转码不行(也能对付看),所以就想直接搞来Mp4的片子直接看,之前写过一个某网的被和谐了,所以重新找一个拿来试试。把折腾的过程整理成文分享出来,也加深自己的记忆。审视之前的文章,发现有点过于啰嗦,本文尽量精简些。本文的核心是解决调试时内存暴增与无限debugger问题,m3u8的url提取相对容易(就是时间问题),m3u8下载以及转码属于正向开发,比较容易。完整的代码,我会放到文末。
使用的工具:MacOS系统、Safari浏览器、Sublime文本工具、Charles青花瓷抓包工具
2、调试内存暴增问题
调试地址:aHR0cHM6Ly9kbXhxLmZ1bi92b2RwbGF5LzE1MzA0Mi02LTEuaHRtbA==
浏览器打开地址,打开调试器并切换到【来源】菜单,看到程序停留在debugger处,查看堆栈,可以确定是无限debugger。
找到无限debugger的代码的文件,这里是dmplay.js , 既然要解决无限debugger,就得修改源码,这里使用Charles替换。
2.1 使用Charles抓包工具替换dmplay.js文件
这里以main_v1.js文件为例。(dmplay.js同理)
将main_v1.js文件保存到本地,然后使用Charles的Map Local即重定位功能,实现替换,替换后就可以随意修改js内容了。这里简单说下过程:
1、使用Charles抓到main_v1.js的请求
2、在mian_v1.js的请求上,右键选择Map Local,选择本地的main_v1.js
3、清空浏览器缓存,重新刷新网页,这时的main_v1.js就是本地的文件了(刷新时,会卡在空白页,就是内存暴增了)。


01 Charles.jpg (537.03 KB, 下载次数: 2)
下载附件
1
2023-11-28 10:07 上传


2.2 初步确定内存暴涨的原因
重刷新网页,你会发现卡在空白页,打开系统任务管理器,可以看到浏览器的内存在飞涨。
内存飞涨,基本就是死循环。这里的代码都是混淆过的,直接看代码是不行的。
那么只能从调试入手,通过断点找到死循环的位置。
下面的操作过程要注意:
1、先取消替换dmplay.js,即关闭抓包工具
2、清空浏览器缓存,重打开网页
3、在dmplay.js代码的前3行都打上断点,因为不确定会停在哪里
4、不要关闭调试器,再次清空缓存,打开Charles抓包工具,替换dmplay.js,重新加载网页
5、可以看到,断点会停在dmplay.js的第一行处
6、接下来单步执行,一直执行,直到某for循环,跳不出去了
这里是dmplay.js的37行的for循环,是个死循环。如下图02


02 dmplay.js死循环.jpg (509.26 KB, 下载次数: 2)
下载附件
2
2023-11-28 10:07 上传

找到了死循环的位置,想跳过它很容易。因为我已经调试过了,它还有其他陷阱。就不再这里一个一个调试了,太费劲(每次要清空缓存,重加载)
因此我们换个思路,既然死循环在dmplay.js中,那么我们把它单独拿出来调试看看行不行。
2.3 把dmplay.js单拿出来调试
写一个测试的main.html,把dmplay.js加载进去,然后用浏览器打开main.html,可以看到一直加载中,并且内存暴增了(说明单独调试是可以的)。如图03


03 dmplay.js单独调试.jpg (289.4 KB, 下载次数: 2)
下载附件
3
2023-11-28 10:07 上传

2.3.1 解除内存暴增的死循环1
由图2,我们知道了死循环的位置,如何避免它呢,这里看函数堆栈,在dmplay.js的80行代码处,调用了setCookie方法,从而触发了死循环。
经过与原网页对比,可以确定,原网页并不会调用setCookie方法。因此就直接在if条件上做手脚,不让进入setCookie方法。
因此直接在78行的下方,添加代码_0x383a9d=true;  如图04


04跳过setCookie死循环.jpg (543.19 KB, 下载次数: 2)
下载附件
2023-11-28 10:07 上传

重新加载main.js,结果顺利的加载网页,也没有内存暴涨,但打开控制台。发现有个错误。
错误:两个函数之间会无限循环调用。
原因:是调用_0x4ed4f8函数时,传递的参数问题。
正常传递的是:'\x69\x6e\x64\x65\x78\x4f\x66'  即indexOf,
不正常的是:'\x69\x6e\x64\u0435\x78\x4f\x66'  也是indexOf
注意,虽然都是indexOf,但他们的编码不同。所以传递他们会有不同的结果。
解决方法:直接在231行处,添加调用正确的代码。然后return。如图05


05 函数循环调用错误.jpg (542.1 KB, 下载次数: 2)
下载附件
5
2023-11-28 10:07 上传

2.3.2 解除内存暴增的死循环2
解决完报错后,重加载main.html,结果又卡主,并内存暴增了。
开始调试,但是因为内存暴增打不开调试器。
这里有个小技巧:
1、先修改代码允许代码出错,然后打开调试器,然后就不要关闭它了
2、在dmplay.js的前三行都加上断点,然后在把dmplay.js代码改到正常的
3、此时重加载html, 调试器就会断到我们刚加的断点处了。就可以单步调试了。
单步调试,结合调用堆栈,观察代码,找到可疑的死循环,这里直接定位到死循环:在170行的for循环上,如下图:
因为循环条件的_0x415bdc的值一直在增大,所以导致死循环,内存暴增。


06 170行的死循环.jpg (479.56 KB, 下载次数: 2)
下载附件
2023-11-28 10:07 上传

下面寻找死循环的原因,看下调用堆栈,确定调用过程:
首先176行的代码:new _0x58722f(_0x54ac)['HIQfdt'](); 创建对象实例,并调用 HIQfdt 函数,
HIQfdt函数在158行 0x58722f['prototype']['HIQfdt'] ,该函数作用是 生成参数,并传递给 ngDNjY 函数。
ngDNjY函数在163行 _0x58722f['prototype']['ngDNjY'] = function(_0x253b56),该函数作用是根据_0x253b56的值,判断是否调用NdNRlq
NdNRlq函数在169行  _0x58722f['prototype']['NdNRlq'] = function(_0x572cd8) ,该函数就是个死循环。
以上分析可知,只要在ngDNjY函数中,不去调用NdNRlq函数,就可以跳过死循环。
因此,我们在函数中ngDNjY,让其直接返回参数_0x253b56。 即注释掉if (!Boolean(~_0x253b56))这行与 }  即可。如下图:


07 死循环分析.jpg (606.96 KB, 下载次数: 2)
下载附件
2023-11-28 10:07 上传

重加载html,顺利加载了。打开调试器,发现断在debugger处,这就是无限debugger了。
3、调试无限debugger问题
解决了内存暴增的问题后,解决下无限debugger问题。重加载main.html, 打开调试器,无限debugger了,看堆栈找源头。
可知调用过程:(如下图)
debugger


08 无限debugger堆栈.jpg (581.72 KB, 下载次数: 2)
下载附件
2023-11-28 10:07 上传

以上,可以确定dmplay.js的1124行的函数 就是debugger代码的调用处。无论是源码还是调试器看到的源码,都是混淆过的。
因此,我们使用调试器,解读下1109--1125行中的if代码,看看到底是在干什么。
3.1 解析调用debugger的函数
现在我们可以确定,dmplay.js的1109-1125行代码调用了debugger方法。因此我们分析下其是如何调用debugger的。
贴出源码:
if (_0x6c015c[_0x54ac('‮1eb', '&973')](_0x6c015c[_0x54ac('‮1ec', '(UWh')]('', _0x6c015c[_0x54ac('‫1ed', 'oUfs')](_0x44cdb6, _0x44cdb6))[_0x6c015c[_0x54ac('‮1ee', '(us1')]], 0x1) || _0x6c015c[_0x54ac('‮1ef', 'mNhh')](_0x6c015c[_0x54ac('‮1f0', 'bEiy')](_0x44cdb6, 0x14), 0x0)) {
    (function(_0x48d943) {
        var _0x403cd3 = {
            'SSUfs': function(_0x55cdc4, _0x2ecd54) {
                return _0x6c015c[_0x54ac('‫1f1', 'O5uG')](_0x55cdc4, _0x2ecd54);
            },
            'Riulo': function(_0x3cdfbc, _0x213fc0) {
                return _0x6c015c[_0x54ac('‫1f2', 'bEiy')](_0x3cdfbc, _0x213fc0);
            },
            'KJDTQ': _0x6c015c[_0x54ac('‮1f3', '!2v&')],
            'ysFFB': _0x6c015c[_0x54ac('‮1f4', 'f35]')]
        };
        return function(_0x48d943) {
            return _0x403cd3[_0x54ac('‫1f5', 'Y8[S')](Function, _0x403cd3[_0x54ac('‮1f6', 'sY^A')](_0x403cd3[_0x54ac('‫1f7', '&KZ5')](_0x403cd3[_0x54ac('‮1f8', '(UWh')], _0x48d943), _0x403cd3[_0x54ac('‮1f9', 'Fnel')]));
        }(_0x48d943);
    }(_0x6c015c[_0x54ac('‫1fa', '0l@P')])('de'));
    ;
}

注意:在调试器中看到的源码可能与编辑器打开看到的有区别。因此我们在编辑器中看源码,在调试器中调试。

看着头疼,但认真看还是能发现规律,即出现了很多的_0x6c015c和_0x54ac。
通过调试可知_0x6c015c是一个对象,通过key取值;这个_0x54ac是一个字符串转换函数,即把乱码转为正常的函数名。如下图:


09 函数分析0.jpg (579.25 KB, 下载次数: 2)
下载附件
2023-11-28 10:07 上传

我们在调试器中,手动调用以上代码中的_0x54ac,然后将结果替换到源码中。替换后的代码:
if (_0x6c015c["FjzrV"](_0x6c015c["szcti"]('', _0x6c015c["TFLMk"](_0x44cdb6, _0x44cdb6))[_0x6c015c["EtIoJ"]], 0x1) || _0x6c015c[_0x54ac('‮1ef', 'mNhh')](_0x6c015c["PbCaD"](_0x44cdb6, 0x14), 0x0)) {
        (function(_0x48d943) {
        var _0x403cd3 = {
            'SSUfs': function(_0x55cdc4, _0x2ecd54) {
                return _0x6c015c["NdFfp"](_0x55cdc4, _0x2ecd54);
            },
            'Riulo': function(_0x3cdfbc, _0x213fc0) {
                return _0x6c015c["EWQLS"](_0x3cdfbc, _0x213fc0);
            },
            'KJDTQ': _0x6c015c["uDfRF"],
            'ysFFB': _0x6c015c["nHsTR"]
        };
        return function(_0x48d943) {
            return _0x403cd3["SSUfs"](Function, _0x403cd3["Riulo"](_0x403cd3["Riulo"](_0x403cd3["KJDTQ"], _0x48d943), _0x403cd3["ysFFB"]));
        }(_0x48d943);
    }(_0x6c015c["qjJvv"])('de'));
}
接下来,调试器中调用_0x6c015c, 将结果替换到源码。在替换之前,先看看以上代码中出现的_0x6c015c的各个key的值是什么。
我直接将结果翻译如下:(可知,有的key值是函数,有的是字符串)
// TFLMk: A/B               //传递两个参数A和B,返回A/B的结果
// szcti: A+B                                 //传递两个参数A和B,返回A+B的结果
// FjzrV: A!=B                                 //传递两个参数A和B,返回A!=B的结果
// PbCaD: A%B                             //同上
// zgxxx: A==B                                 //同上
// NdFfp: A(B)
// EWQLS: A+B
// uDfRF: "Function(arguments[0]+\""
// nHsTR: "\")()"
// SSUfs: A(B)
// Riulo: A+B
// KJDTQ: "Function(arguments[0]+\""
// ysFFB: "\")()"
// qjJvv: "bugger"
替换key值后的源码为:
if ((''+(_0x44cdb6/_0x44cdb6))["length"] != 0x1 || (_0x44cdb6 % 0x14) == 0x0) {
        (function(_0x48d943) {
            return function(_0x48d943) {
                return Function((("Function(arguments[0]+\""+ _0x48d943) + "\")()"));
            }(_0x48d943);
        }("bugger")('de'));
}
if语句就不看了,看内部代码。这里是3个函数
第1个函数传递了一个参数_0x48d943,即"bugger"进去,然后直接return第2个函数;
第2个函数也传递了一个"bugger"进去,然后return第3个函数。
第3个函数,接收一个参数'de',并执行。
大家应明白了。下面看下函数3的执行代码:
function anonymous() {
Function(arguments[0]+"bugger")()
}
这里arguments[0]的值就是"de",所以就调用了debugger。
至此,费了半天劲,就知道这段代码都干啥了,就调用了debugger!!
3.2 跳过无限debugger的函数
我们已经知道无限debugger的一处代码了(后面还有其他地方)。如何跳过,简单粗暴,直接if的条件为否就行了。
所以if语句改为:if( false ) ,这就跳过了 第一个无限debugger处了。
然后,一运行,又跳到了debugger了。参照上面的经验,就可以分析出代码了。这里直接给结果:
在1127行的if条件内部的代码也是调用debugger的,经过复原,与之前的几乎雷同。
看下1127的if语句:
if (_0x6c015c[_0x54ac('‮1fb', '!2v&')](_0x6c015c[_0x54ac('‫1fc', 'sVWj')], _0x6c015c[_0x54ac('‮1fd', '#Aws')]))
解析后:
if ((''+(_0x44cdb6/_0x44cdb6))["length"] != 0x1 || (_0x44cdb6 % 0x14) == 0x0) {
与之前的if对比,完全一样。所以这里也是直接改成if( false ),跳过debugger。
看下跳过debugger后的代码,如下图:


10 跳过无限debugger.jpg (497.42 KB, 下载次数: 1)
下载附件
10
2023-11-28 10:07 上传

至此,dmplay.js 中的无限debugger就饶过了。
3.3 另一个埋伏 main_v1.js
开开心心的把修改后的dmplay.js替换原网址的js后,结果又内存暴增了,当然还有无限debugger。
原因就是:该网页上了双保险,在另一个main_v1.js中,也搞了一套类似的。
分析过程就不写了,与上面的类似,我会把修改过的文件附在附件中。
至此,调试时的内存暴增与无线debugger就都绕过了,下面可以愉快的调试了。
4、分析提取m3u8链接过程
4.1 确定获取m3u8的请求
绕过无线debugger与内存暴增后,打开调试器,打开目标网址,在调试器中查看http请求(勾选XHR/Fetch过滤)
可以看到m3u8的链接,同时还有一个get_play_url请求,该请求响应是加密的,我们比较幸运,经过分析,该请求就是获取m3u8链接的。如下图:


11 查看http请求.jpg (508.31 KB, 下载次数: 2)
下载附件
2023-11-28 10:06 上传

接下来就是定位get_play_url请求的代码在哪里,确定请求参数与请求结果
我们全局搜索get_play_url请求的参数request_token,幸运的是,有两处代码在main_v1.js的994行和1055行(行数可能会有所偏差)
两处我们拿不准,那就在success代码处(请求回调)断点,也就是999行和1061行。然后刷新页面。可以看到代码停在了999行处。
查看989行的url的值,可以确定这就是get_play_url的请求。如下图:


12 get_play_url请求.jpg (673.05 KB, 下载次数: 2)
下载附件
12
2023-11-28 10:06 上传

我们查看下请求结果,单步调试,这里我直接跳到目标代码,即1083行的decryptPackData函数,查看参数,经与http响应对比,可知其是待加密的数据。
从函数名,猜测decryptPackData应为解密函数,直接在控制台手动调用一下this['decryptPackData'](_0x671add)
得到结果如下:
"{\"code\":200,\"data\":{\"url\":\"https:\\/\\/s6.bfzycdn.com\\/video\\/mishilieche\\/HD\\/index.m3u8\"}}"
至此,我们确定了get_play_url请求就是获取m3u8链接的请求,同时也确定了解密的函数。如下图:


13 确定解密函数.jpg (665.13 KB, 下载次数: 2)
下载附件
13
2023-11-28 10:06 上传

4.2 分析get_play_url的请求头与请求参数
回到999行的断点处,我们分析下get_play_url请求,请求url是可知的,请求的method可以得知是GET。
需要分析的是参数data的值以及headers,看下图:


14 解析参数与请求头.jpg (751.79 KB, 下载次数: 2)
下载附件
14
2023-11-28 10:06 上传

我们先分析参数data的值,data是字典,如下:
'data': {
    'app_key': _0x498cf2,
    'client_key': _0x206047,
    'request_token': _0x305c97,
    'access_token': _0x131d73
},
app_key的值是_0x498cf2,它在984行,源码如下:
let _0x498cf2 = CryptoJS[_0xb708('‮164', 'sm*U')](_0x154404[_0xb708('‮165', 'NBpG')])[_0xb708('‮166', 'hZIq')]();
这里的_0xb708函数是个字符串转换函数,与dmplay.js中的转换函数类似。因此,我们手动调用_0xb708得到解析后的代码如下:
let _0x498cf2 = CryptoJS["MD5"]("www.555dy.com")["toString"]();
到这里就很明显了。CryptoJS是解密的库。因此可以确定该参数的实现了。其他的参数同理。这里直接贴出最终的参数结果:
/// 以下是4个参数: app_key  client_key request_token access_token
let _0x498cf2 = CryptoJS["MD5"]("www.555dy.com")["toString"]();
let _0x206047 = CryptoJS["MD5"](navigator["userAgent"])["toString"]();
let _0x305c97 = CryptoJS["MD5"]("https://zyz.sdljwomen.com")["toString"]();
///access_token 去掉请求url的http或者https,再md5
let _0x131d73 = CryptoJS["MD5"](_0xd5c050["replace"]("http:", '')["replace"]("https:", ''))["toString"]();
let _0x131d73 = CryptoJS["MD5"]("//player.ddzyku.com:3653/api/get_play_url")["toString"]();
请求参数搞定了,下面分析请求头,这个复杂点。
请求头的源代码如下:
const _0xd5c050 = _0x154404[_0xb708('‮155', 'q!qS')](server_url, _0x154404[_0xb708('‮156', 'KXCB')]);
const _0x358b48 = _0x154404[_0xb708('‫157', '8eT0')];
const _0xe71271 = Math[_0xb708('‮158', 'NBpG')](_0x154404[_0xb708('‫159', 'e%&C')](new Date(), 0x3e8));
const _0x389592 = _0x154404[_0xb708('‮15a', 'r)w!')];
const _0x24a30a = CryptoJS[_0xb708('‮15b', 'N9nA')](_0x154404[_0xb708('‮15c', '^ixT')](_0x154404[_0xb708('‫15d', 'l04q')](_0x154404[_0xb708('‫15e', '8Xjy')](server_url, _0x358b48), _0xe71271), _0x389592))[_0xb708('‫15f', 'l04q')]();
const _0x51a035 = this[_0xb708('‫160', 'vuF5')](_0x1510a7);
const _0x7a05b1 = CryptoJS[_0xb708('‮161', 'hy(Q')](_0x51a035, _0x24a30a)[_0xb708('‫162', 'PF3I')]();
let _0x102cd8 = {
    'X-PLAYER-TIMESTAMP': _0xe71271,
    'X-PLAYER-SIGNATURE': _0x7a05b1,
    'X-PLAYER-METHOD': _0x358b48,
    'X-PLAYER-PACK': _0x51a035
};
请求头,有4个值,TIMESTAMP、SIGNATURE、METHOD、PACK
TIMESTAMP也即_0xe71271的值,比较简单,就是简单的获取时间戳然后取整。
METHOD即_0x358b48的值,请求方式,这里是GET。
剩下的两个稍微复杂点,先分析下PACK 即_0x51a035的值。 其代码翻译如下:
const _0x51a035 = this["encryptPackData"](_0x1510a7);
搜索encryptPackData可知其是个加密函数,解析后代码如下:
'encryptPackData'(_0x16eb06) {
    //调用getKeys 取得两个key
    let [_0x2a1d3b, _0x56663e] = this["getKeys"]();
    var _0x2cd5c3 = CryptoJS["enc"]["Utf8"]["parse"](_0x16eb06);
    const _0x1924c3 = CryptoJS["AES"]["encrypt"](_0x2cd5c3, _0x2a1d3b, {
        'iv': _0x56663e,
        'mode': CryptoJS["mode"]["CBC"],
        'padding': CryptoJS["pad"]["Pkcs7"]
    });
    return _0x1924c3["ciphertext"]["toString"]()["toUpperCase"]();
},
encryptPackData函数需要一个参数,这个参数是个重点,这里就不写回溯过程了,方法差不多。这个参数的值是从html中传递过来的,html中有段js代码,其中play_aaaa的url的值就是该参数的值。如下图:


15 encryptPackData的参数值.jpg (564.34 KB, 下载次数: 2)
下载附件
15
2023-11-28 10:06 上传

PACK的值搞定了,接下来分析SIGNATURE的值,即_0x7a05b1,源码如下:
const _0x7a05b1 = CryptoJS[_0xb708('‮161', 'hy(Q')](_0x51a035, _0x24a30a)[_0xb708('‫162', 'PF3I')]();
翻译后:
const _0x7a05b1 = CryptoJS["HmacSHA256"](_0x51a035, _0x24a30a)["toString"]();   //哈希算法
可以看到这是一个哈希算法,其中有两个参数_0x51a035和_0x24a30a,其中_0x51a035就是上一步求得的PACK的值。另一个_0x24a30a的源码如下:
const _0x24a30a = CryptoJS[_0xb708('‮15b', 'N9nA')](_0x154404[_0xb708('‮15c', '^ixT')](_0x154404[_0xb708('‫15d', 'l04q')](_0x154404[_0xb708('‫15e', '8Xjy')](server_url, _0x358b48), _0xe71271), _0x389592))[_0xb708('‫15f', 'l04q')]();
虽然它很长,但还是可以翻译成功,如下:
const _0x24a30a = CryptoJS["MD5"]("server_url + GET + TIME + _0x389592")["toString"]();
结果是个md5值,这里解释下,其中的参数:
server_url就是本请求的url去掉请求名get_play_url,
GET就是请求方式GET
TIME就是上面请求参数生成的时间戳
最后一个_0x389592是一个固定的字符串。
至此,请求的header与参数就分析完成了。接下来就是模拟请求获取结果了。
4.3 js生成get_play_url请求参数与headers
已经知道参数的生成过程,接下来就是模拟生成参数。将原网页的CprytoJS.js文件提取出来。
从main_v1.js中,拷贝出必要的函数,只有3个:decryptPackData、encryptPackData、getKeys
直接拷贝出来是不能用的,需要做些处理,将必要的参数函数去掉,添加上必要的值。完整的如下:
function getKeys() {
    var _0x26ded5 = "55cc5c42a943afdc"//_0xb708('‫1a2', '4b]L');
    var _0x590aa3 = "d11324dcscfe16c0"//_0xb708('‮1a3', 'mo8k');
    return [CC.CryptoJS.enc.Utf8.parse(_0x26ded5), CC.CryptoJS.enc.Utf8.parse(_0x590aa3)];
}
function decryptPackData(_0x671add) {
    let [_0x51c696, _0x48abff] = getKeys();
    var _0x95e934 = CC.CryptoJS.enc.Hex.parse(_0x671add);
    var _0x2864b7 = CC.CryptoJS.enc.Base64.stringify(_0x95e934);
    const _0x175606 = CC.CryptoJS.AES.decrypt(_0x2864b7, _0x51c696, {
        'iv': _0x48abff,
        'mode': CC.CryptoJS.mode.CBC,
        'padding': CC.CryptoJS.pad.Pkcs7
    });
    return _0x175606["toString"](CC.CryptoJS.enc.Utf8);
}
function encryptPackData(_0x16eb06) {
    let [_0x2a1d3b, _0x56663e] = getKeys();//this[_0xb708('‫1bb', 'hZIq')]();
    var _0x2cd5c3 = CC.CryptoJS.enc.Utf8.parse(_0x16eb06);
    const _0x1924c3 = CC.CryptoJS.AES.encrypt(_0x2cd5c3, _0x2a1d3b, {
        'iv': _0x56663e,
        'mode': CC.CryptoJS.mode.CBC,
        'padding': CC.CryptoJS.pad.Pkcs7
    });
    return _0x1924c3["ciphertext"]["toString"]()["toUpperCase"]();
}
通过以上函数以及CryptoJS库就可以模拟请求的参数了,封装了一个函数,如下:
function getParameters(server_url, decrypt_url, user_agent) {
    console.log('开始获取参数');
    // var decrypt_url = '8gaFI115QLfir6S0nESXjLxr0VWubomB7GvZ4QkVPBgku7ufRwwgnvVKaUU3QPePJG7tWzW1kJ3llsgwo000o0xwHwO0O0OO0O0O'
    // var server_url = "https://player.ddzyku.com:3653/api"   //请求地址
    var url_name = "/get_play_url"  //接口名字
    var ua = user_agent//"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15"
    let request_url = server_url+url_name
    //请求参数
    let app_key = CC.CryptoJS.MD5("www.555dy.com").toString();// _0xb708('‮141', 'CvId')
    let client_key = CC.CryptoJS.MD5(ua).toString()
    let request_token = CC.CryptoJS.MD5("https://zyz.sdljwomen.com").toString();
    let access_token = CC.CryptoJS.MD5(request_url.replace("http:", '').replace("https:", '')).toString()
    ///请求参数
    let data = {
        "app_key": app_key,
        "client_key": client_key,
        "request_token": request_token,
        "access_token": access_token
    }
    ///请求header
    let timestamp = Math.round(new Date()/0x3e8) + ''
    let method = "GET"   //请求方式, 这里是GET
    //pack获取:this["encryptPackData"](_0x1510a7)
    let pack = encryptPackData(decrypt_url)
    //SIGNATURE 获取
    let randstr = "55ca5c4d11424dcecfe16c08a943afdc"//_0xb708('‫13f', 'r)w!')   //无实际意义,仅仅为了拼接
    let md5str = CC.CryptoJS.MD5(server_url+method+timestamp+randstr).toString();
    let signature = CC.CryptoJS.HmacSHA256(pack, md5str).toString();   //哈希算法
    ///请求header
    let headers = {
        'X-PLAYER-TIMESTAMP': timestamp,
        'X-PLAYER-SIGNATURE': signature,
        'X-PLAYER-METHOD': method,
        'X-PLAYER-PACK': pack,
        'origin':'https://dmxq.fun'
    };
    console.log("request_url:")
    console.log(request_url)
    console.log("request_data:")
    console.log(data)
    console.log('headers:')
    console.log(headers)
    return {"url":request_url, "method": method, "headers": headers, "data": data}
}
参数说明:
server_url :请求url去掉请求名后的url
decrypt_url:html中play_aaaa的url的值
user_agent: 浏览器的userAgent
至此,请求的参数获取就完成了,下面通过代码实现自动解析。
5、python实现m3u8下载以及ffmpeg转码
5.1 python通过url解析得到m3u8链接
因为我们要调用js代码,这里用到的python库是 py_mini_racer,这个库比较友好。
首先封装一个py文件,用来获取模拟请求,获取m3u8的链接。就一个函数:
# 解析大米星球的url,得到m3u8的链接和文件名称
def parseDmxqM3u8(videoUrl) :
        ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15"
        headers = {"User-Agent":ua}
        # 请求获取videoUrl的html内容。  'verify=False,' 忽略ssl
        r = requests.get(videoUrl, headers = headers, verify=False)
        decrypt_url, videoName = getDecryptUrlAndVideoName(r.text)
        print('得到视频名字以及加密url')
        print(videoName)
        print(decrypt_url)
        print('开始获取m3u8视频地址')
        # 创建一个js上下文环境  
        ctx = py_mini_racer.MiniRacer()  
        # 执行 JavaScript 代码  
        ctx.eval(js_code)
        server_url = "https://player.ddzyku.com:3653/api"
        # decrypt_url = '8gaFI115QLfir6S0nESXjLxr0VWubomB7GvZ4QkVPBgku7ufRwwgnvVKaUU3QPePJG7tWzW1kJ3llsgwo000o0xwHwO0O0OO0O0O'
        # 调用js代码获取:请求url、请求的参数、以及请求的header
        result = ctx.call("getParameters", server_url, decrypt_url, ua)
        # print(result)
        # https://player.ddzyku.com:3653/api/get_play_url?app_key=22f5d5ab331a4e97bf4c3e765d61e437&client_key=cd1da34f8e79f73eb8f5bd74198e6c51&request_token=fc0969c6271d642b999a27fe6eff3d09&access_token=e3866b316ca3aca59d4201969c98f7fc
        url = result["url"]
        data = result["data"]
        url = str(url+"?app_key="+data['app_key']+"&client_key"+data['client_key']+"&request_token"+data['request_token']+'&access_token'+data['access_token']).replace("+","%2B")
        headers = result["headers"]
        # http请求获取m3u8的链接        
        x = requests.get(url, headers=headers, verify=False)
        # print(x)
        # print(x.text)
        # print('开始解密:')
        # 解密http请求的结果
        dicstr = ctx.call("decryptPackData",x.text)
        # print(dicstr)
        # {"code":200,"data":{"url":"https:\/\/s6.bfzycdn.com\/video\/mishilieche\/HD\/index.m3u8"}}
        # dic["code"] == 200
        # 将解密的数据转为json
        dic = json.loads(dicstr)
        # print('得到解密的json')
        # print(dic)
        m3u8Url = dic["data"]["url"]
        print('得到m3u8的url')
        print(m3u8Url)
        return m3u8Url, videoName
# 示例用法
# videoUrl = 'https://dmxq.fun/vodplay/153042-6-1.html'
# m3u8Url, videoName = parseDmxqM3u8(videoUrl)
5.2 m3u8下载以及转码(需ffmpeg命令)

在拿到m3u8链接后,就可以开始下载了,写了一个m3u8下载库,可单独使用。
下载时,需要安装ffmpeg命令版本用来转码,若不安装,就是单个的ts文件。

一共3个源文件:m3u.py(m3u8下载的单独入口)、m3u8dl.py(核心下载类)、transcode.py(合并ts并转码mp4)
简述下载过程:
1、通过m3u8库,解析m3u8内容,得到所有的文件的url(如ts文件、加密key的文件)
2、通过m3u8库,将m3u8内容中的文件链接替换为本地的ts等文件的链接,然后保存m3u8文件到本地
3、使用ffmpeg通过本地的m3u8文件将ts解密(若需要)合并转码为mp4文件
简述转码过程:
通过python调用ffmpeg转码命令,并获取转码结果。
6、Demo及说明
Demo实现了输入视频url,自动解析获取m3u8并开始下载,完成后合并转码为mp4。在使用时,请安装ffmpeg命令,也可手动改写代码自行合并ts文件。
使用示例:
python3 main.py https://dmxxxxxx.html 视频名称 "./mp4Download"
直接下载m3u8示例:
python3 m3u.py https://hls.cntv.cdn20.com/asp/hls/1200/0303000a/3/default/1049a60b8b914d4ab7066c568ca616fd/1200.m3u8 小孩子大梦想
如下图:


16 demo使用以及说明.jpg (501.68 KB, 下载次数: 2)
下载附件
16
2023-11-28 10:07 上传

demo地址
链接:https://pan.baidu.com/s/17LvMVfmfQXQoUc_FUyzkww  密码:jxwt
总结
拖拖拉拉的写了起码一个星期,但还好没有放弃。因水平有限,文中若有错误之处,望指出,若demo有任何bug,请自行调试修改,本人概不负责(主要是菜)。
最后希望本文对大家有所帮助,共同进步!!!

函数, 参数

charlesz97   

楼主的帖子写的很详细,代码也有清晰注释,跟着看看能学到很多,感谢。
本地跑了,一切正常。只有一个不是问题的问题,视频名字如果带空格ffmpeg合并时会截断路径转码失败,如“XXX 第01集”。当然直接不输入空格就没事。
解决方式是transcode.py的ffmpeg_command变量,{m3u8Path}和{mp4Path}左右都套上引号\",给其他读帖的人
dork   

采用这套系统源码的,说实话还是不少的。给楼主加鸡腿
ruikai   

牛啊 牛啊 牛啊
Ldormant   

前几天我也做过这个地址得 逆向 ,但是当时给我卡爆了  结果就果断放弃了
geesehoward   

这网站防破解也真够费心的,搞了这么多坑,佩服楼主思路的同时,更佩服楼主的耐心。
孤竹君312   

感谢楼主分享
colinjian22   

好好学习下才行
LiXieZengHui   

牛哇,之前做这个无限debug试着搞过去,但是最后卡爆了,遗憾退场。
a5862253   

大佬确实厉害 。
您需要登录后才可以回帖 登录 | 立即注册

返回顶部