大神论坛

找回密码
快速注册
查看: 223 | 回复: 0

[原创] 对爱奇艺视频网址加密接口逆向分析 附源码

主题

帖子

0

积分

初入江湖

UID
565
积分
0
精华
威望
0 点
违规
大神币
68 枚
注册时间
2023-09-16 14:58
发表于 2024-01-29 22:08
本帖最后由 wangjunbin345 于 2024-01-29 22:08 编辑

前分享过一次帖子,是扣JS的方法来请求接口
希望各位食客给个关注,加个评论来点更新的动力

这次直接使用C#实现了他的算法,然后在分享一下他的接口时怎么请求以及解密的
用到的工具就是Fiddler和浏览器控制台,然后写了一个C#版本的软件(随手写的有BUG哈哈)
地址是:aHR0cHM6Ly9qeC54bWZsdi5jb20v

首先打开FD,然后打开控制台抓一下包,挨个分析

这里有这样一个请求,他的请求参数有我们想看的视频并且响应是加密的

我们先分析他的请求,可以看到有个key是加密的,去控制台打一个XHR断点

可以看到成功断住,我们根据这个断点往上找加密的地方

往上找到这里的时候可以看到key=lllIIlIl,lllIIlIl=sign
先分析sign中的几个参数与结果
I111ill=时间戳,url=编码后的想看的视频的地址

可以看到时间戳拼接视频地址,然后取MD5
这个时候我们去sign中看他的逻辑即可

鼠标悬停进VM虚拟机里看sign代码

是一个AES加密的,然后传过来MD5的结果
就是KEY参数的值
这个时候可以模拟请求拿到对应的响应
结果中有两个参数是加密的,分别是url和html
我们看到同时他的响应中有一个AES_KEY和AES_IV
大胆设想他是AES加密并且同时返回key和iv
一个是url,一个是html,
这个url会返回真实视频地址,两种情况一个是301,一个是直接的视频地址
html是全集和剧的详细信息
C#的代码贴在下方,diamagnetic比较烂只是能用,没有具体优化,各位佬可以根据自己需求在优化
现在他有时候接口返回的响应有一些剧的他自己网站也播放不出来,一直卡着转圈,所以软件也没办法下载,具体哪些能下哪些不能下需要自己测试了

using Newtonsoft.Json;
using System;
using System.IO;
using System.IO.Pipes;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using static System.Net.Mime.MediaTypeNames;
using static System.Runtime.InteropServices.JavaScript.JSType;

namespace xmflv
{
internal class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Hello, World!");


Console.Write("请输入视频URL(支持腾讯视频,爱奇艺):");
string fromUrl = Console.ReadLine();
if (string.IsNullOrEmpty(fromUrl))
{
Console.WriteLine("输入url为空!");
Console.ReadLine();
}

fromUrl = EncodeUrl(fromUrl);
//Console.WriteLine(fromUrl);
//时间戳
long timeStamp = GenerateTimestamp();
//Console.WriteLine(timeStamp);
//时间戳+URL的MD5
string plaintext = CalculateMD5Hash($"{timeStamp}{UrlDecode(fromUrl)}");
//Console.WriteLine(plaintext);
//请求中的key参数
string ciphertext = Sign(plaintext);
//Console.WriteLine(ciphertext);
HttpHelper http = new HttpHelper();
HttpItem item = new HttpItem()
{
URL = "接口地址的url",
Method = "POST",
Accept = "application/json, text/javascript, */*; q=0.01",
ContentType = "application/x-www-form-urlencoded; charset=UTF-8",
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
Postdata = $"wap=1&url={fromUrl}&time={timeStamp}&key={EncodeKey(ciphertext)}",
//ProxyIp = "127.0.0.1:8888"
};
item.Header.Add("Origin", "地址自己抓一下");
HttpResult result = http.GetHtml(item);
if (result.StatusCode != System.Net.HttpStatusCode.OK)
{
Console.Write("请求失败!");
}
else
{
string html = result.Html;
//Console.WriteLine(html);
var resultDynamic = JsonConvert.DeserializeObject<dynamic>(html);
if (!string.IsNullOrEmpty(html) && (int)resultDynamic.code == 200)
{
//解密后的视频地址集合
var extm3u = DecryptAES((string)resultDynamic.url, (string)resultDynamic.aes_key, (string)resultDynamic.aes_iv);
//Console.WriteLine(extm3u);

//取得全集链接
var AllUrl = ExtractURLs(DecryptAES((string)resultDynamic.html, (string)resultDynamic.aes_key, (string)resultDynamic.aes_iv));

item = new HttpItem()
{
URL = RemoveSpecialCharacters(extm3u),
Method = "GET",
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
Accept = "*/*",
//ProxyIp = "127.0.0.1:8888"
};
item.Header.Add("Origin", "地址自己抓一下");
result = http.GetHtml(item);
if (result.StatusCode == System.Net.HttpStatusCode.OK && !string.IsNullOrEmpty(result.Html))
{
html = result.Html;
//Console.WriteLine(html);
string[] lines = html.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
string folderPath = AppDomain.CurrentDomain.BaseDirectory + $"{resultDynamic.name}";
int i = 0;
foreach (string line in lines)
{
if (!line.StartsWith("#"))
{

//文件夹不存在则创建文件夹
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}

string fileName = $"{i}.ts";
string filePath = Path.Combine(folderPath, fileName);

if (!File.Exists(filePath))
{
bool bl = true;
int j = 0;
while (bl)
{
try
{
//WebProxy webProxy = new WebProxy("127.0.0.1", 8888);
using (HttpClient client = new HttpClient())
{
if (extm3u.Contains("IP地址包含就用这个"))
{
//先获取重定向的url
item = new HttpItem()
{
URL = "https://服务器地址自己抓一下/" + line,
Method = "GET",
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
Accept = "*/*",
//ProxyIp = "127.0.0.1:8888"
};
item.Header.Add("Origin", "地址自己抓一下");
result = http.GetHtml(item);
var location = result.RedirectUrl;

using (HttpResponseMessage response = await client.GetAsync(location))
{
response.EnsureSuccessStatusCode();

using (Stream stream = await response.Content.ReadAsStreamAsync())
{
using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await stream.CopyToAsync(fileStream); // 将响应流保存到本地文件
bl = false;
break;
}
}
}
}
else
{
//client.Timeout = TimeSpan.FromSeconds(120); // 设置超时时间为 120 秒
//HttpClient.DefaultProxy = webProxy;
//client.DefaultRequestHeaders.Add("origin", "地址自己抓一下");
string pattern = @"/([^/]+)$";
string match = Regex.Replace(extm3u, pattern, "");
string mp4Url = match + "/" + line;

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(mp4Url);
request.Method = "GET";
request.Headers.Add("sec-ch-ua", "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Microsoft Edge\";v=\"120\"");
request.Headers.Add("sec-ch-ua-mobile", "?0");
request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0";
request.Headers.Add("sec-ch-ua-platform", "\"Windows\"");
request.Accept = "/";
request.Headers.Add("Origin", "地址自己抓一下");
request.Headers.Add("Sec-Fetch-Site", "cross-site");
request.Headers.Add("Sec-Fetch-Mode", "cors");
request.Headers.Add("Sec-Fetch-Dest", "empty");
request.Headers.Add("Accept-Encoding", "gzip, deflate, br");
request.Headers.Add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
try
{
// Send the request and get the response
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

// Read the response content
using (var stream = response.GetResponseStream())
{
using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await stream.CopyToAsync(fileStream); // 将响应流保存到本地文件
bl = false;
}

response.Close();
break;
}
}
catch (WebException ex)
{
Console.WriteLine("An error occurred: " + ex.Message);
}

//using (HttpResponseMessage response = await client.GetAsync(mp4Url))
//{
// if (response.StatusCode == HttpStatusCode.OK)
// {

// using (Stream stream = await response.Content.ReadAsStreamAsync())
// {
// using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
// {
// await stream.CopyToAsync(fileStream); // 将响应流保存到本地文件
// bl = false;
// break;
// }
// }
// }
//}


}
}
}
catch (Exception ex)
{
j++;
if (j > 3)
{
bl = false;
Console.WriteLine($"3次请求地址自己抓一下{line}失败已经跳过!");
}
}
}


}
i++;
}
}

CombineMP3(folderPath);
}
else
{
Console.WriteLine("请求加密接口失败!");
}
}
else
{
Console.WriteLine("请求失败!" + html);
}

}

Console.ReadLine();
}

static void CombineMP3(string folder)
{
string outputFilePath = $"{folder}.mp4";
string[] mp3Files = Directory.GetFiles(folder, "*.ts"); // 获取文件夹中的所有MP3文件路径

List<byte[]> mp3DataList = new List<byte[]>();

List<string> sortedFiles = mp3Files.OrderBy(f => Path.GetFileNameWithoutExtension(f), new NumericFileNameComparer()).ToList();
foreach (string filePath in sortedFiles)
{
byte[] mp3Data = File.ReadAllBytes(filePath); // 读取MP3文件的字节数据
mp3DataList.Add(mp3Data);
}

byte[] outputData = mp3DataList.SelectMany(data => data).ToArray(); // 将所有MP3文件的字节数据合并为一个字节数组

File.WriteAllBytes(outputFilePath, outputData); // 将字节数组写入输出文件
Directory.Delete(folder, true);
Console.WriteLine("合并完成,输出文件路径:" + outputFilePath);
}


public static string RemoveSpecialCharacters(string url)
{
string pattern = @"[^a-zA-Z0-9.:/?=_-]"; // 匹配非法字符的正则表达式

return Regex.Replace(url, pattern, ""); // 去掉非法字符
}
public class NumericFileNameComparer : IComparer<string>
{
public int Compare(string x, string y)
{
string fileNameX = Path.GetFileNameWithoutExtension(x);
string fileNameY = Path.GetFileNameWithoutExtension(y);

if (fileNameX == null || fileNameY == null)
{
return 0; // 如果文件名为空,则认为两个文件名相等
}

int numericValueX, numericValueY;
bool successX = int.TryParse(fileNameX, out numericValueX);
bool successY = int.TryParse(fileNameY, out numericValueY);

if (successX && successY)
{
return numericValueX.CompareTo(numericValueY);
}
else if (successX)
{
return -1; // 如果只有 x 是数字,那么 x 小于 y
}
else if (successY)
{
return 1; // 如果只有 y 是数字,那么 x 大于 y
}
else
{
return string.Compare(fileNameX, fileNameY, StringComparison.Ordinal); // 如果都不是数字,则按照字符串顺序排序
}
}
}


/// <summary>
/// 获取全集链接
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static List<string> ExtractURLs(string text)
{
List<string> urls = new List<string>();
Regex regex = new Regex(@"./\?url=(.*?)'");

MatchCollection matches = regex.Matches(text);
foreach (Match match in matches)
{
string url = match.Groups[1].Value;
urls.Add(url);
}

return urls;
}
/// <summary>
/// 获取请求中的key
/// </summary>
/// <param name="a"></param>
/// <returns></returns>
public static string Sign(string a)
{
// 计算MD5
byte[] md5Bytes;
using (var md5 = MD5.Create())
{
byte[] inputBytes = Encoding.UTF8.GetBytes(a);
md5Bytes = md5.ComputeHash(inputBytes);
}
string b = BitConverter.ToString(md5Bytes).Replace("-", "").ToLower();

// 使用MD5作为密钥
byte[] key = Encoding.UTF8.GetBytes(b);

// 设置IV和Padding
byte[] iv = Encoding.UTF8.GetBytes("3cccf88181408f19");
PaddingMode padding = PaddingMode.Zeros;

// 加密
byte[] encryptedBytes;
using (var aes = new AesCryptoServiceProvider())
{
aes.Key = key;
aes.IV = iv;
aes.Padding = padding;
aes.Mode = CipherMode.CBC;

byte[] inputBytes = Encoding.UTF8.GetBytes(a);
using (var encryptor = aes.CreateEncryptor())
{
encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
}
}

string e = Convert.ToBase64String(encryptedBytes);
return e;
}
/// <summary>
/// 时间戳+url
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
static string CalculateMD5Hash(string input)
{
using (MD5 md5 = MD5.Create())
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);

StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < hashBytes.Length; i++)
{
stringBuilder.Append(hashBytes[i].ToString("x2"));
}

return stringBuilder.ToString();
}
}
/// <summary>
/// 获取时间戳
/// </summary>
/// <returns></returns>
static long GenerateTimestamp()
{
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
TimeSpan timeSpan = DateTime.UtcNow - epoch;
long timestamp = (long)timeSpan.TotalMilliseconds;
return timestamp;
}
/// <summary>
/// 对网址进行url编码
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
static string EncodeUrl(string str)
{
string encodedUrl = Uri.EscapeDataString(str);
encodedUrl = encodedUrl.Replace("%3A", "%253A").Replace("%2F", "%252F");
return encodedUrl;
}
static string EncodeKey(string str)
{
string encodedUrl = Uri.EscapeDataString(str);
return encodedUrl;
}
public static string UrlDecode(string encodedUrl)
{
string decodedUrl = HttpUtility.UrlDecode(encodedUrl);
return decodedUrl;
}
/// <summary>
/// 解密
/// </summary>
/// <param name="encryptedText"></param>
/// <param name="key"></param>
/// <param name="iv"></param>
/// <returns></returns>
static string DecryptAES(string encryptedText, string key, string iv)
{
byte[] keyBytes = Encoding.UTF8.GetBytes(key);
byte[] ivBytes = Encoding.UTF8.GetBytes(iv);
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);

using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = keyBytes;
aesAlg.IV = ivBytes;
aesAlg.Padding = PaddingMode.Zeros;
aesAlg.Mode = CipherMode.CBC;

ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

using (MemoryStream msDecrypt = new MemoryStream(encryptedBytes))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
}
}
}



注:若转载请注明大神论坛来源(本贴地址)与作者信息。





返回顶部