zoukankan      html  css  js  c++  java
  • 解密腾讯课堂视频缓存文件

    背景


    众所周知,类似腾讯课堂的视频有一定的期限。
    如果是付费了,又要永久保存,该怎么办呢?
    录屏?也许是一种思路。但这个时间成本可能会比较大。
    接下来,我提供一种解开加密视频的一种思路,仅供参考。

    探索之旅


    对分析过程不感兴趣的可以直接跳到最后一节

    网页版腾讯课堂分析

    1. 分析网络请求,过滤关键词m3u8

    直接copy这个url,发现可以下载一个m3u8文件,但是只有几十kb,显然不是视频,以文本方式打开该文件,发现像类似配置相关的信息,那什么是m3u8呢?

    1. 关于m3u8

    详细可参考文档m3u8 文件格式详解
    当 m3u8 文件作为媒体播放列表(Meida Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式如下所示:

    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:9.009,
    http://media.example.com/first.ts
    #EXTINF:9.009,
    http://media.example.com/second.ts
    #EXTINF:3.003,
    http://media.example.com/third.ts
    
    1. 分析网络请求,过滤关键词.ts

    竟然m3u8是媒体播放器列表,包含多个ts播放片段资源,再看看.ts视频片段相关的请求

    1. 浏览器下载.ts视频文件
      直接访问该ts视频的url,发现可以直接下载ts视频文件。但选择播放器播放时,却无法播放,可以肯定的是该视频加密了。再看看m3u8文件(文本方式查看):
    ......
    #EXT-X-KEY:METHOD=AES-128,URI="https://ke.qq.com/cgi-bin/qcloud/get_dk?edk=CiC61gXccR0pWqrCSxxx&fileId=5285890801738655666&keySource=VodBuildInKMS&token=dWluPTE0NDExNTIwMDM0NjIwNTA",IV=0x00000000000000000000000000000000
    .......
    

    METHOD:该值是一个可枚举的字符串,指定了加密方法。AES-128:表示表示使用 AES-128 进行加密。
    URI:指定密钥路径。该密钥是一个 16 字节的数据。该键是必须参数,除非 METHOD 为NONE。
    IV:该值是一个 128 位的十六进制数值。AES-128 要求使用相同的 16字节 IV 值进行加密和解密。使用不同的 IV 值可以增强密码强度。

    1. 分析进展
    • 可下载分段的ts文件(该片段url包含sign之类的信息,可以正常下载),但加密了
    • 从m3u8文件可以获取到加密方式和加密密钥
    • m3u8文件中也包含片段信息及sign之类的信息

    缓存文件分析

    1. 获取缓存文件
      腾讯课堂缓存文件存放位置在/sdcard/Android/data/com.tencent.edu/files/tencentedu/video/txdownload/
      可以通过adb将手机中的缓存文件pull到电脑
    adb pull /sdcard/Android/data/com.tencent.edu/files/tencentedu/video/txdownload/
    
    1. 分析缓存文件sqlite
      既然是sqlite文件,放到navicat或者datagrip中看看

    metadata和caches,这里重点关注caches表。从数据库中可以看出在里面塞入了.ts视频文件片段,m3u8目录信息和解密密钥,跟从网页分析得出的数据基本一致。

    • caches表第一行:m3u8文件内容,跟上面网页下载到m3u8文件内容基本一致
    • caches表第二行:AES-128解密文件(16bytes)
    • caches表其余行:ts文件分片
    1. 分析ts片段
      可以看出ts片段的格式是
    https://1258712167.vod2.myqcloud.com/fb8e6c92vodtranscq1258712167/af9366775285890801738655666/drm/v.f56150.ts?start=0&end=250431&type=mpegts
    ...
    ...
    https://1258712167.vod2.myqcloud.com/fb8e6c92vodtranscq1258712167/af9366775285890801738655666/drm/v.f56150.ts?start=37710016&end=38045039&type=mpegts
    

    start和end是连续的,决定了片段的起止
    将链接修改为start=0&end=38045039(即end为最后一个片段的end),访问该链接,但提示need sign info,显然其中缺少了sign相关信息,之前通过网页分析时可以轻松得到sign信息(ts链接或者m3u8文件中获取)

    1. 分析进展
    • 通过修改start,end并拼接sign可以下载完整的ts视频
    • 通过sqlite数据中同样可以获取加密相关信息
    1. 需要解决的问题
    • 用密钥怎么解密完整的视频?

    破解加密的缓存文件


    从sqlite文件获取必要信息

    import sqlite3 as db
    
    # 获取m3u8文件的下载链接,视频base_url,aes解密key
    def get_url_key(sqlite_file):
        con = db.connect(sqlite_file)
        cu = con.cursor()
        result = cu.execute('SELECT * FROM caches')
    
        data = result.fetchall()
        m3u8_url = data[0][0]
        # video_base_url = data[2][0]
        video_base_url = data[2][0].split("?")[0]
        aes_key = data[1][1]
        return m3u8_url, video_base_url, aes_key
    

    拼接完整的视频片段链接

    import m3u8
    import re
    
    def get_seg_uri(m3u8_file):
        m3u8_obj = m3u8.load(m3u8_file)
        seg_start_uri = m3u8_obj.segments[0].uri
        seg_end_uri = m3u8_obj.segments[-1].uri
        end_time = re.search('end=([0-9]*)', seg_end_uri).group(1)
        pattern = r'end=[0-9]*'
        seg_uri = re.sub(pattern, r'end={}'.format(end_time), seg_start_uri)
        return seg_uri.split('?')[-1]
    

    引入第三方库m3u8解析库,详细可参看m3u8解析库github地址

    解密完整视频文件

    import os
    from Crypto.Cipher import AES
    import requests
    import threading
    
    THIS_FILE_PATH = os.path.dirname(os.path.realpath(__file__))
    OUTPUT_FILE_PATH = os.path.join(THIS_FILE_PATH, 'output')
    SQLITE_FILE_PATH = os.path.join(THIS_FILE_PATH, 'sqlite')
    
    def decrypt_video(sqlite_file):
        print("thread start......")
        file_saved_name = sqlite_file.split('.')[0] + ".mp4"
        m3u8_url, video_base_url, aes_key = get_url_key(os.path.join(SQLITE_FILE_PATH, sqlite_file))
        # print(m3u8_url, video_base_url, aes_key, sep='
    ')
        seg_uri = get_seg_uri(m3u8_url)
        video_url = video_base_url + '?' + seg_uri
        # print(video_url)
        res = requests.get(video_url, stream=True)
        video_stream = b''
        for chunk in res.iter_content(chunk_size=1024 * 20):
            if chunk:
                video_stream += chunk
        decrypted_video = AES.new(aes_key, AES.MODE_CBC).decrypt(video_stream)
        with open(os.path.join(OUTPUT_FILE_PATH, file_saved_name), 'ab') as f:
            f.write(decrypted_video)
        print("thread end .....")
    
    
    for file in os.listdir(SQLITE_FILE_PATH):
        if file.endswith('sqlite'):
            decrypt_video_thread = threading.Thread(target=decrypt_video, args=(file,))
            decrypt_video_thread.start()
    

    引入加解密库Crypto,如遇导包问题请参考python3中Crypto ModuleNotFoundError

    几点总结


    1. 通过下载完整视频再解密受网络影响
    2. 考虑直接走sqlite文件得到完整视频,16byte流相加合并是可以得到完整视频的,但声音并不能同步,放弃了
    3. 通过单个ts片段解密多个视频,一个视频解成100+个片段观看不方便,弃之
    4. sqlite文件如果是很早之前缓存的,方法失效,可能是sign之类的信息失效,如果想解密视频文件,需要是近期缓存的sqlite文件
    5. 很多视频网站的视频加解密方式是类似这样的,如下开课吧示例,其他类型站点视频有需要的可自行尝试
    • 开课吧示例:
    from Crypto.Cipher import AES
    import requests
    
    # key_url = "https://p.bokecc.com/servlet/hlskey?info=50E8E2C985FD43379C33DC5901307461&t=1601024556&key=92E401B7B37EC52BF51B9B71B15DABF8"
    # key = requests.get(key_url)
    # print(key.content)
    # iv = 0x50E8E2C985FD43379C33DC5901307461
    # iv = 0x50E8E2C985FD43379 # 取高16位
    # iv = iv.to_bytes(length=16, byteorder='big')
    # iv = b'0000000000000000'  # 经过测试,iv在这里不起作用
    
    for i in range(725):
        video_url = "https://cd15-ccd1-2.play.bokecc.com/flvs/0118CC77B985808D/2020-07-29/50E8E2C985FD43379C33DC5901307461-20.ts?video={}&t=1601134081&key=B23C32BEF607876499FF469AA5B4B46C&tpl=10&tpt=112".format(i)
        print("download video-{}....".format(i))
        res = requests.get(video_url)
    
        # hlskey通过key_url可下载到,也可通过requests.get下载,然后aes_key=key_res.content
        with open("hlskey", "rb") as f:
            key = f.read()
            content_video_part = AES.new(key, AES.MODE_CBC).decrypt(res.content)
    
        with open("kaikeba.mp4", 'ab') as f:  # 追加保存解密结果
            f.write(content_video_part)
    

    写在最后

    如有什么疑问,可关注我的公号 CodeMonkeyJerry,欢迎留言

  • 相关阅读:
    Request功能
    Request继承体系
    HTTP协议:请求消息的数据格式---Request
    HTTP协议---HttpServlet
    hdu 1575 矩阵连乘2
    hdu 1005 Number Sequence(矩阵连乘+二分快速求幂)
    矩阵连乘
    MongoDB(六):选择字段、限制记录数、排序记录
    MongoDB(五):更新文档、删除文档
    爬虫(八):文件处理
  • 原文地址:https://www.cnblogs.com/mliangchen/p/13884294.html
Copyright © 2011-2022 走看看