针对m3u8视频的ts文件解密

针对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;

}

///

/// 十六进制字符串转换为ASCII

///

/// 一条十六进制字符串

/// 返回一条ASCII码

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 error_arr = new List();

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 tsList = Regex.Matches(m3u8Content, @"\n(.*?.ts)").Select(m => m.Value).ToList();

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

}

///

/// 十六进制字符串转换为ASCII

///

/// 一条十六进制字符串

/// 返回一条ASCII码

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;

}

///

/// 16进制字符串转换为二进制数组

///

/// 用空格切割字符串

/// 返回一个二进制字符串

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;

}

///

/// AES解密

///

///

///

///

///

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 errList = new List();

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

}

///

/// AES解密

///

///

///

///

///

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 加密

破解m3u8 加密

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

准备工作

m3u8文件详解

破解ts加密

最后

完整代码

本文用到的核心技术: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

相关推荐

日语中姐姐怎么说?
365bet体育投注网站

日语中姐姐怎么说?

猫的爪子为什么能伸缩?
365速发国际平台登陆

猫的爪子为什么能伸缩?

股票里的恶意做空是啥! 恶意做空有两种模式:第一个,恶意做空某个具体标的,这样做的目的可能有很多,第二个,而已做空某个市场。恶意做空的一个表面现...
365速发国际平台登陆

股票里的恶意做空是啥! 恶意做空有两种模式:第一个,恶意做空某个具体标的,这样做的目的可能有很多,第二个,而已做空某个市场。恶意做空的一个表面现...

模拟人生4买房子哪个好
365dni讲解

模拟人生4买房子哪个好

光辉皮肤大全:获取所有光辉皮肤的攻略与指南(更新至2025版)
365速发国际平台登陆

光辉皮肤大全:获取所有光辉皮肤的攻略与指南(更新至2025版)

BOIS相关频道
365速发国际平台登陆

BOIS相关频道