解密騰訊課堂視訊快取檔案

小猴子jerry發表於2020-10-27

背景


眾所周知,類似騰訊課堂的視訊有一定的期限。
如果是付費了,又要永久儲存,該怎麼辦呢?
錄屏?也許是一種思路。但這個時間成本可能會比較大。
接下來,我提供一種解開加密視訊的一種思路,僅供參考。

探索之旅


對分析過程不感興趣的可以直接跳到最後一節

網頁版騰訊課堂分析

  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='\n')
    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,歡迎留言

相關文章