前些天一位高人发的贴子被删了,好不容易找回来,放在这里慢慢学习
以下为转载内容:
优酷视频的算法在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
上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
/// <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对应的字符;最后组合成新的位数相等的字符串,这就是秘钥。上代码:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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 } |
两个秘钥的原始字符串分别为b4eto0b4和boa4poz1
目标数组:[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)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
自由转载,转载请注明: 转载自WEB开发笔记 www.chhua.com
本文链接地址: 优酷真实视频地址解析破解思路,含破解方法(更新至2016-2-28) http://www.chhua.com/web-note5339
评论