之前因为温习3dsmax,在腾讯课堂app上缓存了不少的视频,因为缓存在手机观看屏幕比较小,一直想把缓存的视频转移到PC上看,对于我这种信号时有时无的人来说,手机是最好的找信号工具,故视频缓存在手机上了。
先放完整代码和转换结果:
# _*_coding:utf-8 _*_
# @Time : 2019/10/16 16:04
# @Author : Shek
# @FileName: run.py
# @Software: PyCharm
import sqlite3 as db
from Crypto.Cipher import AES
def db_fetcher(filename: str):
'''
处理.sqlite文件的入口
:param filename: .sqlite文件名
:return:
'''
caches_table_name = 'caches'
con = db.connect(filename)
cu = con.cursor()
result = cu.execute('SELECT * FROM {}'.format(caches_table_name))
data = result.fetchall()
AES_KEY = data[1][1]
for i in range(2, len(data)):
raw = data[i][1]
dump_name = 'dump-{}.ts'.format(i)
plain = aes128_decrypt(raw=raw, key=AES_KEY, dump_file=dump_name)
if plain:
print('{} of {} dumped succeed'.format(i - 1, len(data)))
def aes128_decrypt(raw: bytes, key: bytes, iv: bytes = b'0000000000000000', dump_file: str = ''):
'''
二进制文件的AES-128解密
:param raw: 原始二进制内容
:param key: AES-128文件二进制内容(16bytes)
:param iv: AES_IV
:param dump_file: 保存文件名
:return: 正常True,异常False
'''
data = raw
cipher = AES.new(key, AES.MODE_CBC, iv)
plain = cipher.decrypt(data)
try:
open(dump_file, 'wb').write(plain)
return True
except Exception as e:
print(e)
return False
db_file = '1e6e9a425ee02902acd996fa5f87eff4.m3u8.sqlite'
db_fetcher(filename=db_file)
缓存数据位置
腾讯课堂app的数据存储在Android/data/com.tencent.edu文件夹中,并且使用sqlite数据库文件进行离线视频进行存储,这首先就很奇怪了,数据库文件一般存的是数据,怎么会存储媒体文件呢?结合数据库经验,猜测应该是在数据库中以blob类型进行存储。所谓blob类型,就是二进制对象,以二进制格式存储所有类型的数据,尽管blob类型在文件后处理方面有一定的优势,但是会在一定程度上降低数据库的性能(使用Navicat打开的时候,我都怀疑屏幕坏了,一卡一卡的……)
数据库结构
使用navicat我们打开其中一个sqlite文件,其中有两张表:metadata和caches,这里我们重点关注caches表。
这印证了之前的推测,sqlite数据库文件中,利用blob类型形成了“类目录”,在里面塞入了.ts视频文件片段、m3u8目录信息和解密密钥(密钥这个后面说)。
再回到caches表中,可以将表中数据按行分为3类:
1.第一行:视频片段目录信息(m3u8)
2.第二行:AES密钥
3.第三行以及以后:视频分段文件,一行代表一个
来看一下第一行数据:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-KEY:METHOD=AES-128,URI="https://ke.qq.com/cgi-bin/qcloud/get_dk?edk=CiCBJj%2BLeHsBnilhenVC3KnMSpmaTIzwGJ%2FWPpEf7symChCO08TAChiaoOvUBCokOTMyNDg4YmItOWZjYS00MzFiLWJiYjItNjFmMDhjYjNlYmM3&fileId=5285890791427386588&keySource=VodBuildInKMS&token=dWluPTE0NDExNTIxMjMwMDk0MTg2MDt0ZXJtX2lkPTEwMDM5Nzc1Njtwc2tleT07ZXh0PTU1ZTczOGQzNjc1YTI2Nzc2YzkxODA4M2FmZTJiMjMwZDIwNzY4Y2M4MDUyMGI4ZTljMzUyZTZiZDA5NDFlOTA3NWYwZWIzYjY2MjNmNjVmODRhNTFiZjFiYjMzYzMwMDM1Y2NmOTYxMjFmNzgzODRjNTc5NWRiNzlhNDU3ZGJlZDhlMWU2NjlkYTgwZWU4Mw",IV=0x00000000000000000000000000000000
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:10
#EXT-X-KEY:METHOD=AES-128,URI="https://ke.qq.com/cgi-bin/qcloud/get_dk?edk=CiCBJj%2BLeHsBnilhenVC3KnMSpmaTIzwGJ%2FWPpEf7symChCO08TAChiaoOvUBCokOTMyNDg4YmItOWZjYS00MzFiLWJiYjItNjFmMDhjYjNlYmM3&fileId=5285890791427386588&keySource=VodBuildInKMS&token=dWluPTE0NDExNTIxMjMwMDk0MTg2MDt0ZXJtX2lkPTEwMDM5Nzc1Njtwc2tleT07ZXh0PTU1ZTczOGQzNjc1YTI2Nzc2YzkxODA4M2FmZTJiMjMwZDIwNzY4Y2M4MDUyMGI4ZTljMzUyZTZiZDA5NDFlOTA3NWYwZWIzYjY2MjNmNjVmODRhNTFiZjFiYjMzYzMwMDM1Y2NmOTYxMjFmNzgzODRjNTc5NWRiNzlhNDU3ZGJlZDhlMWU2NjlkYTgwZWU4Mw",IV=0x00000000000000000000000000000000
#EXTINF:10.000,
v.f30741.ts?start=0&end=273743&type=mpegts
#EXT-X-KEY:METHOD=AES-128,URI="https://ke.qq.com/cgi-bin/qcloud/get_dk?edk=CiCBJj%2BLeHsBnilhenVC3KnMSpmaTIzwGJ%2FWPpEf7symChCO08TAChiaoOvUBCokOTMyNDg4YmItOWZjYS00MzFiLWJiYjItNjFmMDhjYjNlYmM3&fileId=5285890791427386588&keySource=VodBuildInKMS&token=dWluPTE0NDExNTIxMjMwMDk0MTg2MDt0ZXJtX2lkPTEwMDM5Nzc1Njtwc2tleT07ZXh0PTU1ZTczOGQzNjc1YTI2Nzc2YzkxODA4M2FmZTJiMjMwZDIwNzY4Y2M4MDUyMGI4ZTljMzUyZTZiZDA5NDFlOTA3NWYwZWIzYjY2MjNmNjVmODRhNTFiZjFiYjMzYzMwMDM1Y2NmOTYxMjFmNzgzODRjNTc5NWRiNzlhNDU3ZGJlZDhlMWU2NjlkYTgwZWU4Mw",IV=0x00000000000000000000000000000000
#EXTINF:10.000,
v.f30741.ts?start=273744&end=470959&type=mpegts
...
#EXT-X-KEY:METHOD=AES-128,URI="https://ke.qq.com/cgi-bin/qcloud/get_dk?edk=CiCBJj%2BLeHsBnilhenVC3KnMSpmaTIzwGJ%2FWPpEf7symChCO08TAChiaoOvUBCokOTMyNDg4YmItOWZjYS00MzFiLWJiYjItNjFmMDhjYjNlYmM3&fileId=5285890791427386588&keySource=VodBuildInKMS&token=dWluPTE0NDExNTIxMjMwMDk0MTg2MDt0ZXJtX2lkPTEwMDM5Nzc1Njtwc2tleT07ZXh0PTU1ZTczOGQzNjc1YTI2Nzc2YzkxODA4M2FmZTJiMjMwZDIwNzY4Y2M4MDUyMGI4ZTljMzUyZTZiZDA5NDFlOTA3NWYwZWIzYjY2MjNmNjVmODRhNTFiZjFiYjMzYzMwMDM1Y2NmOTYxMjFmNzgzODRjNTc5NWRiNzlhNDU3ZGJlZDhlMWU2NjlkYTgwZWU4Mw",IV=0x00000000000000000000000000000000
#EXTINF:1.154,
v.f30741.ts?start=3967568&end=4075119&type=mpegts
#EXT-X-ENDLIST
咋一看,非常典型的配置类信息,这就是M3U8目录信息的存储行(第一行),记录了整个视频文件应该由哪些片段进行合成、时间位置、格式版本号等等。那么M3U8是什么呢?
M3U8
简单在网上搜索了一下,参考文章 M3U8格式讲解及实际应用分析,M3U8主要用于多码率适配,根据网络带宽,客户端自动选择一个适合自己码率的文件进行播放,保证视频流的流畅,而M3U8是M3U文件的拓展,对照样本来看一下:
EXT-X-VERSION:3
版本信息,可以没有。
EXT-X-MEDIA-SEQUENCE:0
定义当前m3u8文件中第一个文件的序列号,每个ts文件有固定的序列号,用于MBR时切换码率进行对齐。
EXT-X-TARGETDURATION:10
定义每个TS的最大长度。
EXT-X-KEY:METHOD=AES-128,URI="https://ke.qq.com/cgi-bin/qcloud/get_dk?edk=CiCBJj%2BLeHsBnilhenVC3KnMSpmaTIzwGJ%2FWPpEf7symChCO08TAChiaoOvUBCokOTMyNDg4YmItOWZjYS00MzFiLWJiYjItNjFmMDhjYjNlYmM3&fileId=5285890791427386588&keySource=VodBuildInKMS&token=dWluPTE0NDExNTIxMjMwMDk0MTg2MDt0ZXJtX2lkPTEwMDM5Nzc1Njtwc2tleT07ZXh0PTU1ZTczOGQzNjc1YTI2Nzc2YzkxODA4M2FmZTJiMjMwZDIwNzY4Y2M4MDUyMGI4ZTljMzUyZTZiZDA5NDFlOTA3NWYwZWIzYjY2MjNmNjVmODRhNTFiZjFiYjMzYzMwMDM1Y2NmOTYxMjFmNzgzODRjNTc5NWRiNzlhNDU3ZGJlZDhlMWU2NjlkYTgwZWU4Mw",IV=0x00000000000000000000000000000000
定义加密方式和密钥文件的地址,获得16字节的密钥解码ts文件,这里METHOD=AES-128表示使用AES-128进行加密/解密,URI表示密钥文件位置/路径,其中的IV应该是与AES有关的一个参数,类似于偏移量?(在Crypto.Cipher.AES中查看references时看到过)
EXTINF:10.000,
v.f30741.ts?start=0&end=273743&type=mpegts
一些基本信息,数据内容的长度、文件名、时间对齐、文件类型等。
总结下来就是:
caches表第一行:m3u8文件内容
caches表第二行:AES-128解密文件(16bytes)
caches表其余行:ts文件分片
现在基本已经明确了腾讯课堂app缓存文件的数据格式,其实就是m3u8的数据库拓展格式,在一个sqlite文件中利用blob存放了m3u8目录文件、ts分片文件和可能用到的AES-128解密文件(16bytes),根据这个思路,下面开始写解密程序,这里需要用到外部库pycrypto。
import sqlite3 as db
from Crypto.Cipher import AES
def aes128_decrypt(raw: bytes, key: bytes, iv: bytes = b'0000000000000000', dump_file: str = ''):
'''
二进制文件的AES-128解密函数
:param raw: 原始二进制内容
:param key: AES-128文件二进制内容(16bytes)
:param iv: AES_IV
:param dump_file: 保存文件名
:return: 正常True,异常False
'''
data = raw
cipher = AES.new(key, AES.MODE_CBC, iv)
plain = cipher.decrypt(data)
try:
open(dump_file, 'wb').write(plain)
return True
except Exception as e:
print(e)
return False
def db_fetcher(filename: str):
'''
处理.sqlite文件的入口
:param filename: .sqlite文件名
:return:
'''
caches_table_name = 'caches'
con = db.connect(filename)
cu = con.cursor()
result = cu.execute('SELECT * FROM {}'.format(caches_table_name))
data = result.fetchall()
AES_KEY = data[1][1]
for i in range(2, len(data)):
raw = data[i][1]
dump_name = 'dump-{}.ts'.format(i)
plain = aes128_decrypt(raw=raw, key=AES_KEY, dump_file=dump_name)
if plain:
print('{} of {} dumped succeed'.format(i - 1, len(data)))
执行:
db_file = '1e6e9a425ee02902acd996fa5f87eff4.m3u8.sqlite'
db_fetcher(filename=db_file)
总结
以上代码只是验证,并未完善,有兴趣的朋友可以继续深化,封装类,写GUI等。回头想了想,还是文件名*.m3u8.sqlite给我提供了思路,不然看着那么大一个文件,我应该没什么勇气扔进winhex里比对文件头,第一反应就是整个文件都被加密了,却不曾想到腾讯课堂app中首先在外层套的还是一个正常的外衣:一个数据库,然后在里面存放需要的媒体数据。至于使用的AES-128加密,是归咎于m3u8提供了此选项,并不是腾讯课堂app的设计功能,所以才会导致AES-128解密文件共同存放于一个数据库文件中的情况,没有引起重视。
文章发布时已通过微信联系微信团队进行处理,只是聊天画风有点:
终。
--------分割线 2019.10.18更新
关于m3u8信息、aes密钥和ts片段信息的位置,后来实际使用发现并不是严格的按数据行区分,有时候还会获取多次aes密钥(尽管内容一样)甚至先存储ts片段再下载aes密钥。对于此,本文代码无法完美处理,修正、封装好的类已于本文发布第二天完善,详细请看:(文章未完成)
--------分割线 2019.11.14更新
封装好的类已同步更新到原github仓库,同时提供了adb支持,打开手机调试模式,直接转换到本地计算机,详细请看:
tencent-edu-wrapper:http://github.com/r00t1900/tencent-edu-wrapper