Reverse-yang视频

摘要

本文内容转自网络,个人学习记录使用,请勿传播

需求

  • 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
      5
      def 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
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
ctime: 2022-04-16 20:25:42  #变化

ua: mozilla/5.0 (macintosh; intel mac os x 10_15_7) applewebkit/537.36 (khtml, like gecko) chrome/100.0.4896.88 safari/537.36 #固定

hh_ua: mozilla/5.0 (macintosh; intel mac os x 10_15_7) applewebkit/537.36 (khtml, like gecko) chrome/100.0.4896.88 safari/537.36 #固定
platform: 4330701 #固定
guid: l1mzbpxy_s7bp4q6ggze #固定
Pwd: 1698957057 #固定
version: wc-0.2.0 #固定
url: https://w.yangshipin.cn/video?type=0&vid=l000073qxd4&ptag=4_2.4.2.23160_copy #当前播放页的【固定】
hh_ref: https://w.yangshipin.cn/video?type=0&vid=l000073qxd4&ptag=4_2.4.2.23160_copy #当前播放页的【固定】

vid: l000073qxd4 #同一个视频固定,不同视频是变化的
isfocustab: 1 #固定
isvisible: 1 #固定
idx: 0 #固定
val: 8834 #变化
pid: l21u1gis_p0jntraza6l #变化的
bi: 0 #固定
bt: 0 #固定
vurl: https://mp4playcloud-cdn.ysp.cctv.cn/l000073qxd4.NYYk10002.mp4?sdtfrom=4330701&guid=l1mzbpxy_s7bp4q6ggze&vkey=21C1BEE4075885966BD6864866A2A57CA7C53E76CDA6725FC41E7F8715340C138641246FCF3B3E6D8505928599551FC212E235B411EB33F275D5B525F70AC53CF03E24CFC6917D5BC04E4873EA9AAE50D36D8C412F1A668F9E207745DE4EA16691A719E70ABCCD286AFA1A1E0DFFC04F07D6F9960A54EC6D94B94EB2DB38FCBE&platform=2 #变化的
step: 6 #固定
val1: 1 #固定
val2: 1 #固定
fact1: #固定
fact2: #固定
fact3: #固定
fact4: #固定
fact5: #固定
  • 动态变化的请求参数:
1
2
3
4
5
6
7
8
9
ctime: 2022-04-16 20:25:42  #变化

vid: l000073qxd4 #同一个视频固定,不同视频是变化的

val: 8834 #变化

pid: l21u1gis_p0jntraza6l #变化的

vurl: https://mp4playcloud-cdn.ysp.cctv.cn/l000073qxd4.NYYk10002.mp4?sdtfrom=4330701&guid=l1mzbpxy_s7bp4q6ggze&vkey=21C1BEE4075885966BD6864866A2A57CA7C53E76CDA6725FC41E7F8715340C138641246FCF3B3E6D8505928599551FC212E235B411EB33F275D5B525F70AC53CF03E24CFC6917D5BC04E4873EA9AAE50D36D8C412F1A668F9E207745DE4EA16691A719E70ABCCD286AFA1A1E0DFFC04F07D6F9960A54EC6D94B94EB2DB38FCBE&platform=2 #变化的
ctime的处理
1
2
3
4
5
6
def get_ctime():
import time
now = time.time()
l_time = time.localtime(now)
f_time = time.strftime("%Y-%m-%d %H:%M:%S",l_time)
return f_time
vid的处理
  • 在视频的播放页的url中就会存在vid的值,因此直接对url中的vid进行提取即可!
  • 处理方式1:
1
2
3
def get_vid(play_url):
vid = play_url.split('&')[1].split('=')[-1]
return vid
  • 处理方式2:

    • 基于抓包工具全局搜索vid:,在cctvh5-web.js中发现了vid的生成代码:

      • 在js数据包中局部搜索vid:即可,然后在可疑位置打断点

      • Snip20220416_2

      • vid = t.tid,因此,vid的值就是从中获取的,因此需要分析t是什么

        • 1
          2
          3
          var 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
            20
            function 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
            7
            def 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
2
3
4
5
6
val: e - t 
//只需要知道e和t是什么即可
//startLoadTime: t, playingTime: e
//搜索startLoadTime,playingTime发现它们都是时间戳
//startLoadTime刷新页面的时间
//playingTime点击播放按钮后对应的时间戳
  • 通过测试返现val就是刷新页面和点击播放按钮对应的时间间隔
    • 因此val的值自己随机给一个随机数即可(2000-10000之间)
    • val写成固定值3893也可以的,只要不是0即可,大于1000就行。
pid的处理
  • 全局搜索pid,最终定位到stepReport数据包,分析发现:
1
2
3
pid:e
e:a.a.getPlayerId()
//因此需要改写getPlayerId的实现即可
1
2
3
4
5
6
7
8
9
//getPlayerId的实现:
getPlayerId() {
return this.createGUID()
}
createGUID: ()=>`${(new Date).getTime().toString(36)}_${Math.random().toString(36).replace(/^0./, "")}`
//改写工具不认识箭头符号的函数定义,因此可以将其修改为普通的函数定义
function createGUID(){
return (new Date).getTime().toString(36)+'_'+Math.random().toString(36).replace(/^0./, "")
}
1
2
3
4
5
6
7
def get_pid():
import execjs
node = execjs.get()
fp = open('./pid_js.js','r',encoding='utf-8')
ctx = node.compile(fp.read())
ret = ctx.eval("createGUID()")
return ret
vurl的处理
1
2
3
4
5
6
7
8
9
10
11
#下面是vurl参数的值
https://mp4playcloud-cdn.ysp.cctv.cn/l000073qxd4.NYYk10002.mp4?sdtfrom=4330701&guid=l1mzbpxy_s7bp4q6ggze&vkey=21C1BEE4075885966BD6864866A2A57CA7C53E76CDA6725FC41E7F8715340C138641246FCF3B3E6D8505928599551FC212E235B411EB33F275D5B525F70AC53CF03E24CFC6917D5BC04E4873EA9AAE50D36D8C412F1A668F9E207745DE4EA16691A719E70ABCCD286AFA1A1E0DFFC04F07D6F9960A54EC6D94B94EB2DB38FCBE&platform=2

#vurl就是一个携带了请求参数的url
sdtfrom=4330701 #固定

guid=l1mzbpxy_s7bp4q6ggze #固定

vkey=21C1BEE4075885966BD6864866A2A57CA7C53E76CDA6725FC41E7F8715340C138641246FCF3B3E6D8505928599551FC212E235B411EB33F275D5B525F70AC53CF03E24CFC6917D5BC04E4873EA9AAE50D36D8C412F1A668F9E207745DE4EA16691A719E70ABCCD286AFA1A1E0DFFC04F07D6F9960A54EC6D94B94EB2DB38FCBE #变化的

platform=2 #变化

因此:想要处理vurl就需要先处理vkey,因此问题的矛头指向vkey

vkey的处理
  • vkey是一个请求参数,思考:动态变化的请求参数的来源有哪些渠道?

    • 1.可以出现在其他请求的响应数据中
    • 2.js算法生成
  • 全局搜索vkey,定位到playvinfo这个数据包:发现了

  • 1
    "fvkey": "73CD259DE3D16E844E8F9F14959C40F306ED28D2DFE0CC35575A2ADD64A1A371E3B16DCA12F27C06468CAF1475038198F306F9C3DFDBC2B7F5F4200D6A80987734DBBEA7F34762633C7D7A4D426E4998399228922E2148A3EB01DC0096F8BA0AF13213F8FDDCE13FE1950B9A2B7F537607BE7EB4DD98C35C463D74D646E8EA04"
  • 注意:如果我们获取了fvkey的值,则就处理好了vkey,如果vkey处理好了则就处理好了vurl

分析处理playvinfo数据包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
callback: jsonp1 
guid: l1mzbpxy_s7bp4q6ggze
platform: 4330701
vid: l000073qxd4
defn: hd
charge: 0
defaultfmt: auto
otype: json
defnpayver: 1
appVer: 0.2.0
sphttps: 1
sphls: 1
spwm: 4
dtype: 3
defsrc: 2
encryptVer: 8.1
sdtfrom: 4330701
cKey: --01EC059F9470FFF83731628044CB2458C2AD761DD15E21BAA23332D6C9C6922A73D99BD99EF0DFD098089913CD9430BAD13A54A38F1EE4C6EACCBB39642911055FB7D6B96E9DD710DAADBA02ED1B89A4D8E1374E01B92A6F19B1C84299E48B82F7527E4C47A3362BF40540D4B7EC21974AEDC44DDE777827F43148DB93570323A49244665557212175E9A9A06D53C92F804DF3D9832D67019CA1F8080F2D7EDBCF
flowid: l21x9j6y_g9jlkg2piku
  • 动态变化的请求参数:
1
2
3
vid: l000073qxd4  #已知的
flowid: l21x9j6y_g9jlkg2piku #之前pid
cKey:xxx
ckey的处理(最难)
  • 全局搜索ckey,定位到yspalyer数据包,分析后发现getRequestCkey函数返回的内容就是ckey的值。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    getRequestCkey(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
      14
      function 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
      6
      import 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
      23
      return 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
            9
            qn = 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就是上一步的Fn
          • js改写:

          • 1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12
            function 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
            19
            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

            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)