摘要
本文内容转自网络,个人学习记录使用,请勿传播
需求
- 1.央视频中的相关视频进行刷播放
- 2.下载视频
- 注意:央视频是没有web端的,可以在手机中下载一个央视频软件,让后将视频播放页链接发到电脑中即可。
前置知识点
jsonp
什么是json?
json是一种基于文本的数据交换方式/数据描述格式。
1
2
3
4{
'name':'bobo',
'age':20
}
json的优点:
- 基于文本,跨平台传递及其简单。
- 后台语言几乎都支持
- 容易编写和解析
什么是jsonp?
- 非正式的数据传输协议。主要作用在客户端和服务端的数据交互中。
jsonp协议的特性:
允许客户端传递一个callback作为请求参数(callback=jsonp1),然后服务端给客户端返回数据时会将这个callback参数值(jsonp1)作为函数名包裹住json格式的数据一起返回,这样客户端就可以随意指定自己的函数来处理这组返回的数据。
1
jsonp1({name:bobo,age=20}) //jsonp格式的数据
如何解析jsonp格式的数据?
使用python的内置函数eval来解析jsonp格式的数据
1
2
3#eval内置函数的作用
s = "print('hello world')"
eval(s) #eval可以将s这个字符串表示的代码在python环境里执行1
2
3
4
5def jsonp1(arg):
return arg
jsonp_str = 'jsonp1({name:bobo,age=20})'
ret = eval(jsonp_str)
print(ret,type(ret))
请求分析
捕获点击播放按钮后发起的请求
- 通过抓包工具一共捕获到了6个数据包。
- 其中有三个数据包的后缀是.mp4 .png .gif这些都是资源文件,和播放无关
- 还有三个kvcollect数据包,分析后,只有对第一个kvcollect数据包进行模拟就可以增加该视频的一次播放。重点分析第一个kvcollect数据包即可!
kvcollect分析
cookie分析
- 该数据包没有cookie,因此不需要处理cookie
请求参数分析
- 切记:只需要处理动态变化的请求参数
1 | ctime: 2022-04-16 20:25:42 #变化 |
- 动态变化的请求参数:
1 | ctime: 2022-04-16 20:25:42 #变化 |
ctime的处理
1 | def get_ctime(): |
vid的处理
- 在视频的播放页的url中就会存在vid的值,因此直接对url中的vid进行提取即可!
- 处理方式1:
1 | def get_vid(play_url): |
处理方式2:
基于抓包工具全局搜索vid:,在cctvh5-web.js中发现了vid的生成代码:
在js数据包中局部搜索vid:即可,然后在可疑位置打断点
vid = t.tid,因此,vid的值就是从中获取的,因此需要分析t是什么
1
2
3var t = jt()(window.location.href);
//jt()是函数名
//window.location.href是函数参数,表示当前播放页的url需要进入到函数的实现中进行js改写:
有些时候,不能一味的依赖改写工具(改写工具的内核有可能版本较低,新的js语法机制无法识别,也有可能改写的js程序需要依赖浏览器环境,而改写工具无法满足浏览器环境)
- 有些时候,js改写工具报错,并不一定是js程序的问题,也有可能是当前环境的问题。
在改写该函数jt()的实现时,出现一个问题,就是改写工具无法识别代码中的forEach,js中的forEach什么意思
- xxx.forEach(匿名函数):匿名函数是用来将xxx容器中的每一个元素进行处理!
jt()函数实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function get_vid(t) {
var n = {};
var e ;
var r = n[t] || {};
if (t) {
var i = t.indexOf("?");
if (i >= 0) {
var o = t.slice(i + 1, t.length); (o = o.replace(/#.*/, "")).split("&").forEach((function(t) {
var e = t.indexOf("=");
if (e > 0) {
var n = t.slice(0, e),
i = t.slice(e + 1, t.length);
r[n] = i
}
}))
}
n[t] = r
}
return e ? r[e] || "": r
}python代码
1
2
3
4
5
6
7def get_vid(play_url):
import execjs
node = execjs.get()
fp = open('./vid_js.js','r',encoding='utf-8')
ctx = node.compile(fp.read())
ret = ctx.eval("get_vid('%s')"%play_url)
return ret['vid']
val的处理
- 全局搜索val,定位到stepReport数据包:在可疑之处打断点,打完后,刷新页面,发现停留的断点位置的val的值不对,可以尝试点击播放按钮,发现停留在了另一个断点位置:
1 | val: e - t |
- 通过测试返现val就是刷新页面和点击播放按钮对应的时间间隔
- 因此val的值自己随机给一个随机数即可(2000-10000之间)
- val写成固定值3893也可以的,只要不是0即可,大于1000就行。
pid的处理
- 全局搜索pid,最终定位到stepReport数据包,分析发现:
1 | pid:e |
1 | //getPlayerId的实现: |
1 | def get_pid(): |
vurl的处理
1 | #下面是vurl参数的值 |
因此:想要处理vurl就需要先处理vkey,因此问题的矛头指向vkey
vkey的处理
vkey是一个请求参数,思考:动态变化的请求参数的来源有哪些渠道?
- 1.可以出现在其他请求的响应数据中
- 2.js算法生成
全局搜索vkey,定位到playvinfo这个数据包:发现了
1
"fvkey": "73CD259DE3D16E844E8F9F14959C40F306ED28D2DFE0CC35575A2ADD64A1A371E3B16DCA12F27C06468CAF1475038198F306F9C3DFDBC2B7F5F4200D6A80987734DBBEA7F34762633C7D7A4D426E4998399228922E2148A3EB01DC0096F8BA0AF13213F8FDDCE13FE1950B9A2B7F537607BE7EB4DD98C35C463D74D646E8EA04"
注意:如果我们获取了fvkey的值,则就处理好了vkey,如果vkey处理好了则就处理好了vurl。
分析处理playvinfo数据包
- 提取该数据包的url:
- 请求方式:
- get
- 请求参数:
1 | callback: jsonp1 |
- 动态变化的请求参数:
1 | vid: l000073qxd4 #已知的 |
ckey的处理(最难)
全局搜索ckey,定位到yspalyer数据包,分析后发现getRequestCkey函数返回的内容就是ckey的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16getRequestCkey(e) {
const {guid: t=e.guid, platform: r=e.platform} = this.context;
return _(e.vid, e.svrtick || C.a.getTimeStampStr(), "0.2.0", t, r)
}
//重点分析返回值(函数调用):
//函数名:_
//函数参数1:e.vid (之前的vid,已知)
//函数参数2:e.svrtick || C.a.getTimeStampStr()
//e.svrtick == undefine
//C.a.getTimeStampStr()返回python10位的时间戳
//e.svrtick || C.a.getTimeStampStr() 返回的就是时间戳
//函数参数3:"0.2.0" 【固定值】
//函数参数4:t 【固定值:"l1mzbpxy_s7bp4q6ggze"】
//函数参数5:r 【固定值:4330701(整形数据)】
//只需要获取_函数调用的返回值即可获取ckey的值1
2
3
4
5
6
7
8
9
10
11
12
13
14function getTimeStampStr(e, t) {
void 0 === e && (e = +new Date),
void 0 === t && (t = 10);
let r = String(e);
if (r.length === t)
return r;
if (r.length > t)
return r.substring(0, t);
let n = t - r.length;
for (; n > 0; )
r = `0${r}`,
n -= 1;
return r
}1
2
3
4
5
6import execjs
node = execjs.get()
fp = open('./test.js','r',encoding='utf-8')
ctx = node.compile(fp.read())
ret = ctx.eval("getTimeStampStr()")
print(ret)
查看分析_这个函数的实现:惊讶发现该函数的实现有400行代码,几乎很难改写成功(函数调用关系巨复杂)!怎么办?
思考:_函数的返回值就是想要的cKey的值,那么我们为何不单独分析该函数的返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23return Yn = jt + qn + Fn,
Kn = Tt,
Wn = At,
Kn = r[_t][xt][bt](Kn),
Wn = r[gt][ft][ut](Wn),
(Qn = {})[at] = Wn,
Qn[it] = r[tt][Xe],
Qn[Ke] = r[qe][Ge], //再次之前的代码都是在给变量赋值
//如下代码才是真正返回的结果内容(cKey的值)
Ee + Ce + we + me + r[He][Ne](Yn, Kn, Qn)[Ie][Re]()[$e]()
//Ee:_
//Ce:_
//we:0
//me:0
//r[He][Ne](Yn, Kn, Qn):函数调用。r[He][Ne]表示函数名(encrypt),(Yn, Kn, Qn)函数参数。
//函数名叫做encrypt,说明此处的返回这涉及到加密运算,还看到了iv,mod,padding和CBC这些关键字,因此推断采用的是AES加密算法。加密函数encrypt携带的三个参数依次为:明文数据,秘钥,iv
//Ie = ciphertext,密文数据
//Re = toString函数
//$e = toUpperCase函数
//因此encrpt(Yn, Kn, Qn)[Ie][Re]()[$e]()
//encrpt(Yn, Kn, Qn)[Ie]将加密后的密文数据取出
//encrpt(Yn, Kn, Qn)[Ie].String().toUpperCase()单独分析函数的三个参数:Yn,Kn,Qn
Yn(明文数据):Yn = jt + qn + Fn
jt:|
Fn:是有vid,当前的时间戳(10位)和一些固定的字符串组成
1
"|l000073qxd4|1650120140|mg3c3b04ba|0.2.0|l1mzbpxy_s7bp4q6ggze|4330701|https://w.yangshipin.cn/|mozilla/5.0 (macintosh; ||Mozilla|Netscape|MacIntel|"
1
2
3
4
5
6
7
8
9#捕获Fn的值
def get_fn(play_url):
s = "|%s|%s|mg3c3b04ba|0.2.0|l1mzbpxy_s7bp4q6ggze|4330701|https://w.yangshipin.cn/|mozilla/5.0 (macintosh; ||Mozilla|Netscape|MacIntel|"
vid = play_url.split('&')[1].split('=')[1]
import time
ctime = str(int(time.time()))
fn = s%(vid,ctime)
return fn
print(get_fn('https://w.yangshipin.cn/video?type=0&vid=l000073qxd4&ptag=4_2.4.2.23160_copy'))
qn:会发现js代码中有两行代码都是给qn赋值
1
2
3
4
5
6
7
8
9qn = Jn; //赋值位置1 断点1
else {
for (Mr = 0; Mr < Vn[Mt]; Mr++)
Xn = Vn[Lt](Mr),
Jn = (Jn << fe + 1360 + 9081 - 4920) - Jn + Xn,
Jn &= Jn;
qn = Jn //赋值位置2 断点2
//发现,是由断点2给qn赋值
//此处的Vn就是上一步的Fnjs改写:
1
2
3
4
5
6
7
8
9
10
11
12function get_qn(Vn) {
var fe = -5516
var Jn = 0;
var Lt = "charCodeAt";
var Mt = "length";
for (Mr = 0; Mr < Vn[Mt]; Mr++)
Xn = Vn[Lt](Mr),
Jn = (Jn << fe + 1360 + 9081 - 4920) - Jn + Xn,
Jn &= Jn;
qn = Jn
return qn;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19def get_fn(play_url):
s = "|%s|%s|mg3c3b04ba|0.2.0|l1mzbpxy_s7bp4q6ggze|4330701|https://w.yangshipin.cn/|mozilla/5.0 (macintosh; ||Mozilla|Netscape|MacIntel|"
vid = play_url.split('&')[1].split('=')[1]
import time
ctime = str(int(time.time()))
fn = s%(vid,ctime)
return fn
def get_qn(play_url):
import execjs
node = execjs.get()
fp = open('./qn_js.js','r',encoding='utf-8')
ctx = node.compile(fp.read())
Vn = get_fn(play_url)
ret = ctx.eval("get_qn('%s')"%Vn)
return ret
play_url = 'https://w.yangshipin.cn/video?type=0&vid=l000073qxd4&ptag=4_2.4.2.23160_copy'
print(get_qn(play_url))
- Kn(秘钥): "4E2918885FD98109869D14E0231A0BF4"
- Qn(IV):"16B17E519DDD0CE5B79D7A63A4DD801C"
获取密文数据(获取cKey的值):
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#获取C函数的返回值
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii
def aes_encrypt(data_string):
key_string = "key值"
key = binascii.a2b_hex(key_string)
iv_string = "IV值"
iv = binascii.a2b_hex(iv_string)
aes = AES.new(
key=key,
mode=AES.MODE_CBC,
iv=IV
)
raw = pad(data_string.encode('utf-8'), 16)
aes_bytes = aes.encrypt(raw)
return binascii.b2a_hex(aes_bytes).decode().upper()
data = "明文数据"
result = aes_encrypt(data)
C_value = '__01'+result #获取C函数完整的返回值
print(result)