前言
参考文章:https://www.52pojie.cn/thread-609243-1-1.html
最近某酷很多的独播剧在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:间距,用下面的一张图片来解释:
stride指在内存中每行像素所占的空间,它的作用就是前面说的对齐,作用就是提升性能吧(和计算机中的内存对齐概念差不多),所以frameBuffer
实际上包含了很多无用的字节,直接写出到文件播放时花屏的。
需要注意的是stride
并不是图像的width
,一般情况下它和width
相等,但在widevine
中它是大于width
的。
根据上图,我们需要的数据就是Image
块部分的buffer
,在说写出Image
部分的buffer
之前,我们来看一下I420
图像的格式:
I420
使用3个面来存储数据的,在VideoFrame
对象中就是一个planes
数组。YUV420
的数据量为: wh+w/2h/2+w/2h/2 即为wh*3/2,即对应与上图中的YUV
,plane[0]存储Y
,plane1存储U
,plane2存储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
方法阻塞,这是我电脑渣的原因,所以如果电脑配置不够高的话还是选择录屏吧)。
期待你的回复!
地址动态获取:
以上代码测试在
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/ 网站的账号我有,如果您感兴趣的话,我邮件联系您