谷歌 Widevine DRM 破解 时间: 2020-03-24 23:08 分类: 修仙日记 ####前言 参考文章:[https://www.52pojie.cn/thread-609243-1-1.html][1] 最近某酷很多的独播剧在PC端的网页上都开启了谷歌的`widevine`DRM 保护,开了会员在不支持谷歌`widevine`DRM 的浏览器最多也只能观看`720P`的清晰度,网页调试分析了一番,发现目前从`JS`里是无法进行破解了。 简单说下`DRM`的机制: 1、最简单就是`ClearKey`了,从字面上就可以知道,秘钥是暴露清晰可见的,只要你够耐心,总是可以从`JS`里面分析出秘钥,然后进行`AES`解密。 2、`Fair Play`、`Widevine`等其他基于`CDM`解密模块的,这种简单点来说就是客户端或者浏览器将网页中的播放器重写了,标准的`H5`播放器是直接将视频流传入`MediaSource`,显然这是不安全的,即使别人不会从`JS`中找出你的秘钥,但只要拦截`appendBuffer`函数就能够将你解密后的流捕捉。所以`Fair Play`、`Widevine`等这种加密形式就出现了,也就是`JS`中`appendBuffer`也是直接将加密的数据流传进去,然后将`Media`与`DRM`解密实例绑定进行一系列的会话操作,从`DRM license`服务器获取秘钥(获取到的秘钥也是加密的,只有CDM解密模块能够解析),然后对音视频流进行解密,解密后将结果渲染到浏览器。 ####分析 从上面的流程可知,在`JS`中是无法对其进行破解了,也就是说`JS`里面没有解密操作。 那么我们要么就是破解出`DRM`加密的秘钥(这个难度比较大,虽说国外早有报导有人通过白盒DFA攻击能够进行秘钥还原),但对于普通人来说简直是天方夜谭。 所以,退而求其次,还是以`拦截`的形式进行破解,其实前面基于`JS`加密里也说过了,拦截`appendBuffer`。对于`widevine`呢,由于`Chromium`是开源的,所以我们能够在它的源码里发现,有这么个头文件:`content_decryption_module.h` 这就是解密模块一些对外开放的接口定义,虽说个人编译的浏览器是无法使用`widevine`DRM的,但它却能够为我提供拦截的线索。 整个流程在前面的参考文章中说的已经比较清楚了,这里不想赘述,主要讲下自己实现的时候遇到的一些问题,也是参考文章中没有提及的问题。 ``` virtual Status Decrypt(const InputBuffer& encrypted_buffer, DecryptedBlock* decrypted_buffer) = 0; ``` 我们真的是仅仅只要拦截此方法的`decrypted_buffer`吗?答案是否定的,经过我的多方验证,无论是同时包含了音视频流还是只包含视频流,该方法都不是用来解密视频流的,它的作用是:若有音频流,则用来解密音频,并且不包含未加密的音频流部分(有些m3u8里面前面开头几段是未加密的,比如某酷)。 那么视频流该如何捕获? ``` virtual Status DecryptAndDecodeFrame(const InputBuffer& encrypted_buffer, VideoFrame* video_frame) = 0; ``` 没错,就是它,函数名称显而易见,与其类似的还有个`DecryptAndDecodeSamples`,官方注释说是用来解密与解码音频流的,但是测试时音频流一直都是`Decrypt`方法进行解密。 `DecryptAndDecodeFrame`与`Decrypt`方法的不同看参数就能够知道了,`Decrypt`返回的是`DecryptedBlock`解密后的数据块,注意:此时的数据,是没有解码的。其实我们要的就是解密后的这种数据,但是可惜的是`Decrypt`并不解密视频。 `DecryptAndDecodeFrame`这个方法就很坑了,返回的是解码后的视频帧,实践测试的时候`H264`视频返回的解码格式是`I420`裸流。直接写出到文件?写个锤子,一部几百兆的视频,能给你输出几十G的`yuv`数据,并且直接写出保存为`.yuv`文件还是播放花屏的。 到此为止,已经触碰到自己的盲区了,无奈,只好硬着头皮去学习图像的一些知识,这些东西在这里我就不写出来了。 `VideoFrame`对象包含了几个重要的信息:frameBuffer、planeOffset、stride。 frameBuffer:视频帧的数据(对齐后的数据,而不是.yuv文件里的那种能够直接写出文件播放的) planeOffset:YUV420P平面格式,在这里I420即是由3个`plane`来存储图像的,可以通过该方法获取每个`plane`在`frameBuffer`中的相对位置。 timestamp:时间戳,实践时我抓取的是按40000递增,其实有一个坑是需要这个值来进行解决的,最后再说这个坑。 stride:间距,用下面的一张图片来解释: ![0_1321278834jZXb.gif][2] stride指在内存中每行像素所占的空间,它的作用就是前面说的对齐,作用就是提升性能吧(和计算机中的内存对齐概念差不多),所以`frameBuffer`实际上包含了很多无用的字节,直接写出到文件播放时花屏的。 需要注意的是`stride`并不是图像的`width`,一般情况下它和`width`相等,但在`widevine`中它是大于`width`的。 根据上图,我们需要的数据就是`Image`块部分的`buffer`,在说写出`Image`部分的`buffer`之前,我们来看一下`I420`图像的格式: ![20181109115016207.png][3] `I420`使用3个面来存储数据的,在`VideoFrame`对象中就是一个`planes`数组。 `YUV420`的数据量为: w*h+w/2*h/2+w/2*h/2 即为w*h*3/2,即对应与上图中的`YUV`,plane[0]存储`Y`,plane[1]存储`U`,plane[2]存储`V`。 下面来说下如何将`frameBuffer`中的有效数据写出到文件,我们要做的就是将三个面的数据按照上面的格式取出然后写出到文件,`Y`的数据,即`width*height`,但是`widevine`中`stride`与`width`不相等,我们无法直接从`video_frame->PlaneOffset(video_frame->kYPlane)`的的位置按顺序取出`width*height`个字节写入文件,这样子会把前面图片中`padding`的无用数据也写出导致花屏。 解决办法是,按行读取数据,每次读取`width`个字节,后面跳过`stride - width`个字节,同理`U`、`V`实现如下: ``` uint32_t c = video_frame->Size().width * video_frame->Size().height; if (buffer == NULL) buffer = (unsigned char*)malloc(video_frame->Size().width * video_frame->Size().height * 1.5); //Y Plane uint32_t offset = 0; for (int i = 0; i < video_frame->Size().height; i++) { memcpy(buffer + video_frame->Size().width * i, video_frame->FrameBuffer()->Data() + offset, video_frame->Size().width); offset += video_frame->Stride(video_frame->kYPlane); } //U Plane offset = 0; for (int i = 0; i < video_frame->Size().height / 2; i++) { memcpy(buffer + c + (video_frame->Size().width / 2) * i, video_frame->FrameBuffer()->Data() + video_frame->PlaneOffset(video_frame->kUPlane) + offset, video_frame->Size().width / 2); offset += video_frame->Stride(video_frame->kUPlane); } //V Plane offset = 0; for (int i = 0; i < video_frame->Size().height / 2; i++) { memcpy(buffer + c + (c / 4) + (video_frame->Size().width / 2) * i, video_frame->FrameBuffer()->Data() + video_frame->PlaneOffset(video_frame->kVPlane) + offset, video_frame->Size().width / 2); offset += video_frame->Stride(video_frame->kVPlane); } fwrite(buffer, 1, video_frame->Size().width * video_frame->Size().height * 1.5, pVideo); ``` 关于`stride`,在我搜索资料时,一直有个千篇一律的文章: `u_stride = width / 2`,`v_stride = width / 2`,实则不然,`widevine`中`Y`、`U`、`V`的`stride`都是相同的。 以上就是对`I420`buffer的处理,写出的`yuv`数据是可以直接进行播放的,但是如果全部写出最后再进行编码成H264的话会产生一个很大的`yuv`文件,所以需要改进的就是按帧进行编码成`H264`格式再写出到文件。 由于还未去实现最后的这一步,代码就不贴出来了。 前面说的`timestamp`,有个坑就是:如果视频播放卡了,我们在`DecryptAndDecodeFrame`中将会收到重复的视频帧。由于有`timestamp`,所以我们可以记录最后一次写出视频帧的`timestamp`,虽说这个值对于编码成H264没什么作用,由于它是递增的,我们只需做如下判断即可: lastTimestamp 初始化为 -1; lastTimestamp == -1 || lastTimestamp > video_frame->Timestamp() 则表示是最新的视频帧而不是重复的。 以上都是针对老版本的`Chrome`,用的是`IAT`修改导入表的函数地址来实现拦截的,新版本的貌似没了`widevinecdmadapter.dll`文件,但`widevinecdm.dll`还是有的,`CreateCdmInstance`导出函数也还在,我们只需将`IAT`改成`EAT`应该还是照样可以进行拦截的。 完~~~~ 2020.7.7补充: -------- 实践测试某酷发现`yuv`编码回`h264`经常会出现视频时长发生变化导致音视频不同步,视频帧率为`25`应该是没有错的,最有可能导致这个问题的原因就是进程在调用`ffmpeg`编码时导致网页播放卡顿,卡顿导致画面图像丢帧或者重复帧,所以最终出现`yuv`编码回`h264`视频时长发生变化。 至于先将`yuv`保存到本地,使用快进功能快速将视频的`yuv`dump下来,然后再进行编码成`h264`,算了下一集40多分钟的1080P视频大概会产生`100多G`的`yuv`文件,计算方法:以分辨率1920:1080为例,帧率为25时长为40分钟的视频,共有`40*60*25`帧图像,每帧图像大小为`1920*1080*1.5`,所以`yuv`总大小为`1920*1080*1.5*40*60*25/1024/1024/1024`约等于`173G`,由于我的电脑没有空闲磁盘所以就没有测试了,关键还是电脑配置太渣了,网页边播放边编码只能按照正常播放速度进行勉强不会卡顿。 `h264`编码视频调用`DecryptAndDecodeFrame`解密并解码,`vp9`视频编码貌似是调用的`Decrypt`解密,因此`vp9`编码的视频应该可以直接`dump`下来无需编码回`h264`这一耗时操作,具体我没去验证了,有兴趣的朋友可以去验证下。 最后更新: ---- 都洗洗睡吧,查看了`widevine`的设计文档,视频流解密只会调用`DecryptAndDecodeFrame`来进行解密,也就是解密与解码是同时进行的,设计初衷就是不让原始视频流暴露在内存中。所以尽管你在`DecryptAndDecodeFrame`截获了`yuv`视频帧,然后再编码回去,也不是原始的视频流了,这样一来与录屏无任何差别,相当于进行了二次转码。 而二次`yuv`到`mp4`的转码操作,不仅耗时,而且经过我多方测试,同一视频调用`DecryptAndDecodeFrame`解码出来的`yuv`总帧数经常出现变动,这样一来如果总帧数不正确,最终导致的结果就是视频总时长要么偏短要么偏长,也就无法与音频进行同步(编码本来就是耗时操作,最终少帧或者多帧视频时长不对相当于白压制了,非常蛋疼),测试时已经根据`timestamp`来进行重复帧的校正了,但最终不知道什么原因还是会导致前面视频时长不对的问题(测试时低分辨率的视频时长没出现这种问题,1080p的经常出现,应该是后台编码时网页视频卡顿导致的,也就是说在处理的时候`ffmpeg`编码的速度跟不上`yuv`视频帧的产生速度,使得`DecryptAndDecodeFrame`方法阻塞,这是我电脑渣的原因,所以如果电脑配置不够高的话还是选择录屏吧)。 [1]: https://www.52pojie.cn/thread-609243-1-1.html [2]: https://0o0.me/usr/uploads/2020/03/2095606326.gif [3]: https://0o0.me/usr/uploads/2020/03/2806547134.png 标签: 无
期待你的回复!
地址动态获取:
以上代码测试在
win7
32位上通过,win10
无效。[secret]大神能透露下是在哪个版本的chromium上做的吗[/secret]
大佬请问有小白能用的方法吗? 有偿也行呀!
没有,实践测试只做到简单的拦截视频流写出
.yuv
文件播放,若想做成工具也比较麻烦,需要添加拦截开关(即何时开始拦截,何时结束拦截),以及多个网页视频同时拦截,.yuv
重新编码回H264
,实现比较复杂。在头文件前面还有InitializeVideoDecoder这个虚函数的定义,博主有尝试过hook让它返回kInitializationError之后能否用Decrypt方法来解密视频吗?
试过直接解密报错。
很有帮助
[secret]你好,可以付费定制工具吗,方便的话,可以加您QQ吗[/secret]
没有工具,无法全自动,目前已实现
yuv420p
编码回h264
,按播放列表自动dump
视频文件保存到本地,音频暂未处理(音频处理比较麻烦,比如某酷视频开头部分音频未加密是不会走Decrypt
方法的,需要dump
Decrypt
解密后的音频然后合并自己手动下载未加密的部分,最终再与h264
视频合并),测试与录屏差不多,使用ffmpeg
编码yuv
到h264
比较耗时,可能我电脑的原因编码比较慢,无法使用视频快进加快编码速度,理论上电脑配置足够好的话可以使用快进功能加快视频编码,这样一来相比录屏速度就会加快,编码参数可控,视频大小与编码参数设置有关。您好,不知道您有兴趣看一下这个网站的widevine吗?可以付费 。https://movie.trueid.net/ 网站的账号我有,如果您感兴趣的话,我邮件联系您