• [C#/Java] 针对 QINIU-PROTECTION-10 的m3u8视频文件解密


    源码地址:https://github.com/Myron1024/m3u8_download

    今年上网课很流行,有些在线的课程视频想下载下来到本地看,发现视频的链接是m3u8格式的,下载下来后,提取出视频切片的各个.ts文件的链接,把这些视频片段下载到本地后,却播放不了。于是就花点时间研究研究。网上了解了一下情况,知道视频是加密的, 不过搜了一大圈,都是讲的加密方式为 METHOD=AES-128 的解密方法,可我下载的m3u8文件打开看是 METHOD=QINIU-PROTECTION-10

    了解到解密视频需要key和IV, 我们可以看到 IV在m3u8文件里有,每一个.ts文件都有一个对应的IV,#EXT-X-KEY:后面的 IV=**** 就是我们需要用到的 IV了, 可是key却没有,那就只能从网页上找找了,打开控制台,重新加载页面,发现一个 qiniu-web-player.js 在控制台输出了一些配置信息和日志记录,其中 hls.DRMKey 引起了我的注意

      

    数组长度也是16位,刚好加解密用到的key的长度也是16位,, 所以这个应该就是AES加解密要用到的key了,不过需要先转换一下。。

    网上的方法 转换步骤为:把数组里每一位数字转换成16进制字符串,然后把16进制字符串转为ASCII码,最终拼接出来的结果就是AES的key了。

    C#代码:

    private static string getAESKey(string key)
    {
        string[] arr = key.Split(",");
        string aesKey = "";
        for (int i = 0; i < arr.Length; i++)
        {
            string tmp = int.Parse(arr[i].Trim()).ToString("X");     //10进制转16进制
            tmp = HexStringToASCII(tmp);
            aesKey += tmp;
        }
        return aesKey;
    }
    
    /// <summary>
    /// 十六进制字符串转换为ASCII
    /// </summary>
    /// <param name="hexstring">一条十六进制字符串</param>
    /// <returns>返回一条ASCII码</returns>
    public static string HexStringToASCII(string hexstring)
    {
        byte[] bt = HexStringToBinary(hexstring);
        string lin = "";
        for (int i = 0; i < bt.Length; i++)
        {
            lin = lin + bt[i] + " ";
        }
        string[] ss = lin.Trim().Split(new char[] { ' ' });
        char[] c = new char[ss.Length];
        int a;
        for (int i = 0; i < c.Length; i++)
        {
            a = Convert.ToInt32(ss[i]);
            c[i] = Convert.ToChar(a);
        }
        string b = new string(c);
        return b;
    }

    把js获取的DRMKey数组内容当做字符串传入,获取AES的key

    string DRMKey = "11, 22, 33, 44, 55, 66, 77, 88, 99, 00, 111, 111, 111, 111, 111, 111";
    string aesKey = getAESKey(DRMKey);
    Console.WriteLine("aesKey:" + aesKey);

    现在AES_KEY和IV都有了,可以加解密了,不过这个IV有点特殊,是32位的,我们需要进行切片取前16位,16位是固定位数,必须这么取。

    通过分析页面js代码得知这种AES的加密模式为CBC模式,PaddingMode采用PKCS7.

    加密模式、补码方式、key、IV都有了,剩下的就是编码测试了。

    下面是C#版的完整代码, Java版请看这里

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Security.Cryptography;
    using System.Text;
    using System.Text.RegularExpressions;
    
    namespace VideoDownload
    {
        class Program
        {
            private static List<string> error_arr = new List<string>();
    
            static void Main(string[] args)
            {
                string DRMKey = "11, 22, 33, 44, 55, 66, 77, 88, 99, 00, 111, 111, 111, 111, 111, 111";        //DRMKey
                string m3u8Url = "https://XXXXXXX/123.m3u8";     //m3u8在线地址
                string savePath = "D:\VIDEO\";                //保存的本地路径
                string saveFileName = "VIDEO_FILE_NAME";        //保存的文件(夹)名称,如果为空 则使用默认m3u8文件名
    
                try
                {
                    // 创建本地保存目录
                    int index = m3u8Url.LastIndexOf("/");
                    string dirName = string.IsNullOrEmpty(saveFileName) ? m3u8Url.Substring(index + 1) : saveFileName;
                    string finalSavePath = savePath + dirName + "\";
                    if (!Directory.Exists(finalSavePath))
                    {
                        Directory.CreateDirectory(finalSavePath);
                    }
    
                    // 读取m3u8文件内容
                    string m3u8Content = HttpGet(m3u8Url);
                    //string m3u8Content = File.ReadAllText("D:/test.m3u8");
    
                    string aesKey = getAESKey(DRMKey);
                    //Console.WriteLine("aesKey:" + aesKey);
    
                    Uri uri = new Uri(m3u8Url);
                    string domain = uri.Scheme + "://" + uri.Authority;
                    //Console.WriteLine("m3u8域名为:" + domain);
    
                    List<string> tsList = Regex.Matches(m3u8Content, @"
    (.*?.ts)").Select(m => m.Value).ToList();
                    List<string> ivList = Regex.Matches(m3u8Content, @"IV=(.*?)
    ").Select(m => m.Value).ToList();
                    if (tsList.Count != ivList.Count || tsList.Count == 0)
                    {
                        Console.WriteLine("m3u8Content 解析失败");
                    }
                    else
                    {
                        Console.WriteLine("m3u8Content 解析完成,共有 " + ivList.Count + " 个ts文件");
    
                        for (int i = 0; i < tsList.Count; i++)
                        {
                            string ts = tsList[i].Replace("
    ", "");
                            string iv = ivList[i].Replace("
    ", "");
                            iv = iv.Replace("IV=0x", "");
                            iv = iv.Substring(0, 16);   //去除前缀,取IV前16位
    
                            int idx = ts.LastIndexOf("/");
                            string tsFileName = ts.Substring(idx + 1);
    
                            try
                            {
                                string saveFilepath = finalSavePath + tsFileName;
                                if (!File.Exists(saveFilepath))
                                {
                                    Console.WriteLine("开始下载ts: " + domain + ts);
                                    byte[] encByte = HttpGetByte(domain + ts);
                                    if (encByte != null)
                                    {
                                        Console.WriteLine("开始解密, IV -> " + iv);
                                        byte[] decByte = null;
                                        try
                                        {
                                            decByte = AESDecrypt(encByte, aesKey, iv);
                                        }
                                        catch (Exception e1)
                                        {
                                            error_arr.Add(tsFileName);
                                            Console.WriteLine("解密ts文件异常。" + e1.Message);
                                        }
                                        if (decByte != null)
                                        {
                                            //保存视频文件
                                            File.WriteAllBytes(saveFilepath, decByte);
                                            Console.WriteLine(tsFileName + " 下载完成");
                                        }
                                    }
                                    else
                                    {
                                        error_arr.Add(tsFileName);
                                        Console.WriteLine("HttpGetByte 结果返回null");
                                    }
                                }
                                else
                                {
                                    Console.WriteLine($"文件 {saveFilepath} 已存在");
                                }
                            }
                            catch (Exception ee)
                            {
                                error_arr.Add(tsFileName);
                                Console.WriteLine("发生异常。" + ee);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("发生异常。" + ex);
                }
    
                Console.WriteLine("所有操作已完成. 保存目录 " + savePath);
                if (error_arr.Count > 0)
                {
                    List<string> list = error_arr.Distinct().ToList();
                    Console.WriteLine($"其中 共有{error_arr.Count}个文件下载失败:");
                    list.ForEach(x =>
                    {
                        Console.WriteLine(x);
                    });
                }
                Console.ReadKey();
            }
    
    
            private static string getAESKey(string key)
            {
                string[] arr = key.Split(",");
                string aesKey = "";
                for (int i = 0; i < arr.Length; i++)
                {
                    string tmp = int.Parse(arr[i].Trim()).ToString("X");     //10进制转16进制
                    tmp = HexStringToASCII(tmp);
                    aesKey += tmp;
                }
                return aesKey;
            }
    
            /// <summary>
            /// 十六进制字符串转换为ASCII
            /// </summary>
            /// <param name="hexstring">一条十六进制字符串</param>
            /// <returns>返回一条ASCII码</returns>
            public static string HexStringToASCII(string hexstring)
            {
                byte[] bt = HexStringToBinary(hexstring);
                string lin = "";
                for (int i = 0; i < bt.Length; i++)
                {
                    lin = lin + bt[i] + " ";
                }
                string[] ss = lin.Trim().Split(new char[] { ' ' });
                char[] c = new char[ss.Length];
                int a;
                for (int i = 0; i < c.Length; i++)
                {
                    a = Convert.ToInt32(ss[i]);
                    c[i] = Convert.ToChar(a);
                }
                string b = new string(c);
                return b;
            }
    
            /// <summary>
            /// 16进制字符串转换为二进制数组
            /// </summary>
            /// <param name="hexstring">用空格切割字符串</param>
            /// <returns>返回一个二进制字符串</returns>
            public static byte[] HexStringToBinary(string hexstring)
            {
                string[] tmpary = hexstring.Trim().Split(' ');
                byte[] buff = new byte[tmpary.Length];
                for (int i = 0; i < buff.Length; i++)
                {
                    buff[i] = Convert.ToByte(tmpary[i], 16);
                }
                return buff;
            }
    
            /// <summary>
            /// AES解密
            /// </summary>
            /// <param name="cipherText"></param>
            /// <param name="Key"></param>
            /// <param name="IV"></param>
            /// <returns></returns>
            public static byte[] AESDecrypt(byte[] cipherText, string Key, string IV)
            {
                // Check arguments.
                if (cipherText == null || cipherText.Length <= 0)
                    throw new ArgumentNullException("cipherText");
                if (Key == null || Key.Length <= 0)
                    throw new ArgumentNullException("Key");
                if (IV == null || IV.Length <= 0)
                    throw new ArgumentNullException("IV");
    
                // Declare the string used to hold
                // the decrypted text.
                byte[] res = null;
    
                // Create an AesManaged object
                // with the specified key and IV.
                using (AesManaged aesAlg = new AesManaged())
                {
                    aesAlg.Key = Encoding.ASCII.GetBytes(Key);
                    aesAlg.IV = Encoding.ASCII.GetBytes(IV);
                    aesAlg.Mode = CipherMode.CBC;
                    aesAlg.Padding = PaddingMode.PKCS7;
    
                    // Create a decrytor to perform the stream transform.
                    ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
    
                    // Create the streams used for decryption.
                    using (MemoryStream msDecrypt = new MemoryStream(cipherText))
                    {
                        using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
                        {
                            byte[] tmp = new byte[cipherText.Length + 32];
                            int len = csDecrypt.Read(tmp, 0, cipherText.Length + 32);
                            byte[] ret = new byte[len];
                            Array.Copy(tmp, 0, ret, 0, len);
                            res = ret;
                        }
                    }
                }
                return res;
            }
    
    
            public static string HttpGet(string url)
            {
                try
                {
                    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                    request.Timeout = 20000;
                    var response = (HttpWebResponse)request.GetResponse();
                    using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                    {
                        return reader.ReadToEnd();
                    }
                }
                catch (Exception ex)
                {
                    Console.Write("HttpGet 异常," + ex.Message);
                    Console.Write(ex);
                    return "";
                }
            }
    
            public static byte[] HttpGetByte(string url)
            {
                try
                {
                    byte[] arraryByte = null;
    
                    HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                    request.Timeout = 20000;
                    request.Method = "GET";
                    using (WebResponse wr = request.GetResponse())
                    {
                        int length = (int)wr.ContentLength;
                        using (StreamReader reader = new StreamReader(wr.GetResponseStream(), Encoding.UTF8))
                        {
                            HttpWebResponse response = wr as HttpWebResponse;
                            Stream stream = response.GetResponseStream();
                            //读取到内存
                            MemoryStream stmMemory = new MemoryStream();
                            byte[] buffer1 = new byte[length];
                            int i;
                            //将字节逐个放入到Byte 中
                            while ((i = stream.Read(buffer1, 0, buffer1.Length)) > 0)
                            {
                                stmMemory.Write(buffer1, 0, i);
                            }
                            arraryByte = stmMemory.ToArray();
                            stmMemory.Close();
                        }
                    }
                    return arraryByte;
                }
                catch (Exception ex)
                {
                    Console.Write("HttpGetByte 异常," + ex.Message);
                    Console.Write(ex);
                    return null;
                }
            }
        }
    }

    新建个控制台应用,代码复制过去,改一下最上面的四个参数值就可以运行。本来想做个桌面应用程序的,结果嫌麻烦,费时间就没做了。哪位看官要是有时间可以做个桌面程序方便操作,另外可以加上多线程去下载会快一些。下载解密完之后的ts文件后,使用其他工具合并ts文件或者用windows自带cmd执行以下命令也可以合并文件

    copy /b D:VIDEO*.ts D:VIDEO ewFile.ts

    参考资料:

    python爬虫---破解m3u8 加密   

  • 相关阅读:
    PHP 语法
    PHP 安装
    06_传智播客iOS视频教程_方法的本质是SEL消息
    05_传智播客iOS视频教程_类对象的使用
    04_传智播客iOS视频教程_类是以Class对象存储在代码段
    03_传智播客iOS视频教程_作业讲解及结构体与类的区别
    02_传智播客iOS视频教程_子类在内存中的存储和方法调用过程
    01_传智播客iOS视频教程_课程介绍与知识点回顾
    Day01-Objective-C语法基础-video 01_传智播客iOS视频教程_OC的简要历史
    17_关于上下文的说明
  • 原文地址:https://www.cnblogs.com/myron1024/p/13532379.html
Copyright © 2020-2023  润新知