本帖最后由 残剑丶 于 2023-11-04 11:43 编辑
前言需要了解m3u8基础知识。 初步分析目标站点:aHR0cHM6Ly93d3cuOTJneXcuY29tL3Nob3J0VmlkZW8vYWxidW0vMjA1P3ZpZGVvSWQ9NDY4NQ== 打开浏览器,按下F12开发者工具,然后粘贴链接进入页面。 在上方点击 Network 菜单栏(在这里可以监视浏览器与服务器之间的网络请求和响应。你可以查看请求的详细信息、响应的状态码和内容,并分析网络性能。) 接下来就是分析 m3u8和key 的链接 观察m3u8,发现是标准格式,并没有经过加密处理 接着观察key,发现是16字节
可事实真的如此吗?打开 M3U8批量下载器 试试。 https://www.dslt.tech/article-4101-1.html 粘贴m3u8链接,点击【添加】按钮 再点击【全部开始】按钮 提示 文件解码失败,请检查key是否正确
此时有2种可能,我们依次分析 第1种可能:key链接,存在次数限制,在浏览器打开了首次,然后第2次访问,不给数据或者是假数据(key) 在连续多次请求,与浏览器首次响应,对比发现是一致的字节,那么就不是这个原因。 第2种可能:key密钥的响应值被加密了,市面上别的平台通常是返回32位或者更长的字节。 此时就得js逆向了,分析 hls.js 文件,这里面有做特殊处理,播放前会解密出正确的密钥。 找到 001.ts,菜单栏点击 Initiator ,这是 js函数调用堆栈,然后进去下断点,动态调试分析。 这个过程比较繁琐,需要了解 Hls.js 的加载过程。 当然我是这么分析出的,教大家一招简单的技巧。 我们换一种思路,通过关键词来定位 aes-128 decryptdata.key buffer
观察到这行有点可疑,可能是个解密函数,又将key和iv传递进去。 let _ = C.softwareDecrypt(n, R.key.buffer, R.iv.buffer);
点击行号,到这里下断,然后F5重新加载网页。 观察发现,n是ts的文件数据,R里面有我们想要的key和iv数据。 key [101, 77, 103, 113, 121, 121, 112, 108, 55, 84, 71, 79, 55, 99, 65, 98]
iv [55, 108, 98, 109, 52, 103, 113, 57, 106, 114, 108, 66, 50, 86, 67, 49]
主要是key,在python中转成hex十六进制,看看。 b = bytearray([101, 77, 103, 113, 121, 121, 112, 108, 55, 84, 71, 79, 55, 99, 65, 98]) hex_str = b.hex() print(hex_str) # 654d67717979706c3754474f37634162
然后打开 m3u8下载器,自定义key下载,试试 神奇的一幕发生了,居然下载成功了 那么真实的key就是 [101, 77, 103, 113, 121, 121, 112, 108, 55, 84, 71, 79, 55, 99, 65, 98]
但.key文件,返回的却是 [128, 245, 244, 139, 212, 166, 215, 255, 208, 226, 106, 4, 240, 217, 14, 134]
我们需要得知是如何互转的,这个过程就是js逆向,继续回到浏览器。 在右侧 调用堆栈(Call Stack),需要逐个分析出key是在哪里赋值的。 观察到 play-utils-508447cf.js 的 onSuccess 关键词附近,貌似在请求并取回响应,到这里下断看看。 然后刷新网页 const pt = new ut; const mt = lt("nt:main/lib/js/ntPlayer") , z = class extends nt.DefaultConfig.loader { constructor(e) { super(e); d(this, "_superLoad"); this._superLoad = super.load.bind(this) } load(e, i, a) { if (e.keyInfo) { const p = a.onSuccess; a.onSuccess = function(E, g, c, v) { const y = E.data , $ = new DataView(y); if (z._revise) { const b = z._revise; $.setInt32(0, $.getInt32(0) ^ b[0]), $.setInt32(4, $.getInt32(4) ^ b[1]), $.setInt32(8, $.getInt32(8) ^ b[2]), $.setInt32(12, $.getInt32(12) ^ b[3]), E.data = $.buffer } p(E, g, c, v) } } this._superLoad(e, i, a) } static setRevise(e) { z._revise = e } } ;
z._revise [3854078970, 2917115795, 3887476043, 3350876132]
发现该变量,在下面有用到,于是全局搜索,共4处引用,挨个下断,再次刷新网页 发现这里的入参是这些值,那么从堆栈里往上跟,就能找出来了。 const t = document.querySelector(".player[nt-main-skey]") , e = document.querySelector("#app-key") , i = document.querySelector(".end-tips") , a = document.querySelector("[nt-buy-vip]") , l = parseInt(t.getAttribute("nt-video-id") || "0") , p = t.getAttribute("nt-main-poster"); this.existSrt = t.getAttribute("nt-srt") == "1", this.skey = t.getAttribute("nt-main-skey") || ""; const E = JSON.parse(`[${e.dataset.keys}]`); t.removeAttribute("nt-main-poster"), t.removeAttribute("nt-main-skey"), t.removeAttribute("nt-srt"), e.remove(), this.playLog = new Et, this.pageState = JSON.parse(localStorage.getItem(st) || "{}"), tt("pageState: %o", this.pageState), this.player = new _t(t,{ poster: p || "", logo: ct, logoWidth: 120, autoplay: !0, muted: this.pageState.muted || !1, volume: this.pageState.volume || 1, playbackRate: this.pageState.playbackRate || 1, enablePlaybackRate: !0, enableCue: this.existSrt, revise: E });
于是找到了关键位置 e = document.querySelector("#app-key") ... const E = JSON.parse(`[${e.dataset.keys}]`); ...
querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。 于是到 HTML 里搜索 app-key 看看 果然找到了 <div id="app-key" data-keys="3854078970,2917115795,3887476043,3350876132"></div>
接着在Nodejs补环境,完成脱机调用,也就是还原解密过程。 纯算法 Nodejsvar z = { '_revise' : [3854078970, 2917115795, 3887476043, 3350876132] // app-key 的值 } // console.log(z)
var key = [128, 245, 244, 139, 212, 166, 215, 255, 208, 226, 106, 4, 240, 217, 14, 134]; // 网页 .key文件返回的加密值
let buffer = new ArrayBuffer(16); let data = new Uint8Array(key); let view = new DataView(buffer); for (let i = 0; i < data.length; i++) { view.setUint8(i, data[i]); }
var T = new DataView(view.buffer); if (z._revise) { const L = z._revise; T.setInt32(0, T.getInt32(0) ^ L[0]); T.setInt32(4, T.getInt32(4) ^ L[1]); T.setInt32(8, T.getInt32(8) ^ L[2]); T.setInt32(12, T.getInt32(12) ^ L[3]); ok = T.buffer
dec_16 = new Uint8Array(ok)+""; console.log(dec_16); // 101,77,103,113,121,121,112,108,55,84,71,79,55,99,65,98 }
封装成解密函数 function decrypt(app_key, en_key) { var z = { '_revise': app_key }
let buffer = new ArrayBuffer(16); let data = new Uint8Array(en_key); let view = new DataView(buffer); for (let i = 0; i < data.length; i++) { view.setUint8(i, data[i]); }
var T = new DataView(view.buffer); if (z._revise) { const L = z._revise; T.setInt32(0, T.getInt32(0) ^ L[0]); T.setInt32(4, T.getInt32(4) ^ L[1]); T.setInt32(8, T.getInt32(8) ^ L[2]); T.setInt32(12, T.getInt32(12) ^ L[3]); ok = T.buffer;
dec_16 = new Uint8Array(ok); }
return dec_16 }
var app_key = [3854078970, 2917115795, 3887476043, 3350876132]; var en_key = [128, 245, 244, 139, 212, 166, 215, 255, 208, 226, 106, 4, 240, 217, 14, 134];
var ok = decrypt(app_key, en_key); console.log(ok); console.log(ok + "" == [101, 77, 103, 113, 121, 121, 112, 108, 55, 84, 71, 79, 55, 99, 65, 98]);
Blob和ArrayBuffer是什么鬼?最早是数据库直接用Blob来存储二进制数据对象,这样就不用关注存储数据的格式了。在web领域,Blob对象表示一个只读原始数据的类文件对象,虽然是二进制原始数据但是类似文件的对象,因此可以像操作文件对象一样操作Blob对象。 ArrayBuffer对象用来表示通用的、固定长度的原始二进制数据缓冲区。我们可以通过new ArrayBuffer(length)来获得一片连续的内存空间,它不能直接读写,但可根据需要将其传递到TypedArray视图或 DataView 对象来解释原始缓冲区。实际上视图只是给你提供了一个某种类型的读写接口,让你可以操作ArrayBuffer里的数据。TypedArray需指定一个数组类型来保证数组成员都是同一个数据类型,而DataView数组成员可以是不同的数据类型。
TypedArray视图的类型数组对象有以下几个: Int8Array:8位有符号整数,长度1个字节。 Uint8Array:8位无符号整数,长度1个字节。 Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。 Int16Array:16位有符号整数,长度2个字节。 Uint16Array:16位无符号整数,长度2个字节。 Int32Array:32位有符号整数,长度4个字节。 Uint32Array:32位无符号整数,长度4个字节。 Float32Array:32位浮点数,长度4个字节。 Float64Array:64位浮点数,长度8个字节。 Int8Array:1 个字节的有符号整数类型,范围在 -128 ~ 127 之间; Uint8Array:1 个字节的无符号整数类型,范围在 0 ~ 255 之间; Uint16Array:2 个字节的无符号整数类型,范围在 0 ~ 65535 之间; Int16Array:2 个字节的有符号整数类型,范围在 -32768 ~ 32767 之间; Uint32Array:4 个字节的无符号整数类型,范围在 0 ~ 4294967295 之间; Float32Array:4 个字节的单精度浮点数类型; Float64Array:8 个字节的双精度浮点数类型。
DIV标签的 app-key 值 Uint32Array [3854078970, 2917115795, 3887476043, 3350876132]
Uint8Array [229, 184, 147, 250, 173, 223, 167, 147, 231, 182, 45, 75, 199, 186, 79, 228]
加密key文件返回的是 Uint8Array [128, 245, 244, 139, 212, 166, 215, 255, 208, 226, 106, 4, 240, 217, 14, 134]
hex 80f5f48bd4a6d7ffd0e26a04f0d90e86
真实key Uint8Array [101, 77, 103, 113, 121, 121, 112, 108, 55, 84, 71, 79, 55, 99, 65, 98]
十六进制(hex) 654d67717979706c3754474f37634162
字符串 eMgqyypl7TGO7cAb
Uint32Array 转换 Uint8Arraylet uint32Arr = new Uint32Array([1902595429, 1819310457, 1330074679, 1648452407]);
// 创建新的Uint8Array,长度为Uint32Array的4倍 let uint8Arr = new Uint8Array(uint32Arr.length * 4);
// 遍历Uint32Array并转换每个元素为4个字节序列 for (let i = 0; i < uint32Arr.length; i++) { let value = uint32Arr[i]; for (let j = 0; j < 4; j++) { // 将32位值右移8位(除以256),然后取低8位作为8位无符号整数 uint8Arr[i * 4 + j] = value >> 8 * j & 0xFF; } }
console.log(uint8Arr); // 输出转换后的Uint8Array // [101, 77, 103, 113, 121, 121, 112, 108, 55, 84, 71, 79, 55, 99, 65, 98]
纯算法 Pythondef int_to_bytes(n): # 使用 '>I' 表示大端序(Most Significant Byte First)的无符号整数格式 return n.to_bytes((n.bit_length() + 7) // 8, 'big')
def decrypt(app_key,en_key): int_list = app_key byte_list = [int_to_bytes(i) for i in int_list] app_key = b''.join(byte_list) # app_key = bytes([229, 184, 147, 250, 173, 223, 167, 147, 231, 182, 45, 75, 199, 186, 79, 228]) # print(list(app_key))
# 对每个字节进行异或操作 app_key_xor = bytes([app_key[i] ^ en_key[i] for i in range(len(app_key))]) return app_key_xor
app_key = [3854078970, 2917115795, 3887476043, 3350876132] en_key = bytes([128, 245, 244, 139, 212, 166, 215, 255, 208, 226, 106, 4, 240, 217, 14, 134])
ok = decrypt(app_key,en_key) print(ok) print(list(ok)) print(ok.decode() == 'eMgqyypl7TGO7cAb') print(ok.hex() == '654d67717979706c3754474f37634162')
完 。
注:若转载请注明大神论坛来源(本贴地址)与作者信息。
|