admin

谷歌 Widevine DRM 破解
前言参考文章:https://www.52pojie.cn/thread-609243-1-1.html最近某酷很...
扫描右侧二维码阅读全文
24
2020/03

谷歌 Widevine DRM 破解

前言

参考文章:https://www.52pojie.cn/thread-609243-1-1.html

最近某酷很多的独播剧在PC端的网页上都开启了谷歌的widevineDRM 保护,开了会员在不支持谷歌widevineDRM 的浏览器最多也只能观看720P的清晰度,网页调试分析了一番,发现目前从JS里是无法进行破解了。

简单说下DRM的机制:
1、最简单就是ClearKey了,从字面上就可以知道,秘钥是暴露清晰可见的,只要你够耐心,总是可以从JS里面分析出秘钥,然后进行AES解密。
2、Fair PlayWidevine等其他基于CDM解密模块的,这种简单点来说就是客户端或者浏览器将网页中的播放器重写了,标准的H5播放器是直接将视频流传入MediaSource,显然这是不安全的,即使别人不会从JS中找出你的秘钥,但只要拦截appendBuffer函数就能够将你解密后的流捕捉。所以Fair PlayWidevine等这种加密形式就出现了,也就是JSappendBuffer也是直接将加密的数据流传进去,然后将MediaDRM解密实例绑定进行一系列的会话操作,从DRM license服务器获取秘钥(获取到的秘钥也是加密的,只有CDM解密模块能够解析),然后对音视频流进行解密,解密后将结果渲染到浏览器。

分析

从上面的流程可知,在JS中是无法对其进行破解了,也就是说JS里面没有解密操作。
那么我们要么就是破解出DRM加密的秘钥(这个难度比较大,虽说国外早有报导有人通过白盒DFA攻击能够进行秘钥还原),但对于普通人来说简直是天方夜谭。
所以,退而求其次,还是以拦截的形式进行破解,其实前面基于JS加密里也说过了,拦截appendBuffer。对于widevine呢,由于Chromium是开源的,所以我们能够在它的源码里发现,有这么个头文件:content_decryption_module.h
这就是解密模块一些对外开放的接口定义,虽说个人编译的浏览器是无法使用widevineDRM的,但它却能够为我提供拦截的线索。

整个流程在前面的参考文章中说的已经比较清楚了,这里不想赘述,主要讲下自己实现的时候遇到的一些问题,也是参考文章中没有提及的问题。

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方法进行解密。

DecryptAndDecodeFrameDecrypt方法的不同看参数就能够知道了,Decrypt返回的是DecryptedBlock解密后的数据块,注意:此时的数据,是没有解码的。其实我们要的就是解密后的这种数据,但是可惜的是Decrypt并不解密视频。

DecryptAndDecodeFrame这个方法就很坑了,返回的是解码后的视频帧,实践测试的时候H264视频返回的解码格式是I420裸流。直接写出到文件?写个锤子,一部几百兆的视频,能给你输出几十G的yuv数据,并且直接写出保存为.yuv文件还是播放花屏的。

到此为止,已经触碰到自己的盲区了,无奈,只好硬着头皮去学习图像的一些知识,这些东西在这里我就不写出来了。
VideoFrame对象包含了几个重要的信息:frameBuffer、planeOffset、stride。
frameBuffer:视频帧的数据(对齐后的数据,而不是.yuv文件里的那种能够直接写出文件播放的)
planeOffset:YUV420P平面格式,在这里I420即是由3个plane来存储图像的,可以通过该方法获取每个planeframeBuffer中的相对位置。
timestamp:时间戳,实践时我抓取的是按40000递增,其实有一个坑是需要这个值来进行解决的,最后再说这个坑。
stride:间距,用下面的一张图片来解释:
0_1321278834jZXb.gif

stride指在内存中每行像素所占的空间,它的作用就是前面说的对齐,作用就是提升性能吧(和计算机中的内存对齐概念差不多),所以frameBuffer实际上包含了很多无用的字节,直接写出到文件播放时花屏的。

需要注意的是stride并不是图像的width,一般情况下它和width相等,但在widevine中它是大于width的。
根据上图,我们需要的数据就是Image块部分的buffer,在说写出Image部分的buffer之前,我们来看一下I420图像的格式:
20181109115016207.png

I420使用3个面来存储数据的,在VideoFrame对象中就是一个planes数组。
YUV420的数据量为: wh+w/2h/2+w/2h/2 即为wh*3/2,即对应与上图中的YUV,plane[0]存储Y,plane[1]存储U,plane[2]存储V

下面来说下如何将frameBuffer中的有效数据写出到文件,我们要做的就是将三个面的数据按照上面的格式取出然后写出到文件,Y的数据,即width*height,但是widevinestridewidth不相等,我们无法直接从video_frame->PlaneOffset(video_frame->kYPlane)的的位置按顺序取出width*height个字节写入文件,这样子会把前面图片中padding的无用数据也写出导致花屏。

解决办法是,按行读取数据,每次读取width个字节,后面跳过stride - width个字节,同理UV实现如下:

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 / 2v_stride = width / 2,实则不然,widevineYUVstride都是相同的。
以上就是对I420buffer的处理,写出的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应该还是照样可以进行拦截的。

完~~~~

Last modification:March 24th, 2020 at 11:19 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment