腾讯课堂app 离线视频格式破解——完结

之前因为温习3dsmax,在腾讯课堂app上缓存了不少的视频,因为缓存在手机观看屏幕比较小,一直想把缓存的视频转移到PC上看,对于我这种信号时有时无的人来说,手机是最好的找信号工具,故视频缓存在手机上了。

先放完整代码和转换结果:


由.sqlite文件转换成的ts片段(还没合并)
# _*_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表。


caches表内容,另外可右键-设计表可以查看到value字段是blob类型

这印证了之前的推测,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


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容