優酷真實視頻地址解析破解思路,含破解方法(更新至2016-2-28)


前些天一位高人發的貼子被刪了,好不容易找回來,放在這里慢慢學習

以下為轉載內容:

優酷視頻的算法在2015年11月24日起至今連續更改了好幾個版本,之前發的這篇臨時解決方案得到很多響應,非常感謝!現在對這篇文章重新修改,全面規整完整的破解思路(含破解方法)!

對了,這篇文章只是針對m3u8格式的視頻。

.准備工作

所謂工欲善其事必先利其器,做好破解的准備工作會令你事半功倍。

1.首先准備一個Http抓包工具,PC上推薦Fiddler或者Postman,iOS上推薦Surge

2.手備一台iOS測試設備(因為在Safari里優酷視頻是確定使用m3u8進行播放的)

.抓包過程

Fiddler為例子,先配置Fiddler的設置和iOS設備,讓Fiddler可以抓取iOS設備的請求數據,這一步如果不會請先自行摸索。

打開Safari,輸入優酷網址,進入一個視頻查看Fiddler窗口,需要關注的一些重要信息:

  • 請求v.youku.com/v_show/id_{vid}.html時,返回的Cookie信息ykss
  • 請求play.youku.com/play/get.json?vid={vid}&ct=12時,請求頭部的Cookie信息ykss和__ysuid,以及返回的security節點
  • 最終拼出m3u8地址pl.youku.com/playlist/m3u8?vid=....,請求后得到m3u8的具體視頻內容

前兩步都好說,可第三部的m3u8這么一個復雜的串,格式如http://pl.youku.com/playlist/m3u8?vid=XMTQzNzIwODQ3Ng==&type=flv&ts=1452839810&keyframe=0&ep=ciaRGEGOX8YB5SPYjD8bNC6xJnIGXJZ3kn7P%2F5gbR8RQKevBzjPcqJ21TPs%3D&sid=045283981007412c2cb59&token=0524&ctype=12&ev=1&oip=2093868719,是怎么得到的?回頭看一下抓到的一堆js代碼,你會發現精髓就在這里——http://player.youku.com/embed/unifull/unifull_.js,這里面就是全部優酷真實地址的解析算法,源碼非常長,如果你有興趣,可以對文件格式化一下后慢慢閱讀。下一節就具體來講講這里邊的算法是怎樣的。

.真實地址算法解析

在上一節各個步驟標注的重要信息中,get.json這個接口返回的securiy節點就對算法起着很重要的作用(其實從名字就能看出它的特別意義嘛~)

1. 獲取sid, token和ep

security節點有兩個值,一個是encryp_string,一個是ip。

http://player.youku.com/embed/unifull/unifull_.js里,隨處可以找到YK.m3u8src這個function,其中傳入兩個參數,一個是視頻的vid,另一個是視頻片段的格式(如"mp4")。

總結一下這個方法做的事情(這個算法需要計算3個值:sid, token和ep):

1). 對encrypt_string進行Decode64,即對其進行base64解碼,得到一個byte數組decoded_ep

2).生成一個秘鑰key_a(其值其實是固定的),利用這個秘鑰與decoded_ep作Rc4算法加密,得到一個字符串temp

3).temp的結果由“xxxx_xxxx”組成,切割‘_’,前者為sid,后者為token

4).生成一個秘鑰key_b(其值也是固定的),利用這個秘鑰與字符串{sid}_{vid}_{token}的ASCII碼字節數組whole記性Rc4算法加密,得到一個字符串temp2

5).對temp2進行base64轉換,然后再做url encode,得到ep

至此sid, token和ep就得到了!

關於Rc4加解密,可以參考https://zh.wikipedia.org/wiki/RC4

上代碼:

 /// <Summary>
  2 /// 計算sid, token和ep
  3 /// </Summary>
  4 public static void GetParameters(string vid, string encryptString, ref string sid, ref string token, ref string ep)
  5 {
  6             string keyA= "becaf9be";
  7             byte[] decodeEp= Decode64(encryptString); // 對encryptString作base64解碼
  8             string temp = Rc4(keyA, decodeEp, false); // 用秘鑰keyA之作Rc4加密,不做base64編碼
  9             string[] part = temp.Split('_');
 10             sid = part[0];
 11             token = part[1];
 12 
 13             string keyB= "bf7e5f01";
 14             byte[] whole= Encoding.ASCII.GetBytes(string.Format("{0}_{1}_{2}", sid, vid, token)); // 組合字符串的ASCII字節數組
 15             ep= WebUtility.UrlEncode(Rc4(keyB, whole, true)); // 用秘鑰keyB與之作Rc4加密,且結果進行base64編碼,之后再做url encode
 16 }
 17 
 18 private static byte[] Decode64(string a)
 19 {
 20             if (string.IsNullOrEmpty(a))
 21             {
 22                 return null;
 23             }
 24             int f;
 25             int g;
 26             string h;
 27             List<byte> l = new List<byte>();
 28             int[] i =
 29             {
 30                 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
 31                 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
 32                 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
 33                 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29,
 34                 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1,
 35                 -1, -1
 36             };
 37             for (g = a.Length, f = 0, h = ""; g > f;)
 38             {
 39                 int b;
 40                 do
 41                 {
 42                     b = i[255 & a[f++]];
 43                 } while (g > f && -1 == b);
 44 
 45                 if (-1 == b) break;
 46 
 47                 int c;
 48                 do
 49                 {
 50                     c = i[255 & a[f++]];
 51                 } while (g > f && -1 == c);
 52 
 53                 if (-1 == c) break;
 54 
 55                 byte[] bytes0 = { (byte)(b << 2 | (48 & c) >> 4) };
 56                 h += Encoding.ASCII.GetString(bytes0);
 57                 l.Add(bytes0[0]);
 58                 int d;
 59                 do
 60                 {
 61                     d = 255 & a[f++];
 62                     if (61 == d) return l.ToArray();
 63                     d = i[d];
 64                 } while (g > f && -1 == d);
 65 
 66                 if (-1 == d) break;
 67                 byte[] bytes1 = { (byte)((15 & c) << 4 | (60 & d) >> 2) };
 68                 h += Encoding.ASCII.GetString(bytes1);
 69                 l.Add(bytes1[0]);
 70                 int e;
 71                 do
 72                 {
 73                     e = 255 & a[f++];
 74                     if (61 == e) return l.ToArray();
 75                     e = i[e];
 76                 } while (g > f && -1 == e);
 77 
 78                 if (-1 == e) break;
 79                 byte[] bytes2 = { (byte)((3 & d) << 6 | e) };
 80                 h += Encoding.ASCII.GetString(bytes2);
 81                 l.Add(bytes2[0]);
 82             }
 83             return l.ToArray();
 84 }
 85 
 86 private static string Rc4(string a, byte[] c, bool isToBase64)
 87 {
 88             // rc4加密算法
 89             int f = 0, h = 0, q;
 90             int[] b = new int[256];
 91             for (int i = 0; i < 256; i++)
 92             {
 93                 b[i] = i;
 94             }
 95             while (h < 256)
 96             {
 97                 f = (f + b[h] + a[h % a.Length]) % 256;
 98                 int temp = b[h];
 99                 b[h] = b[f];
100                 b[f] = temp;
101                 h++;
102             }
103 
104             f = 0; h = 0; q = 0;
105             string result = "";
106             List<byte> bytesR = new List<byte>();
107             while (q < c.Length)
108             {
109                 h = (h + 1) % 256;
110                 f = (f + b[h]) % 256;
111                 int temp = b[h];
112                 b[h] = b[f];
113                 b[f] = temp;
114                 byte[] bytes = { (byte)(c[q] ^ b[(b[h] + b[f]) % 256]) };
115                 bytesR.Add(bytes[0]);
116                 result += Encoding.ASCII.GetString(bytes);
117                 q++;
118             }
119 
120             if (isToBase64)
121             {
122                 var byteR = bytesR.ToArray();
123                 result = Convert.ToBase64String(byteR);
124                 //result = Encode64(result);
125             }
126 
127             return result;
128 }

 

2. 固定秘鑰key_a和key_b的生成方法

如果對秘鑰key_a和秘鑰key_b怎么得到有興趣,請繼續細度下面的內容,不感興趣可以跳過這一段。

http://player.youku.com/embed/unifull/unifull_.js里,秘鑰由方法translate(a, b)生成,其中a為一個組合字符串,b是一個固定的目標數組。

translate方法的作用是利用字符的ASCII碼進行變形,對於組合字符串中的每個字符,如果是小寫字母a-z,則取其ASCII碼,否則將其ASCII碼+26;接下來如果能在目標數組里找到這個數碼,如果能找到,且碼的值 > 25,則取碼-26,否則取碼+97對應的字符;最后組合成新的位數相等的字符串,這就是秘鑰。上代碼:

 

private static string Translate(string a, int[] b)
 2 {
 3             List<string> c = new List<string>();
 4             foreach (char t in a)
 5             {
 6                 int e;
 7                 e = t >= 'a' && t <= 'z' ? t - 'a' : t - '0' + 26;
 8                 for (int j = 0; j < 36; j++)
 9                 {
10                     if (b[j] == e)
11                     {
12                         e = j;
13                         break;
14                     }
15                 }
16                 if (e > 25)
17                 {
18                     c.Add((e - 26).ToString());
19                 }
20                 else
21                 {
22                     var bytes = new[] { (byte)(e + 97) };
23                     c.Add(Encoding.ASCII.GetString(bytes));
24                 }
25             }
26             string result = c.Aggregate(string.Empty, (current, cc) => current + cc.ToString());
27             return result;
28 }

 

兩個秘鑰的原始字符串分別為b4eto0b4boa4poz1

目標數組:[19, 1, 4, 7, 30, 14, 28, 8, 24, 17, 6, 35, 34, 16, 9, 10, 13, 22, 32, 29, 31, 21, 18, 3, 2, 23, 25, 27, 11, 20, 5, 15, 12, 0, 33, 26]

(不要問我怎么知道,看js源碼自己組一下,都是固定的)

3.拼接m3u8地址

接下來就是按照m3u8指定的格式將數值填入即可,格式如下:

http://pl.youku.com/playlist/m3u8?vid={vid}&type={type}&ts={ts}&keyframe=1&ep={ep}&sid={sid}&token={token}&ctype=12&ev=1&oip={oip}

在這個串中還有其他一些變量需要填寫,其中

type指m3u8的內容中視頻片段的格式,可取值標清(flv, 3pgphd)、高清(mp4, flvhd)、超清(hd2)、1080P(hd3)

ts指當前時間的Unix時間戳(精確到秒)

oip指security節點中的ip

組合完成后就去請求m3u8地址吧,會得到一系列的視頻片段。

 

4.注意事項

算法邏輯清楚了,還需要有寫注意點,一旦忽略就會導致最終即使拼出m3u8地址,也請求不到視頻內容。

回顧一下第二節的3個步驟,每個步驟都有注意點:

第一個步驟,請求v.youku.com/v_show/id_{vid}.html ,返回值會帶有名字為ykss的Cookie,這個很重要,不能丟

第二個步驟,請求get.json時,第一步的Cookie:ykss也要發送,並且還要增加一個名為__ysuid的Cookie,__ysuid=getPvid(6),算法請看以往的更新記錄;還有一點,Referer要填視頻的url地址,絕對不能丟,否則即使get.json能正常返回數據,也能拿到security節點,但是拼出來的m3u8無論你怎么請求都無法得到視頻內容,切記Referer不要丟!

第三個步驟,請求m3u8地址時,就目前看已經不需要再傳名為r的Cookie,但是以優酷的尿性,保不准以后還會再加上。

 

如果有什么疑問,歡迎留言。(以下內容是以往的一些變更記錄,已作廢)

————————————————————————————————————————————————————————————————

11-30更新:

今日又發現了一個問題,這里補充一下。get.json這個接口返回的時候,Response帶有Cookie,如果你是自己提供web api,這個Cookie記得也要返回去給應用,否則應用即使拼出m3u8的地址,也無法獲取到完整的視頻內容。

 

————————————————————————————————————————————————————————————————

12-8更新:

感謝@kkia 發現,

請求http://play.youku.com/play/get.json?vid={vid}&ct=12這個api的時候,http請求需要帶上Referer:{url},url為需要獲取的視頻的頁面鏈接,不然會出現非主站請求的錯誤。

另外,cookie校驗機制又啟動了。

所以,如果自己封裝web api,建議每次請求,都把m3u8文件的內容下載下來,自己生成並存儲臨時的m3u8文件,再把這個文件的鏈接返回給客戶端,再定時去清理這些臨時文件

 

————————————————————————————————————————————————————————————————

12-29更新:

感謝@kkia 發現,name=r的Cookie獲取途徑再次發生了變化,步驟更新如下:

1.通過完整的視頻地址url(http://v.youku.com/v_show/id_{vid}.html)中拿到一堆Cookie,只提取其中的ykss=132b825604a2f7516fd91fc0; path=/; domain=.youku.com; 這條Cookie

2.請求get.json時帶上以上名為ykss的Cookie,且加上Referer=視頻url,請求結果中就會出現名為r的Cookie了

3.最后請求m3u8地址的時候帶上名為r的Cookie,Referer=視頻url,就能得到結果了。

 

————————————————————————————————————————————————————————————————

12-30更新:

優酷日常作死,又更新了Cookie算法,看來這是持久戰,大家做好心理准備,相信這么折騰大家都明白名為r的Cookie的重要性了吧。

目前可以這樣獲取名為r的Cookie:

直接請求get.json這個API,頭部需要有如下格式的信息:

Cookie: __ysuid={16-length-string};

Referer: http://v.youku.com/v_show/id_{vid}.html

其中__ysuid的算法如下(感謝@kkia)

 

 

public string getPvid(int len)
 2 {
 3     string[] randchar = new string[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
 4 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
 5 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
 6     };
 7     var i = 0;
 8     var r = "";
 9     TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
10     Int64 seconds = Convert.ToInt64(ts.TotalMilliseconds);
11  
12     for (i = 0; i < len; i++)
13     {
14         var index = System.Convert.ToInt32(new Random().Next() * Math.Pow(10, 6) % randchar.Length);
15         r += randchar[index];
16     }
17     return seconds + r;
18 }

 

由13位長的UNIX時間戳拼接3位長的隨機字符串組成__ysuid。

事實上,只要在請求m3u8鏈接的時候Cookie里含有r(不用管value是啥,至少現階段一直是這樣的),就能請求成功。

 

附上WPF的demo

 

本文僅供學習參考

 附上:http://www.520hd.cc


免責聲明!

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



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