吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 14193|回复: 266
收起左侧

[原创] 某网课m3u8视频流hls.js算法逆向

    [复制链接]
OVVO 发表于 2023-11-2 19:52
本帖最后由 OVVO 于 2023-11-2 22:07 编辑

前言

需要了解m3u8基础知识,可在下方链接阅读笔者之前的文章。
https://www.52pojie.cn/thread-1686788-1-1.html

初步分析

目标站点:aHR0cHM6Ly93d3cuOTJneXcuY29tL3Nob3J0VmlkZW8vYWxidW0vMjA1P3ZpZGVvSWQ9NDY4NQ==

打开浏览器,按下F12开发者工具,然后粘贴链接进入页面。

在上方点击 Network 菜单栏(在这里可以监视浏览器与服务器之间的网络请求和响应。你可以查看请求的详细信息、响应的状态码和内容,并分析网络性能。)

1 Snipaste_2023-11-02_14-14-21.png

接下来就是分析 m3u8和key 的链接

2 Snipaste_2023-11-02_14-21-49.png

观察m3u8,发现是标准格式,并没有经过加密处理
接着观察key,发现是16字节

3 Snipaste_2023-11-02_14-24-21.png

可事实真的如此吗?打开 M3U8批量下载器 试试。

M3U8批量下载器 V1.4.8 0508【5月8日更新】
https://www.52pojie.cn/thread-1631141-1-1.html

4 Snipaste_2023-11-02_14-27-08.png

粘贴m3u8链接,点击【添加】按钮

5 Snipaste_2023-11-02_14-29-12.png

再点击【全部开始】按钮

6 Snipaste_2023-11-02_14-30-10.png

提示

文件解码失败,请检查key是否正确

此时有2种可能,我们依次分析

第1种可能:key链接,存在次数限制,在浏览器打开了首次,然后第2次访问,不给数据或者是假数据(key)

7 Snipaste_2023-11-02_14-46-04.png

在连续多次请求,与浏览器首次响应,对比发现是一致的字节,那么就不是这个原因。

第2种可能:key密钥的响应值被加密了,市面上别的平台通常是返回32位或者更长的字节。

此时就得js逆向了,分析 hls.js 文件,这里面有做特殊处理,播放前会解密出正确的密钥。

8 Snipaste_2023-11-02_15-00-34.png

找到 001.ts,菜单栏点击 Initiator ,这是 js函数调用堆栈,然后进去下断点,动态调试分析。

这个过程比较繁琐,需要了解 Hls.js 的加载过程。

9 hls v2-e0c062dd742f1a53748216c90a38a710_720w.png

当然我是这么分析出的,教大家一招简单的技巧。

我们换一种思路,通过关键词来定位

aes-128
decryptdata.key
buffer

9 Snipaste_2023-11-02_15-11-19.png

观察到这行有点可疑,可能是个解密函数,又将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十六进制,看看。

10 Snipaste_2023-11-02_15-17-07.png


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下载,试试

11 Snipaste_2023-11-02_15-19-35.png

12 Snipaste_2023-11-02_15-21-49.png

神奇的一幕发生了,居然下载成功了

那么真实的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逆向,继续回到浏览器。

13 Snipaste_2023-11-02_15-30-36.png

在右侧 调用堆栈(Call Stack),需要逐个分析出key是在哪里赋值的。

观察到  play-utils-508447cf.js 的 onSuccess 关键词附近,貌似在请求并取回响应,到这里下断看看。

14 Snipaste_2023-11-02_15-35-18.png

然后刷新网页

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
    }
}
;

15 Snipaste_2023-11-02_15-39-19.png

z._revise
[3854078970, 2917115795, 3887476043, 3350876132]

发现该变量,在下面有用到,于是全局搜索,共4处引用,挨个下断,再次刷新网页

16 Snipaste_2023-11-02_15-43-47.png

发现这里的入参是这些值,那么从堆栈里往上跟,就能找出来了。

17 Snipaste_2023-11-02_15-45-38.png

        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
        });

18 Snipaste_2023-11-02_15-47-04.png

于是找到了关键位置

e = document.querySelector("#app-key")
...
const E = JSON.parse(`[${e.dataset.keys}]`);
...

querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。

于是到 HTML 里搜索 app-key 看看

19 Snipaste_2023-11-02_15-50-07.png

果然找到了

<div id="app-key" data-keys="3854078970,2917115795,3887476043,3350876132"></div>

接着在Nodejs补环境,完成脱机调用,也就是还原解密过程。

纯算法 Nodejs

20 Snipaste_2023-11-02_15-55-56.png


var 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是什么鬼?

  1. 最早是数据库直接用Blob来存储二进制数据对象,这样就不用关注存储数据的格式了。在web领域,Blob对象表示一个只读原始数据的类文件对象,虽然是二进制原始数据但是类似文件的对象,因此可以像操作文件对象一样操作Blob对象。

  2. 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 转换  Uint8Array


let 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]

纯算法 Python


def 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')

21 Snipaste_2023-11-02_19-04-58.png

免费评分

参与人数 113吾爱币 +108 热心值 +101 收起 理由
shenghuo2 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
modesty88 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
3lizabeth + 1 + 1 用心讨论,共获提升!
JiangtaoChiu + 1 + 1 细致,贴心,手把手教学,谢谢楼主。
笙若 + 1 + 1 谢谢@Thanks!
Explorer12138 + 1 + 1 我很赞同!
jerrydeng + 1 谢谢@Thanks!
xintian + 1 + 1 鼓励转贴优秀软件安全工具和文档!
吾爱Po解啊 + 1 + 1 我很赞同!
南方路人 + 1 谢谢@Thanks!
宇宙狂人 + 1 + 1 用心讨论,共获提升!
chgr123 + 1 + 1 谢谢@Thanks!
laironggui + 1 + 1 我很赞同!
saltedfish1019 + 1 不明觉厉~~~~!!!!!!!
gjdjjwzx + 1 + 1 用心讨论,共获提升!
windyvincent + 1 + 1 感谢分享
川普 + 1 + 1 用心讨论,共获提升!
ws001980 + 1 + 1 谢谢@Thanks!
dext1231 + 1 + 1 大佬思路厉害
18and02 + 1 谢谢@Thanks!
CCTV13 + 1 我很赞同!
xixiyo + 1 热心回复!
li542182964 + 1 + 1 谢谢@Thanks!
jiyuecaiyun968 + 1 + 1 谢谢@Thanks!
dudupangle + 1 + 1 我很赞同!
whyso + 1 + 1 用心讨论,共获提升!牛人啊!
zhaocz + 1 + 1 用心讨论,共获提升!受益匪浅!!!
luolifu + 1 + 1 谢谢@Thanks!
CryUshio + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
qdnim112 + 1 + 1 用心讨论,共获提升!
yangfu + 1 + 1 用心讨论,共获提升!
aAChengYay + 1 + 1 用心讨论,共获提升!
llq@xsh + 1 + 1 我很赞同!
沉寂黄昏 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ird_wen + 1 + 1 用心讨论,共获提升!
mEIhUAlU123 + 1 + 1 热心回复!
把钱藏内裤里 + 1 热心回复!
foxfoxfoxfox + 1 我很赞同!
tryit + 1 + 1 谢谢@Thanks!
aigc + 1 热心回复!
LoveCode + 1 + 1 我很赞同!
ufldh + 1 + 1 谢谢@Thanks!
zscasd1 + 1 太厉害了
alexdev + 1 我很赞同!
sddpxy + 1 + 1 热心回复!
dongxili + 1 + 1 用心讨论,共获提升!
有人氏 + 1 我很赞同!
make666 + 1 + 1 用心讨论,共获提升!
ccqzzx + 1 + 1 用心讨论,共获提升!
XMQ + 1 我很赞同!
luxiyv456456 + 1 &amp;lt;font style=&amp;quot;vertical-align: inherit;&amp;quot;&amp;gt;&amp;lt;font style=
sanmylc + 1 + 1 谢谢@Thanks!
lean16 + 1 我很赞同!
wodes + 1 用心讨论,共获提升!
hhengzui + 1 我很赞同!
jiaotong + 1 + 1 大佬好强!
李玉风我爱你 + 3 + 1 我很赞同!
fkyangmi + 1 + 1 我很赞同!
DM4406 + 1 + 1 谢谢@Thanks!
10230564 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
linzhoulxyz + 1 + 1 用心讨论,共获提升!
shiqiangge + 1 我很赞同!
svenxu + 1 + 1 我很赞同!
changesmile + 1 + 1 谢谢@Thanks!
xiaohaisec + 1 + 1 用心讨论,共获提升!
qian408 + 1 + 1 用心讨论,共获提升!
WUAI2023NXD + 1 我很赞同!
wlxxlhh + 1 + 1 热心回复!
chuan9 + 1 + 1 谢谢@Thanks!
Spid3r + 1 + 1 谢谢@Thanks!
fengmi19 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
Damon0506 + 1 + 1 我很赞同!
cao1129 + 1 保姆级教程
abwuge + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
gqdsc + 1 + 1 大佬永远是大佬
Kristin_ + 1 我很赞同!
长得太帅很烦 + 1 + 1 谢谢@Thanks!
cocos327 + 1 + 1 太强了吧,大佬!
寻找乐趣 + 1 + 1 谢谢@Thanks!
小叔sir + 1 + 1 这简直是什么样的存在了,佩服啊~
jxhuangwei + 1 + 1 每天学一篇,方法小技巧!
fanruinet + 1 + 1 非常有帮助,谢谢@Thanks!
gz7uuuuuuuuu + 1 + 1 用心讨论,共获提升!
starcrafter + 1 + 1 用心讨论,共获提升!
wendaochangshen + 1 用心讨论,共获提升!
zhengsg5 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ruoyoo + 1 我很赞同!
jason76wong + 1 + 1 我很赞同!
ziang + 1 + 1 我很赞同!
yjn866y + 1 + 1 谢谢@Thanks!
zd53011 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
坏蟑螂 + 1 + 1 谢谢@Thanks!
LuckyClover + 1 + 1 热心回复!
文蛮 + 1 + 1 用心讨论,共获提升!
GuMoon + 1 我很赞同!
杨辣子 + 1 + 1 谢谢@Thanks!
诚实的笑了 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
purefeeling + 1 + 1 用心讨论,共获提升!
prince_cool + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

tian321 发表于 2023-11-2 22:30
思路严谨细致,佩服!
lfer 发表于 2023-11-3 09:06
hwjsj00101 发表于 2023-11-3 09:21
jim19 发表于 2023-11-9 17:32
顺着堆栈网上调试的时候, 因为异步调用的问题, 找不到源头, 因此直接一路key搜索反倒快速解决了。这个教程不太适合新手, 很多功能以及线索是断开的
小豆丁 发表于 2023-11-3 08:25
未免也太厉害了吧                       
lea999 发表于 2023-11-2 20:03
厉害啊!多谢分享。
wasm2023 发表于 2023-11-2 20:25
厉害,感谢分享
Eqwer 发表于 2023-11-2 20:40
很详细,学习到了
baliao 发表于 2023-11-2 20:44

厉害,感谢分享
bohong65 发表于 2023-11-2 21:27
很棒的思路,学习了
seapsy 发表于 2023-11-2 22:00
厉害,感谢分享
helloword121 发表于 2023-11-2 22:05
大佬厉害了,感谢分享
fjunqwq 发表于 2023-11-2 22:17
厉害,学习一下

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
yn12322 + 1 + 1 我很赞同!

查看全部评分

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-28 17:00

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表