我不理解,为什么像Netflix、Spotify流媒体服务提供商,包括苹果、谷歌等公司,会使用数字版权管理。他们在我买的设备上,浪费我的硬件算力,加密我买的数字内容,以阻止我下载。君子也防?[doge]
本文尝试欺骗DRM License Server获取秘钥,来解密被DRM加密的多媒体内容。
了解Google Widevine
通过阅读文章大致了解了widevine的工作原理,以及DRM(Digital Rights Management,数字版权管理)系统的执行过程:
获取CDM Device验证文件
CDM(内容解密模块)是嵌入在Chrome浏览器或Android操作系统中用来加解密流媒体的闭源模块。因此,我们可以模拟一个安卓设备。
使用pywidevine创建CDM Device。创建设备需要两个关键文件,client_id.bin和private_key.pem。
-
按照帖子Dumping Your own L3 CDM with Android Studio的操作,下载并安装Android Studio,再使用
frida-server获取关键文件。 -
在Android Studio的Device Manager中新建设备,选一台更帖子描述一样的设备,
Pixel 6、Pie,生成了Pixel 6 API 28设备,启动后显示emulator-5554。(我的Andriod Studio装好时,Device Manager中已经有了一部虚拟手机Pixel_3A_API_34_extension_level_7_x86_64(注意这是一台x86_64的设备),这个设备浏览器解析CDM时貌似有问题) -
在Windows下的
conda中新开一个环境安装pip install frida frida-tools,并确定frida版本(我使用了16.2.1),frida与frida-server版本需一致。 -
将下载的
frida-server-16.2.1-android-x86放到C:\Users\<YourUserName>\AppData\Local\Android\Sdk\platform-tools\目录中,命令行进入该目录,执行:
> .\adb.exe devicesList of devices attachedemulator-5554 device可以看到虚拟设备已被识别。
- 将
frida-server上传到虚拟设备:
> .\adb.exe push .\frida-server-16.2.1-android-x86 /sdcard.\frida-server-16.2.1-android-x86: 1 file pushed, 0 skipped. 152.8 MB/s (51807740 bytes in 0.323s)- 连接虚拟设备,将
frida-server放在正确的位置,赋予执行权限并启动:
> .\adb.exe shellsumv /sdcard/frida-server-16.2.1-android-x86 /data/local/tmp/chmod +x /data/local/tmp/frida-server-16.2.1-android-x86/data/local/tmp/frida-server-16.2.1-android-x86此时server启动,保持终端运行。
- 下载dumper,进入目录并安装依赖
pip install -r requirements.txt。
运行时报错提示:
python .\dump_keys.pyTraceback (most recent call last): File "C:\Users\qinzi\pyws\wdevof\dumper\dump_keys.py", line 6, in <module> from Helpers.Scanner import Scan File "C:\Users\qinzi\pyws\wdevof\dumper\Helpers\Scanner.py", line 7, in <module> from Helpers.wv_proto2_pb2 import SignedLicenseRequest File "C:\Users\qinzi\pyws\wdevof\dumper\Helpers\wv_proto2_pb2.py", line 33, in <module> _descriptor.EnumValueDescriptor( File "C:\Users\qinzi\anaconda3\envs\wdev\Lib\site-packages\google\protobuf\descriptor.py", line 789, in __new__ _message.Message._CheckCalledFromGeneratedFile()TypeError: Descriptors cannot be created directly.If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc >= 3.19.0.If you cannot immediately regenerate your protos, some other possible workarounds are: 1. Downgrade the protobuf package to 3.20.x or lower. 2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will use pure-Python parsing and will be much slower).因此安装建议为PowerShell添加环境变量,再次运行python .\dump_keys.py即可:
> $env:PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION = 'python'> $env:PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATIONpython
> python .\dump_keys.py2024-03-02 12:22:36 PM - root - 16 - INFO - Connected to Android Emulator 55542024-03-02 12:22:36 PM - root - 17 - INFO - scanning all processes for the following libraries...2024-03-02 02:17:06 PM - Helpers.Scanner - 82 - INFO - Running libwvhidl.so at 0xec7170002024-03-02 02:17:06 PM - Helpers.Scanner - 75 - DEBUG - { "from": "Dynamic Function", "message": "L3 RSA Key export function found: cwkfcplc"}...2024-03-02 12:22:37 PM - root - 25 - INFO - Hooks completed此时dumper启动,保持终端运行。
创建CDM设备
此时可以使用client_id.bin和private_key.pem,通过pywidevine命令行工具来生成wvd设备文件了:
pywidevine create-device -k ./private_key.pem -c ./client_id.bin -t "ANDROID" -l 3得到一个名为google_aosp_on_ia_emulator_14.0.0_67016d30_4464_l3.wvd的设备文件。
获取秘钥
使用pywidevine的例程即可获取秘钥:
pssh在希望下载的视频描述文件中,可能是浏览器请求的一个https://a.com/b/c.mpdxml文件或是一个https://a.com/b/c.m3u8list文件。device的路径即为刚才创建的设备文件路径。licence请求的地址,是浏览器加载视频时发出的一次或两次(aduio和video各一次)秘钥请求,URL特征应当非常明显,通常为POST方式,带有二进制负载。
另外需要注意的是,CDM秘钥的请求原理上是独立于媒体提供平台的,或是与媒体服务API不同的一套API,因此要使得licence请求成功,可能还需要从浏览器中复制秘钥请求headers一并加入requests.post(如使用浏览器复制为cURL (bash))。
from pywidevine.cdm import Cdmfrom pywidevine.device import Devicefrom pywidevine.pssh import PSSH
import requests
# prepare psshpssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa" "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
# load devicedevice = Device.load("C:/Path/To/A/Provision.wvd")
# load cdmcdm = Cdm.from_device(device)
# open cdm sessionsession_id = cdm.open()
# get license challengechallenge = cdm.get_license_challenge(session_id, pssh)
# send license challenge (assuming a generic license server SDK with no API front)licence = requests.post("https://...", data=challenge)licence.raise_for_status()
# parse license challengecdm.parse_license(session_id, licence.content)
# print keysfor key in cdm.get_keys(session_id): print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
# close session, disposes of session datacdm.close(session_id)请求成功后会打印SIGNING和CONTENT,这个CONTENT就是我们需要用来解密音视频的秘钥对kid:key,我的请求输出大致格式为:
[SIGNING] <32bit-hex>:<128bit-hex>[CONTENT] <32bit-hex-kid>:<32bit-hex-key>解密多媒体文件
解密工具有现成的,比如mp4decrypt、shaka,我用的是mp4decrypt,使用刚才拿到的`kid
mp4decrypt --key <kid>:<key> my_encrypted_file.mp4 my_decrypted_file.mp4也可以直接使用支持DRM解密的下载工具,如使用rust编写的dash-mpd-cli,只需要在参数中指定解密工具和秘钥即可:
dash-mpd-cli https://url_of_.mpd_file.mpd --output my_decrypted_file.mp4 -H cURL_format_headers -H ... --decryption-application mp4decrypt \ --mp4decrypt-location /home/wsl/devof/Bento4/cmakebuild/mp4decrypt \ --key d2afe67c226cf2e932ac9b4c0c13432d:c5cf130ddf1087ca2f134725e82f1c1e