針對m3u8視頻的ts文件解密


[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, @"\n(.*?.ts)").Select(m => m.Value).ToList();
                List<string> ivList = Regex.Matches(m3u8Content, @"IV=(.*?)\n").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("\n", "");
                        string iv = ivList[i].Replace("\n", "");
                        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\newFile.ts

 

 

出處:https://www.cnblogs.com/myron1024/p/13532379.html

=======================================================================================

我這幾天也下載了網上的視頻,其中m3u8和ts文件格式如下:

 

 其中也是有了AES-128加密的,其中key.key文件的內容就是:c355b7c32d7b97ed

有了pk,還需要iv,分析文件名應該像,但又不符合長度,每個文件名重復一遍就是16位了,試試看,還真蒙對了

這里要說明下:從網上找了很多AES解密的算法都不對,可能是缺少了某些aes加密時的配置。

我這里也根據上面的代碼,根據自己的需求修改了一下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.IO;

namespace AES_128
{
    class Program
    {
        static void Main(string[] args)
        {
            string pk = "";
            string iv = "";
            string strFolder = @"D:\_Del\00";

            if (args.Length != 3)
            {
                Console.WriteLine($"參數異常:未指定待解密文件的目錄,以及public key和iv的值");
                Console.WriteLine($"調用方式:");
                Console.WriteLine($"{AppDomain.CurrentDomain.SetupInformation.ApplicationName} 待解密文件夾名 -pk=值 -iv=值");
                Console.WriteLine($"\r\n\r\n按任意鍵繼續......");
                Console.ReadKey();
                return;
            }
            strFolder = args[0];
            pk = args[1].Split('=')[1];
            iv = args[2].Split('=')[1];

            DirectoryInfo di = new DirectoryInfo(strFolder);
            string tagFolder = strFolder + @"\Dec\";
            DelFolder(tagFolder);
            var fs = di.GetFiles();
            int secessCount = 0;
            List<FileInfo> errList = new List<FileInfo>();
            foreach (var item in fs)
            {
                var fc = ReadFileContent(item.FullName);
                iv = item.Name.Substring(5, 8);
                Console.WriteLine($"開始處理:{item.Name},size={fc.Length}");
                try
                {
                    var fc_de = Comm.AES_EnorDecrypt.AESDecrypt2(fc, pk, iv + iv);
                    string tagFile = tagFolder + item.Name;
                    WriteFileContent(tagFile, fc_de);
                    Console.WriteLine($"解密處理已完成,已保存在:{tagFile},size={fc_de.Length}");
                    secessCount++;
                }
                catch (Exception)
                {
                    errList.Add(item);
                }
            }

            Console.WriteLine($"\r\n解密處理{fs.Count()}個文件,成功解密完成{secessCount}個文件");
            if (errList.Count() > 0)
            {
                Console.WriteLine($"\r\n解密失敗的文件如下:");
                foreach (var item in errList)
                    Console.WriteLine(item.Name);
            }
            Console.ReadKey();
        }


        /// <summary>
        /// AES解密
        /// </summary>
        /// <param name="cipherText"></param>
        /// <param name="Key"></param>
        /// <param name="IV"></param>
        /// <returns></returns>
        public static byte[] AESDecrypt2(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;
        }


        private static byte[] ReadFileContent(string fileName)
        {
            byte[] res = new byte[0];
            using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                try
                {
                    byte[] buffur = new byte[fs.Length];
                    fs.Read(buffur, 0, (int)fs.Length);
                    res = buffur;
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
            return res;
        }

        private static void WriteFileContent(string fileName, byte[] fc)
        {

            if (!Directory.Exists(Path.GetDirectoryName(fileName)))
                Directory.CreateDirectory(Path.GetDirectoryName(fileName));

            using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.Write))
            {
                try
                {
                    fs.Write(fc, 0, fc.Length);
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }

        private static void DelFolder(string fileName)
        {

            if (Directory.Exists(Path.GetDirectoryName(fileName)))
                Directory.Delete(fileName, true);
        }

    }
}
View Code

 

=======================================================================================

python爬蟲---破解m3u8 加密

 

本文用到的核心技術:AES CBC方式解密

	基於Crypto的python3 AES CBC pcks7padding 中文、英文、中英文混合加密

具體加密解密方式請自行百度或者谷歌,不做詳細說明(因為實在是很麻煩~!)

准備工作

安裝方式 pip install pyCrypto

m3u8文件詳解

這個m3u8文件並不是一個視頻,而是一個記錄了視頻流下載地址的文件,所以我們需要下載並打開這個文件,用文本的方式打開之后是這個樣子的
在這里插入圖片描述
這里涉及到了一些m3u8的基礎,簡單來說就是這個ts文件是經過加的,加密方式是method后面的值,偏移量是iv后面的值,這里加密方式比較奇葩,是第三方網站自寫的加密方式,也就是文章開頭提到的AES CBC方式加密的

破解ts加密

然后我們通過瀏覽器斷點,發現實例的代碼在一個JS文件里,這個文件包含了該網站絕大多數的JS代碼
在這里插入圖片描述
通過斷點會發現一個很有用的參數 DRMKey,然后我們會發現DRMKey這個參數很奇怪,它並不是常見的一種密鑰,並且通過斷點得知它是這個樣子的

稍微有點基礎的同學可能知道,在源代碼里面的是16進制的,而這里面的是十進制的,所以我們需要用到進制轉換,包括后面我們還要再次轉成ascii碼,代碼我就直接貼在這里了

def get_asc_key(key):
	''' 獲取密鑰,把16進制字節碼轉換成ascii碼 :param key:從網頁源代碼中獲取的16進制字節碼 :return: ascii碼格式的key '''
	# 最簡潔的寫法
	# asc_key = [chr(int(i,16)) for i in key.split(',')]
	# 通俗易懂的寫法
	key = key.split(',')
	asc_key = ''
	for i in key:
	    i = int(i, 16)  # 16進制轉換成10進制
	    i = chr(i)  # 10進制轉換成ascii碼
	    asc_key += i
	return asc_key

此時我們已經找到很關鍵的值了 asc_key 密鑰
那么現在我們所需要的2個值就已經全部找到了,asc_key和iv
不過這個iv有點特殊,是32位的,所以我們需要進行切片取前16位,16位是固定位數,必須這么取

最后

說一下我是怎么知道它是aes cbc加密的吧
逆向JS需要比較強的推測能力,既然ts文件中含有加密方式和偏移量,那么JS代碼中肯定有加密的方法,因此我全局搜索的關鍵詞就是aes decrypt ,然后發現pkcs7這種加密方式
然后查了一下pkcs7這個東西,發現它其實就是aes的一種加密方式,在已知加密方式,密鑰和iv的情況下,就很好破解了,一下是完整的代碼,比較簡潔,爬取思路的話有一些變化,因為我發現隨便打開一個視頻都能獲取到其他視頻的標題和m3u8鏈接,所以我隨機打開了一個免費視頻的頁面,並通過這個頁面獲取這個免費課程下所有的視頻

完整代碼

# coding:utf-8

import os
import re

import requests
from Crypto.Cipher import AES
from lxml import etree

class Spider():
    def __init__(self):
        self.asc_key = ''

    def down_video(self, title, m3u8):
        ''' 通過m3u8文件獲取ts文件 :param title:視頻名稱 :param m3u8: m3u8文件 :return: None '''      
        ts_files = re.findall(re.compile("\n(.*?.ts)"), m3u8)  # ts文件下載路徑
        ivs = re.findall(re.compile("IV=(.*?)\n"), m3u8)  # 偏移量
        for index, (ts_file, iv) in enumerate(zip(ts_files, ivs)):
            ts_file = 'xxxx' + ts_file
            content = requests.get(ts_file, headers=headers).content
            iv = iv.replace('0x', '')[:16].encode()  # 去掉前面的標志符,並切片取前16位
            content = self.decrypt(content, self.asc_key, iv)  # 解密視頻流
            open('video/%s/%s.ts' % (title, index), 'wb').write(content)  # 保存視頻
            print('下載進度:%s/%s' % (index, len(ts_files)))
        print(title, '下載成功')

    def get_asc_key(self, key):
        ''' 獲取密鑰,把16進制字節碼轉換成ascii碼 :param key:從網頁源代碼中獲取的16進制字節碼 :return: ascii碼格式的key '''
        # 最簡潔的寫法
        # asc_key = [chr(int(i,16)) for i in key.split(',')]
        # 通俗易懂的寫法
        key = key.split(',')
        asc_key = ''
        for i in key:
            i = int(i, 16)  # 16進制轉換成10進制
            i = chr(i)  # 10進制轉換成ascii碼
            asc_key += i
        return asc_key

    def makedirs(self, path):
        if not os.path.exists(path):
            os.makedirs(path)

    def decrypt(self, content, key, iv):
        cipher = AES.new(key, AES.MODE_CBC, iv)
        msg = cipher.decrypt(content)
        paddingLen = msg[len(msg) - 1]
        return msg[0:-paddingLen]


if __name__ == '__main__':
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3676.400 QQBrowser/10.5.3738.400"
    }
    spider = Spider()
    spider.run()

 

 

出處:https://blog.csdn.net/weixin_40346015/article/details/102595690


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM