某网课平台m3u8 key分析以及脚本下载

查看 93|回复 9
作者:boomx7   
获取m3u8 key
1.播放视频,搜索key关键字,找到请求key文件


image.png (279.34 KB, 下载次数: 1)
下载附件
搜索key
2024-12-5 10:30 上传

2.下载key之后发现,是33字节文件,这明显不是正常的key(正常为16字节)


image.png (56.19 KB, 下载次数: 1)
下载附件
key文件信息
2024-12-5 10:31 上传

3.接下来就去js,看看是哪个程序调用这个请求,发现有个叫onkeyload的堆栈,感觉像生成key的程序,进去看看


image.png (303.56 KB, 下载次数: 0)
下载附件
js调试1
2024-12-5 10:42 上传

4.把onkeyload这段设置断点,刷新浏览器并播放,发现key没获取到,


image.png (566.16 KB, 下载次数: 0)
下载附件
获取key1
2024-12-5 11:28 上传

5.但发现loadsuccess这段函数,把这段也打个断点。重新播放视频,发现有16字节的key了,拿去下载试试。


image.png (367.73 KB, 下载次数: 0)
下载附件
获取key2
2024-12-5 11:31 上传

6.先把这16字节转换成base64先得到:uGD7jfQ3cVBooMDg7tv7PA==
7.打开m3u8发现没补全链接,手动补全一下


image.png (112.46 KB, 下载次数: 0)
下载附件
m3u8文件
2024-12-5 11:34 上传



image.png (158.35 KB, 下载次数: 0)
下载附件
m3u8文件2
2024-12-5 11:35 上传

8.使用逍遥一仙 大神的m3u8下载工具,成功下载。


image.png (166.25 KB, 下载次数: 0)
下载附件
2024-12-5 11:39 上传

编写批量下载脚本
  • 既然要编写批量脚本,最方便的当然是尝试获取加密算法
  • 继续跟踪js,多次调试发现loadsuccess,上一个堆栈为r.onsuccess,进去瞧一瞧

         


    image.png (262.92 KB, 下载次数: 0)
    下载附件
    2024-12-5 12:00 上传

       2.发现key是从gt这里传过来的,继续跟。
       


    image.png (361.11 KB, 下载次数: 0)
    下载附件
    2024-12-5 13:40 上传

      3.发现是从decoderModule来的,继续
      


    image.png (183.25 KB, 下载次数: 0)
    下载附件
    2024-12-5 13:44 上传

    [color=] 4.跟进来发现是wasm,研究半天搞不懂算了算了。。。开摆!
      


    image.png (214.46 KB, 下载次数: 0)
    下载附件
    2024-12-5 13:47 上传

  • 换思路:直接从gt这截取数组,通过油猴脚本,发送base64到本地py程序,py程序用来修改key链接,最后直接把m3u8文件拖进下载器下载。
    这里就直接贴代码了,代码基本来自gpt。
    [color=]油猴脚本
    // ==UserScript==
    // @name         推送base64密码到python
    // @namespace    http://tampermonkey.net/
    // @version      0.1
    // @description  请求完某个API,打印其中对应的全局参数
    // @AuThor       Your Name
    // @match        https://www.xiao.com/*
    // @grant        GM_xmlhttpRequest
    // @connect *
    // ==/UserScript==
    (function() {
        'use strict';
        // 目标API地址
        const targetURL = 'https://www.xiao.com/courselab/public/v1.0/course-videos:play';
        // 保存原始的 XMLHttpRequest 原型方法
        const originalOpen = XMLHttpRequest.prototype.open;
        const originalSend = XMLHttpRequest.prototype.send;
        // 覆盖 open 方法
        XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
            this._url = url; // 保存请求的 URL
            return originalOpen.apply(this, arguments);
        };
        // 覆盖 send 方法
        XMLHttpRequest.prototype.send = function(data) {
            if (this._url === targetURL) {
                // 监听 readyState 变化
                this.addEventListener('readystatechange', function() {
                    if (this.readyState === 4 && this.status === 200) {
                        try {
                            setTimeout(() => {
                                // 将响应处理为 ArrayBuffer
                                const buffer = this.response;
                                // 将 ArrayBuffer 转换为 Uint8Array
                                const uint8Array = new Uint8Array(decoderModule.HEAPU8.buffer,5244968,16);
                                console.log('uint8Array Data:', uint8Array);
                                // 将 Uint8Array 转换为 Base64
                                const base64String = uint8ArrayToBase64(uint8Array);
                                // 打印 Base64 编码的数据
                                console.log('Base64 Data:', base64String);
                                // 打印videoId
                                console.log('Payload Data:', data);
                                const videoId = JSON.parse(data).videoId;
                                console.log('videoId:', videoId);
                                // 复制 Base64 数据到剪贴板
                                // copyToClipboard(base64String);
                                // 带着 Base64 字符串请求本地服务器
                                send(base64String,videoId);
                                // 你可以在此处对 Uint8Array 数据进行进一步处理
                            }, 500); // 延迟500毫秒
                        } catch (error) {
                            console.error('Error processing ArrayBuffer:', error);
                        }
                    }
                });
                // 设置 responseType 为 arraybuffer
                this.responseType = 'arraybuffer';
            }
            // 调用原始的 send 方法
            return originalSend.apply(this, arguments);
        };
        // 将 Uint8Array 转换为 Base64 编码的函数
        function uint8ArrayToBase64(uint8Array) {
            let binaryString = '';
            for (let i = 0; i  {
                console.log('Base64 data copied to clipboard');
            }).catch(err => {
                console.error('Failed to copy data to clipboard:', err);
            });
        }
        function send(base64String,videoId) {
            'use strict';
            // Format data
            let data = `#KEY,${base64String}\r\n`;
            // Convert data to GBK encoding
            let gbkEncodedData = btoa(unescape(encodeURIComponent(data)));
            // Function to send data
            function sendData() {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'http://127.0.0.1:8789/modify-file',
                    data: JSON.stringify({
                        video_id: videoId,
                        base64_string: base64String
                    }),
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    onload: function(response) {
                        console.log('Response from server:', response.responseText);
                    },
                    onerror: function(error) {
                        console.error('Error communicating with server:', error);
                    }
                });
            }
            // Call the sendData function
            sendData();
        }
    })();
    [color=]py程序(需要手动设置文件夹)
    from flask import Flask, request, jsonify
    import os,re
    app = Flask(__name__)
    # 设置文件夹路径
    TARGET_FOLDER = "E:\study\Python\m3u8"  # 修改为你的目标文件夹路径
    @app.route('/modify-file', methods=['POST'])
    def modify_file():
        # 获取请求参数
        data = request.json
        video_id = data.get("video_id")
        base64_string = data.get("base64_string")
        if not video_id or not base64_string:
            return jsonify({"error": "缺少参数video_id和base64_string"}), 400
        # 搜索目标文件
        target_file = None
        for root, dirs, files in os.walk(TARGET_FOLDER):
            for file in files:
                if video_id in file:
                    target_file = os.path.join(root, file)
                    break
            if target_file:
                break
        if not target_file:
            return jsonify({"error": f"No file found with video_id: {video_id}"}), 404
        try:
            # 读取文件内容
            with open(target_file, 'r', encoding='utf-8') as f:
                content = f.read()
                print('搜索到文件:',target_file)
            # 替换内容
            modified_content = re.sub(r'URI=".*?"', f'URI="base64:{base64_string}"', content)
            # 保存修改后的文件
            with open(target_file, 'w', encoding='utf-8') as f:
                f.write(modified_content)
            return jsonify({"message": f"File {target_file} modified successfully"}), 200
        except Exception as e:
            return jsonify({"error": f"Failed to modify file: {str(e)}"}), 500
    if __name__ == '__main__':
        # 运行服务器,监听端口 8788
        app.run(host='0.0.0.0', port=8789)

    下载次数, 下载附件

  • lxxfhtd   

    谢谢分享
    johnsonbo   

    虽然看不懂,不过感谢分享
    鹿鸣   

    发个地址学习下  地址编码下
    xuwei179   

    学好数理化,走遍天下都不怕
    Tianshan   

    机器可以解决的问题,决不再手动
    SherlockProel   

    我看懂了,先这样再那样,相当于勾股定理
    boomx7
    OP
      


    鹿鸣 发表于 2024-12-5 16:38
    发个地址学习下  地址编码下

    aHR0cHM6Ly93d3cueGlhb3FpcWlhby5jb20v
    不过需要账号权限
    m066061   

    不知道怎么用代码
    鹿鸣   


    boomx7 发表于 2024-12-5 17:29
    aHR0cHM6Ly93d3cueGlhb3FpcWlhby5jb20v
    不过需要账号权限

    好的谢谢
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部