逆向并下载某课堂付费视频,一种独辟蹊径的方式

21 阅读7分钟

加密与解密

由于是付费课程,加密是必不可少的,这个网站使用了m3u8作为播放方式,m3u8是一个​播放列表索引文件。它本质上是一个普通的文本文件,只不过后缀名是 .m3u8

它是苹果公司推出的 HLS (HTTP Live Streaming) 协议的核心部分,现在广泛应用于各类手机直播、点播以及网页视频播放。

1. 它是如何工作的?

如果你直接下载一个 1GB 的 MP4 视频,网络不好时会非常卡。m3u8 采用了不同的逻辑:

  • 切片​:它将一整段视频切割成无数个微小的短片段(通常是 .ts 格式,每段几秒钟)。
  • 索引​:m3u8 文件就像一份“清单”,记录了这些 ts 片段的下载地址和播放顺序。
  • 播放​:播放器先下载 m3u8,根据清单一段一段地下载并拼接播放。

2. m3u8 内部长什么样?

如果你用记事本打开一个 m3u8 文件,你会看到类似这样的内容:

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXTINF:10.0,
https://example.com/video/part1.ts
#EXTINF:10.0,
https://example.com/video/part2.ts
...

普通的ts文件是不加密的,下载后直接可以用一些播放器播放,所以解析m3u8 中所有ts文件,然后把这些所有ts文件合并成一个mp4文件即可,可以使用ffmpeg合并

但问题就是在这类网站中ts是加密的,m3u8中自带一个#EXT-X-KEY用于加密,简单来说,它的作用是告诉播放器:"接下来的视频分片(ts文件)是加密的,你需要去哪里下载密钥,并用什么算法来解密它们。"

1. 核心属性解析

一个典型的 #EXT-X-KEY 标签看起来像这样:#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key",IV=0x123456789...

它通常包含以下几个核心参数:

  • METHOD(加密算法)
    • NONE:不加密(默认)。
    • AES-128:最常见的加密方式,使用 128 位密钥。
    • SAMPLE-AES:通常用于高阶加密(如 FairPlay DRM),仅加密音频或视频的部分采样数据。
  • URI(密钥地址)
    • 播放器通过这个 URL 获取用于解密的二进制密钥。如果这个链接失效或需要授权(比如 Cookie 或 Token),视频就无法播放。
  • IV(初始化向量)
    • 一个 16 字节的十六进制数,配合密钥增加解密安全性。如果省略,通常会使用媒体序列号作为 IV。
  • KEYFORMAT(密钥格式)
    • 定义密钥的存储格式(如 identity 表示原始二进制,或者特定的 DRM 标准)。

当播放器解析 M3U8 文件时,它的流程如下:

  1. 扫描标签​:看到 #EXT-X-KEY
  2. 获取密钥​:向 URI 指定的地址发送请求,下载 16 字节的密钥。
  3. 解密分片​:下载后续的 .ts 视频分片。由于这些分片是加密的原始数据,无法直接播放,播放器会利用刚才下载的密钥和指定的算法进行实时解密。
  4. 渲染播放​:将解密后的视频流交给解码器。

所以对于这类加密的网站,下载密钥后,自行解密也是可以做到逆向的。

而现在面对的不同,实际中的密钥,是无法解密的,而本次演示的网站,在长达2天半的研究下,他一共有4道锁,

第一道是前端限制,在打开"开发者工具"时候,页面会瞬间跳转到空白页面,这显然是前端做了监听,防止打开开发者工具,只需要安装油猴+"反 devtools-detector 反调试"插件即可,在 devtools-detector 中配置要反的域名。

第2、3道是根据课程id获取播放的m3u8地址,虽然可以通过开发者工具直接看到请求地址,但是我们要做的是通过程序批量下载,所以这种方式不行,需要逆向他的解密过程,好在这部分不是很难,而最后一道是很难的,用于获取ts的解密密钥,跟踪了老半天,发现最他的解密太复杂了,虽然定位到了具体的方法,但是涉及到的上下文太多,用程序无法还原出他的解密过程,所以放弃了,

但是,天无绝人之路,后来又研究了下,这个网站使用了腾讯云点播,引入他的js,然后用下面代码,传入指定参数,就可以播放视频了,其中appID是固定的,应该是腾讯云分配给应用的唯一id,fileID是视频的id,而psign是签名,这个也比较好获取。


var config = {
autoplay: true,
posterImage: false,
muted: true,
playbackRates: [0.8, 1, 1.25, 1.5, 1.75, 2, 3],
controlBar: {
subsCapsButton: false,
textTrackSettings: false,
QualitySwitcherMenuButton: false,
},

persistTextTrackSettings: true,
fileID: fileID,
psign: psign,
appID: xxxx,
};
let h = undefined;
var player = TCPlayer("player-container-id", config, h);

psign的获取方式是请求他的服务器,有一个https://www.xxxx.com/xxx/api/video_play_detail地址,传入课程的id,和章节的id,会返回这个课程的详细信息,然后用一个aes解密就可以得到,下面是python的代码,aes的key是直接在js中写死的,跟踪一下就可以获取到。

url = f"https://www.xxx.com/xxx/api/video_play_detail?from=web&channel=web&devtype=web&platform_type=web&course_section_id={course_section_id}&course_id=xxx&play_course_section_id={play_course_section_id}&t={time.time()}"
cursor_detail = requests.get(url, headers=headers).json()
PLAY_AES_KEY = "bl538e945d5d3c41047b3b50j34ca72c"
ENCRYPTED_T = cursor_detail.get('data').get('play_auth')
decrypted_json = decrypt_play_auth(ENCRYPTED_T, PLAY_AES_KEY)
p_sign = json.loads(decrypted_json).get('p_sign')

然后网页就可以自动播放视频这个课程了,但是播放了有什么用呢,如何下载呢,下面就是一个技巧。

我们把TCPlayer("player-container-id", config, h);这些已经放在单独html服务了,然后用playwright访问http://127.0.0.1:5500/index.html,传入psign和file_id,在用playwright监听发起的m3u8链接,这样我们就通过程序提取到了关键的一部分,剩下一部分就是解密ts的密钥。

我们可以直接修改hls.min.1.1.7.js中的代码,在解密的方法结束后,直接console.log出密钥,哈哈哈哈哈,playwright是可以监听控制台输出的,这让我们就有了m3u8和ts的解密密钥,通过python就可以直接下载和解密了

image.png

hls.min.1.1.7.js是在腾讯云点播的tcplayer.v5.1.0.min.js中引入的,默认当然是请求他的服务器,我们在修改一下他的代码,让hls地址请求我们修改过后的。

image.png

下面是简短的代码演示

def call_playwright(my_psign, my_file_id):
    result = {"url": None, "decrypt_key": None}

    def handle_response(response):
        url = response.url
        pattern = r'video_[^/]+.m3u8'
        if re.search(pattern, url):
            result["url"] = url

    def handle_console(msg):
        text = msg.text
        match = re.search(r'解密:(\S+)', text)
        if match:
            result["decrypt_key"] = match.group(1)
            print(f"解密:{result['decrypt_key']}")

    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp("http://localhost:9222")
        default_context = browser.contexts[0]
        page = default_context.new_page()
        page.on("response", handle_response)
        page.on("console", handle_console)
        page.goto(f"http://127.0.0.1:5500/index.html?psign={my_psign}&fileID={my_file_id}")

        max_wait = 60
        for _ in range(max_wait):
            if result["url"] is not None and result["decrypt_key"] is not None:
                break
            page.wait_for_timeout(500)

        if result["url"]:
            print(f"检测到URL: {result['url']}")

        if not result["decrypt_key"]:
            print("超时:未检测到解密密钥")
        page.close()
        browser.close()
        return result

最后就是批量下载了,这就很容易了,每个课程都有一个课程id,请求他的课程详细接口,会返回所有章节信息,每个章节都有它位于腾讯云点播中的file_id。这些都是没有加密的。

另外从上面代码中可以发现,我们用chromium通过cdp协议连接本地的服务,因为发现,playwright自动下载的chromium,是无法播放视频的,提示当前浏览器不支持此协议,具体不知道是哪里的问题,所以我们通过chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\temp\chrome_dev"启动自己浏览器,一定要指明--user-data-dir