本文仅讲述 bilibili DRM 视频解密密钥的获取与整体流程简介,不包含任何实现与算法分析
浏览器角色: 现代浏览器 (如 Chrome, Firefox, Edge) 都内置了“加密媒体扩展” (Encrypted Media Extensions, EME)。EME 是一个 W3C 标准的 API,它允许网页通过 JavaScript 与浏览器的内容解密模块 (Content Decryption Module, CDM) 进行通信,而无需了解解密过程本身。
CDM (内容解密模块): 这是浏览器中真正负责解密的部分。每个浏览器或操作系统都有自己的 CDM,用于处理特定的 DRM 系统。例如:
Google Widevine: 通常用于 Chrome 浏览器和 Android 设备。
Apple FairPlay: 用于 Safari 浏览器和 iOS/macOS 设备。
Microsoft PlayReady: 用于 Edge 浏览器和 Windows 设备。
这里我们只需要了解 DRM 用到了 EME 和 CDM 模块即可
对于 DASH 这种自适应流媒体,你不能像播放普通 MP4 一样直接设置 video.src = "video.mp4"。你必须:
这是不使用库时最复杂的部分。整个过程是一个精确的“握手”协议:

286K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4m8#2k6%4k6Q4x3V1k6H3L8r3q4&6k6i4u0Q4x3V1k6%4k6h3u0Q4x3V1k6H3L8r3q4&6N6i4u0D9i4K6y4r3j5i4k6A6k6q4)9K6c8q4!0q4x3W2)9^5x3q4!0m8y4W2)9J5y4X3q4E0M7q4)9K6b7X3c8J5L8g2)9#2k6Y4c8W2j5$3S2Q4y4h3k6@1P5i4m8W2i4K6y4p5x3R3`.`.
视频元信息
5cbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6X3y4Q4x3X3c8V1M7X3#2Q4x3X3g2T1K9h3I4A6N6X3W2V1k6h3!0Q4x3X3g2U0L8$3#2Q4x3V1k6U0k6i4u0Q4x3V1k6T1K9h3I4A6j5X3W2D9K9g2)9#2k6X3y4W2M7Y4c8A6k6X3W2U0j5i4c8W2i4K6u0W2j5X3W2F1
2acK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6X3y4Q4x3X3c8V1M7X3#2Q4x3X3g2T1K9h3I4A6N6X3W2V1k6h3!0Q4x3X3g2U0L8$3#2Q4x3V1k6T1K9h3I4A6i4K6g2X3N6$3W2V1k6i4k6A6L8X3f1`.
c3aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4m8#2k6%4k6Q4x3V1k6H3L8r3q4&6k6i4u0Q4x3V1k6%4k6h3u0Q4x3V1k6H3L8r3q4&6N6i4u0D9i4K6y4r3j5i4k6A6k6q4)9K6c8q4!0q4x3W2)9^5x3q4!0m8y4R3`.`.
注意这个和上面那个用途不一样
c39K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6x3g2)9J5k6h3S2V1M7$3I4T1i4K6u0W2j5$3!0E0i4K6u0r3j5X3k6K6i4K6u0r3M7%4c8S2N6r3W2U0i4K6u0r3M7r3I4S2P5h3g2J5i4K6u0r3L8h3q4A6L8W2)9J5c8Y4N6A6k6r3N6W2N6s2y4Q4x3V1k6F1M7r3c8Q4x3X3g2V1M7X3#2Q4y4h3k6K6k6r3E0Q4x3X3f1%4k6o6S2W2x3h3f1#2k6W2)9J5k6h3A6K6
CKC 解密模块
445K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6X3y4Q4x3X3c8V1M7X3#2Q4x3X3g2T1K9h3I4A6N6X3W2V1k6h3!0Q4x3X3g2U0L8$3#2Q4x3V1k6U0k6i4u0Q4x3V1k6T1K9h3I4A6k6s2u0E0i4K6g2X3M7s2g2T1i4K6u0W2K9$3g2&6
DRM 公钥文件
905K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6X3y4Q4x3X3c8V1M7X3#2Q4x3X3g2T1K9h3I4A6N6X3W2V1k6h3!0Q4x3X3g2U0L8$3#2Q4x3V1k6T1K9h3I4A6k6s2u0E0
CKC 数据
在 playurl 的请求里可以发现多出了一些奇怪的字段 drm_type widevine_pssh

加载请求中的 widevine_pssh 数据到每个音频 / 视频流数据上


该数据将作为 KeySystem 的 InitData 值
其中 KeySystem 可能是其中的一个(根据环境变化)

通过以下方式解码,注意该处会先调用所有可用的 KeySystem 进行 InitData(测试哪些可用)
这里可以把所有的 KeySystem 都断点

在请求结果处理函数中,我们注意到有一个 setMpdBody 的函数,很明显是用于设定 Mpd 元信息的函数

跟踪进去

attachExternal 调用 attachSourceProxy

在 attachSourceProxy 内,判断视频流的格式,然后第一次进入该函数会请求 bilibili_certificate.bin 也就是证书文件

获取到证书文件后 base64 作为 serverCertificate

然后 POST 请求 bili_widevine 接口得到一个奇怪的字符串(请求数据 TODO )

在请求完成回调处断点,我们会得到这样的东西

接着他会进入 updateKeySession 函数,该函数就是更新 kid 与 key 的地方
只不过传入以上数据它并不是合法的,所以我们会等到第二次调用这个函数,第二次调用才会传入正确值

抛出错误后他会再进行一次请求,不过这次请求的接口是 playurl ,请求函数见上
接着它就能获取到 kid (从 playurl 接口得到),注意对比两次的 playurl 请求会发现第一次请求是没有 bilidrm_uri 而只有 widevine_pssh 的,第二次则相反,因为第二次请求没有加 &drm_tech_type=2 的参数

该值是获取 bilidrm_uri 的最后一个 // 后的文本得出的,而该值从 得出

最后获取 key 的函数均在 getKeyDetail 内,比较简单,不做分析

最后我们能够得到
使用 clearkeys 解密即可
使用 bbdown 下载视频流和音频流(注意不要合并)
kid 和 key 解码 base64 (注意是 url safe )

3eeK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1k6h3&6@1L8K6c8Q4x3X3g2U0L8$3#2Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0K6i4K6u0r3k6r3q4K6K9q4)9J5c8X3g2F1j5%4u0&6M7s2c8A6L8$3&6Q4y4h3k6S2L8X3c8Q4y4h3k6V1M7X3#2Q4x3V1j5`.
d59K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6F1K9h3I4S2L8$3c8S2i4K6u0r3b7V1u0p5L8%4N6F1
| 接口/方法/事件 |
作用 |
| navigator.requestMediaKeySystemAccess() |
入口函数。询问浏览器是否支持特定的 DRM 系统(如 'com.widevine.alpha')和视频格式。这是所有操作的第一步。 |
| MediaKeySystemAccess.createMediaKeys() |
如果上一步成功,调用此方法来创建一个 MediaKeys 对象,这个对象就代表了 CDM 的一个实例。 |
| video.setMediaKeys() |
将创建好的 MediaKeys 对象与你的 |
| mediaKeys.createSession() |
创建一个 MediaKeySession 对象。一个会话代表一次完整的许可证获取流程。 |
| video.addEventListener('encrypted', ...) |
关键事件监听器。当 MSE 收到加密数据时, |
| session.generateRequest() |
在 'encrypted' 事件处理器中调用。它告诉 CDM:“根据我给你的初始化数据 (initData),请生成一个许可证请求(Challenge)。” |
| session.addEventListener('message', ...) |
关键事件监听器。当 CDM 成功生成许可证请求后,会触发这个事件。事件的 message 属性就是需要发送给许可证服务器的二进制数据。 |
| fetch() |
使用标准的 fetch API,将从 'message' 事件中获取的请求体 POST 到你的许可证服务器 URL。 |
| session.update() |
关键方法。当你从许可证服务器那里 fetch 到了许可证 (License) 之后,调用这个方法,将许可证交给 CDM。CDM 处理后,就拥有了真正的解密密钥。握手至此成功。 |
BBDown.exe https://www.bilibili.com/cheese/play/ep1302284 --skip-mux
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!