jimmyzang 发表于 2020-4-22 17:00

HTML5视频解密的方法(widevine的破解思路)

本帖最后由 jimmyzang 于 2020-4-30 07:17 编辑

首先这是继续上一篇的m3u8通用下载, https://www.52pojie.cn/thread-1161169-1-1.html这篇文章之所以没有和上一篇合并,原因主要是上篇介绍的是一个通用方法,这篇主要是针对于widevine的加密提供一个crack思路。

这篇文章比上一篇讨论的更深入了一些。上一篇讨论的是js调用的appendbuff 方法,这篇讨论的是widevine加密部分。

在正文开始之前,还是强调一下,请不要问我要成型的软件工具,无论如何都不提供,这里仅仅提供技术讨论,不提供任何工具。谢谢。

如果您在实践中遇到什么技术难题,可以回帖讨论。我会尽我所能的提供帮助。

感谢@逍遥一仙给在第一篇帖子里提了一个好问题,引导我去考虑widevine的解密。整个思路在实现过程得到了他的支持,思想碰撞才能有火花,在此表示感谢。


前言:
经常有人问为啥研究这个,不是发现m3u8后随便找个下载器就可以下载了么?

先大概介绍一下情况:
1:市面上大部分采用的都是HLS加密,通过下载m3u8文件,就可以下载绝大多数,最多就是m3u8里面隐藏一下key,换换偏移之类,这些都是js层的捉迷藏游戏,提供给浏览器底层的其实还是解密后的数据。

没有采用加密方式的,基本上随便找个m3u8下载器就可以下载。 如果有一些偏移或者aes加密的,可以用某些下载器下载,其实是下载器的作者分析过这些代码逻辑。下载后帮大家解密过了,但是如果换一个网站就不一定可以适用了。

所以我上篇文章提到过的通过编译chrome,改写appendbuff方法,从接口拦截数据,基本上可以扫清市面上能见到的js层的加密,而不用去和js捉迷藏(其实主要是我不懂js,所以才去搞了这个方法。呵呵。。。)

2:还有一些网站,比如某酷,某异 有些电影启用了google提供的widevine加密方式,这个加密方式笼统的说是解码库和解密库集成在了一起,不在js层,是在底层解密,一般的m3u8下载器都会失效。
(并非所有视频都采用widewine加密的,这些网站上还有一些视频时没有widevine加密的。一般国外电影或者注重版权的会有widevine加密)。


长话短说,先找到我们的目标网站。为了不引起版权纠纷,这里采用了某个 https://shaka-player-demo.appspot.com/ 这个被网上广泛公开用来测试widevine是否可用的网站。
这网站打开后有五个视频,中间一个A Blender Foundation short film, protected by Widevine encryption. 就是用widevine加密,我们这次就是针对这个视频的破解。

让您刚刚编译的浏览器支持widevine,这一步真是让人头疼,网上没有任何文档对于windows编译的描述,我之前一直以为需要响应的sdk,这个sdk需要和google签订协议,而且要经过一次考试。。。基本上个人不可能拿到,当时就卡在这里。

后来翻遍全网连猜带蒙才发现,支持widevine在自己编译的chromium上只需要两步:

2.1 打开编译开关enable_widevine = true
2.2 在这个地址下载google的widevine库,https://dl.google.com/widevine-cdm/4.10.1582.2-win-x64.zip,在编译好的chrome目录新建一个目录,WidevineCdm,把解压缩的东西放进去,就可以成功。检查log可以发现如下字样就是成功了:

Preinstalled component found for Widevine Content Decryption Module at widevine\WidevineCdm with version 4.10.1582.2.
FinishRegistration for Widevine Content Decryption Module
Component ready, version 4.10.1582.2 in E:\chrome_dev\depot_tools\chromium\src\out\widevine\WidevineCdm
Component ready, version 4.10.1582.2 in E:\chrome_dev\depot_tools\chromium\src\out\widevine\WidevineCdm
Register Widevine CDM with Chrome
Register Widevine CDM with Chrome

顺便说一句:
网上只看到过linux的下载地址,这个win的下载地址从来没有看到过,也许有地方记载,反正我没找到,我是根据命名规则猜出来的。
有人在linux下用chrome的自带dll也可以成功,也有人失败,我机器上不行,可能是因为缺少sig文件?反正用google的这个文件没问题。


下面解释一下widevine的解码逻辑:

万事俱备,只欠东风,用编译好的chrome打开上面提到的网站,视频应该是可以播放状态,play按钮可以点击了。播放视频,跟踪log解释一下逻辑

void DecoderStream<StreamType>::OnBufferReady//读取buff,这里buff已经把audio video分流了。video解码一路,audio解码一路

对应log: decoder_stream.cc(680)] OnBufferReady<video>: 0, timestamp=0 duration=42000 size=169 side_data_size=0 is_key_frame=1 encrypted=0 discard_padding (us)=(0, 0)


void DecoderStream<StreamType>::Decode(scoped_refptr<DecoderBuffer> buffer)         //利用template,不管是audio,video继续走decode方法
对应log:decoder_stream.cc(434)] Decode<video>


void DecoderStream<StreamType>::DecodeInternal//继续调用到decodeinternal
对应log:decoder_stream.cc(459)] DecodeInternal<video>



void DecryptingVideoDecoder::Decode(                //检查当前线程,调用这个类的DecodePendingBuffer方法。
对应log:decrypting_video_decoder.cc(95)] Decode()


void MojoDecryptor::DecryptAndDecodeVideo(            //这里开始解码了。remote_decryptor_应该就是widevine的解码方法,直接出来的videoframe就是视频图片。
    scoped_refptr<DecoderBuffer> encrypted,
    const VideoDecodeCB& video_decode_cb) {
DVLOG(3) << __func__ << ": " << encrypted->AsHumanReadableString();
DCHECK(thread_checker_.CalledOnValidThread());

mojom::DecoderBufferPtr mojo_buffer =
      video_buffer_writer_->WriteDecoderBuffer(std::move(encrypted));
if (!mojo_buffer) {
    video_decode_cb.Run(kError, nullptr);
    return;
}

remote_decryptor_->DecryptAndDecodeVideo(
      std::move(mojo_buffer),
      base::BindOnce(&MojoDecryptor::OnVideoDecoded, weak_factory_.GetWeakPtr(),
                     mojo::WrapCallbackWithDefaultInvokeIfNotRun(
                         ToOnceCallback(video_decode_cb), kError, nullptr)));
}

对应log:mojo_decryptor.cc(180)] DecryptAndDecodeVideo: timestamp=0 duration=42000 size=169 side_data_size=0 is_key_frame=1 encrypted=0 discard_padding (us)=(0, 0)


所以最后调到了MojoDecryptor::OnVideoDecoded。video_frame就是解密+解码后的数据。
这个函数里面的void MojoDecryptor::OnVideoDecoded(
    VideoDecodeOnceCB video_decode_cb,
    Status status,
    const scoped_refptr<VideoFrame>& video_frame,
    mojo::PendingRemote<mojom::FrameResourceReleaser> releaser) {


检验是否正确也很简单我们再log里面把这个frame转换为png输出即可,具体代码如下:我把这个frame转换成png图片输出到log,看看是不是解码后的图片。

灰色字体是原有代码,蓝色为添加代码。主要逻辑,用libyuv转换I420到ARGB,在转换为PNG输出到log,通过查看png数据证明当前图形是解码过的实际图形而非乱码不可见。

DCHECK(thread_checker_.CalledOnValidThread());

//added by jimmyzang
DVLOG(1) << "this is the jimmyzang output : " << video_frame->AsHumanReadableString();


               gfx::Rect visible_rectrc = video_frame->visible_rect();
               auto argb_out_frame = VideoFrame::CreateFrame(
                         VideoPixelFormat::PIXEL_FORMAT_ARGB, visible_rectrc.size(), visible_rectrc, visible_rectrc.size(),
                         base::TimeDelta());
                  const int width = visible_rectrc .width();
                  const int height = visible_rectrc .height();
                  uint8_t* const dst_argb = argb_out_frame.get()->data(VideoFrame::kARGBPlane);
                  const int dst_stride = argb_out_frame.get()->stride(VideoFrame::kARGBPlane);
                  libyuv::J420ToARGB(video_frame->data(VideoFrame::kYPlane),
                              video_frame->stride(VideoFrame::kYPlane),
                              video_frame->data(VideoFrame::kVPlane),
                              video_frame->stride(VideoFrame::kVPlane),
                              video_frame->data(VideoFrame::kUPlane),
                              video_frame->stride(VideoFrame::kUPlane),
                              dst_argb, dst_stride, width, height);

      // Convert the ARGB frame to PNG.
      std::vector<uint8_t> png_output;
      gfx::PNGCodec::Encode(
                argb_out_frame->data(VideoFrame::kARGBPlane), gfx::PNGCodec::FORMAT_BGRA,
                argb_out_frame->visible_rect().size(),
                argb_out_frame->stride(VideoFrame::kARGBPlane),
                true, /* discard_transparency */
                std::vector<gfx::PNGCodec::Comment>(), &png_output);

      const int size = base::checked_cast<int>(png_output.size());


unsigned char * memory = png_output.data();
const char* pszNibbleToHex = {"0123456789ABCDEF"};
size_t nNibble;
int i;
int infoLength = size;
std::string text = "";


if (infoLength > 0) {
                for (i = 0; i < infoLength; i++) {
                  nNibble = memory >> 4;
                              text.append(&pszNibbleToHex,1);
                  nNibble = memory & 0x0F;
                              text.append(&pszNibbleToHex,1);
                }
      }
      


         DVLOG(1) << "this is find picture: " << text;

//end by jimmyzang

// If using shared memory, ensure that |releaser| is closed when
// |frame| is destroyed.
if (video_frame && releaser) {

注意,这里用到了libyuv这个转化库,chromium\src\media\mojo\clients\BUILD.gn中需要加上这个的引用。

然后从log中导出对应的png文件,采用ffmpeg转换生成avi看看是不是正确。就是大家看到的附件中的那个视频了。


已知问题:
1:我代码有问题,内存没释放,反正理论验证就不改了。
2:应该不用直接写png文件经过转码后再直接输出,生成视频文件,这样就占用内存小,而且灵活。
3:audio decrpt没有做,道理是一样。
4:x酷和x异 html播放器会提示浏览器不支持,我查看了他们js里面代码,应该是html5里面判断过,绕过即可,反正浏览器源码就在那里,我不懂js,具体那句我不知道。。。。汗。留给大家发挥吧。

总的来说,chromium浏览器编译进行html5视频解密告一段落了,基本上没啥挑战了,收工。。。

Widevine破解后,原帖子中写的是输出成PNG文件,今天突然想到chromium里面有一个方法是mojovideoencode,可以用这个方法编码输出成H264,
伪码贴一下,做个备忘。

auto mock_vea_client = std::make_unique< MojoVideoEncodeAccelerator>();
      Initialize(mock_vea_client.get());
base::UnsafeSharedMemoryRegion shmem =
    base::UnsafeSharedMemoryRegion::Create(
      VideoFrame::AllocationSize(PIXEL_FORMAT_I420, kInputVisibleSize) *
      2);
base::WritableSharedMemoryMapping mapping = shmem.Map();
const scoped_refptr<VideoFrame> video_frame = VideoFrame::WrapExternalData(
    PIXEL_FORMAT_I420, kInputVisibleSize, gfx::Rect(kInputVisibleSize),
    kInputVisibleSize, mapping.GetMemoryAsSpan<uint8_t>().data(),
    mapping.size(), base::TimeDelta());
video_frame->BackWithSharedMemory(&shmem);
const bool is_keyframe = true;

EXPECT_CALL(*mock_vea_client, BitstreamBufferReady(kBistreamBufferId, _))
    .WillOnce(testing::Invoke(
      (int32_t, const BitstreamBufferMetadata& metadata) {
          EXPECT_EQ(is_keyframe, metadata.key_frame);
      }));

mojo_vea()->Encode(video_frame, is_keyframe);

附件是解密过的视频文件的前10几秒,不是任何工具,或者源码,仅仅是证明可行性,如无必要请不必下载。






jimmyzang 发表于 2022-5-13 19:56

ZHANDOU 发表于 2022-5-12 18:10
大佬你好,萌新大学期间只学习了HTML和PHP的相关编写知识,有Python基础,感谢您提供的方法,但是萌新能力 ...

①上一篇文章里提到的chromium编译的目录貌似和我的Chrome的目录不太相同,我的目录没有chromium/src......这种目录地址

【这是我很早之前写的文章了,我现在本地没有代码,你如果下载了源码,可以直接安装文件名查找。也可以用chrome的web页面访问源码,比如,https://source.chromium.org/chromium/chromium/src/+/main:media/mojo/clients/mojo_audio_decoder.cc;l=1?q=mojo_audio_decoder.cc&sq=&ss=chromium(如何访问你懂的)】
②本篇文章从enable_widevine = true开始的代码输入是在哪里进行编写的?
【同样,可以搜索一下这句话,我印象中是在widevine的配置文件中,可能是这个
third_party/widevine/cdm/widevine.gni】

q522849100 发表于 2020-5-29 22:36

jimmyzang 发表于 2020-5-3 07:39
widevine解密和解码是在一个库里面的,我看上去是没有方法直接decrypt,但不decoded的。其实chrome里面有 ...

几个月前按照论坛某大佬的思路操作过一次:https://www.52pojie.cn/thread-609243-1-1.html    最终也是发现视频一直都只会使用DecryptAndDecodeFrame来进行解密与解码,无论是音视频是否分离的情况下,只有音频会调用Decrypt解密,与其类似还有个DecryptAndDecodeSamples方法根本就不会使用,我就很纳闷了。所以最终也是卡在yuv420i转h264上面了(测试时候发现一个四五百兆的某酷视频如果把yuv写到文件貌似至少10G以上,真够蛋疼的,由于本人主要编程语言是java,c++只是业余,所以编码视频懒得瞎折腾了,据我所知可以利用ffmpeg的管道来进行实时编码而不用将yuv写到文件之后再编码),总感觉这种操作还不如录屏来得方便。还有个需要注意的地方楼主是没有提到的,就是DecryptAndDecodeFrame可能会出现重复视频帧,有兴趣的朋友可以参看我当时做的一些小记录:https://0o0.me/java/crack-widevine-drm.html

popandpipi 发表于 2022-1-9 20:36

之前的大神解密chrome插件被关停了,现在再看看文章学习一下思路!

q522849100 发表于 2020-5-29 22:51

jixun66 发表于 2020-5-15 19:28
解密不等于解码… 我只能说这是我自己推测的原理,因为我没搞过 widevine,但其它方向的加解密应该都差不 ...

如果经过解码到屏幕这一过程后 dump,那和我搞个外置采集卡的画质没多大区别了 _(:3__对于最后这一点不怎么认同,网站扒视频不仅仅考虑画质,如何快速抓取也是非常重要的,录屏的缺点就是视频播放时间有多久就得录多久(不知道能不能快进录屏,没研究过,个人感觉应该不行),而hook widevine的DecryptAndDecodeFrame方法可以利用浏览器的快进功能实现快速将整个视频抓取下来(实际上也就是快速将原始加密数据输入到解密方法进行解密,而这个过程还原出来的视频也是正常速度的)

jimmyzang 发表于 2020-6-24 15:59

dnv123820 发表于 2020-6-24 12:52
这是一款叫做 SmartNovel 的工具,它来自吾爱破解的 jimmyzang,你只要输入一句话,它就能根据你选的主题自 ...

https://www.52pojie.cn/forum.php?mod=viewthread&tid=1147758&page=166#pid32557155

jimmyzang 发表于 2020-4-22 20:50

本帖最后由 jimmyzang 于 2020-4-24 06:35 编辑

Hmily 发表于 2020-4-22 19:13
代码用代码库处理下。
已经处理了。@Hmily

jimmyzang 发表于 2020-4-24 13:44

本帖最后由 jimmyzang 于 2020-4-24 13:46 编辑

帖子每次修改都要被审核,不敢改了,新想法贴后面吧。

Widevine破解后,原帖子中写的是输出成PNG文件,今天突然想到chromium里面有一个方法是mojovideoencode,可以用这个方法编码输出成H264,
伪码贴一下,做个备忘。


    auto mock_vea_client = std::make_unique< MojoVideoEncodeAccelerator>();
          Initialize(mock_vea_client.get());
    base::UnsafeSharedMemoryRegion shmem =
      base::UnsafeSharedMemoryRegion::Create(
            VideoFrame::AllocationSize(PIXEL_FORMAT_I420, kInputVisibleSize) *
            2);
    base::WritableSharedMemoryMapping mapping = shmem.Map();
    const scoped_refptr<VideoFrame> video_frame = VideoFrame::WrapExternalData(
      PIXEL_FORMAT_I420, kInputVisibleSize, gfx::Rect(kInputVisibleSize),
      kInputVisibleSize, mapping.GetMemoryAsSpan<uint8_t>().data(),
      mapping.size(), base::TimeDelta());
    video_frame->BackWithSharedMemory(&shmem);
    const bool is_keyframe = true;

    EXPECT_CALL(*mock_vea_client, BitstreamBufferReady(kBistreamBufferId, _))
      .WillOnce(testing::Invoke(
            (int32_t, const BitstreamBufferMetadata& metadata) {
            EXPECT_EQ(is_keyframe, metadata.key_frame);
            }));

    mojo_vea()->Encode(video_frame, is_keyframe);

Hmily 发表于 2020-4-22 19:13

代码用代码库处理下。

爱飞的猫 发表于 2020-4-22 20:01

… 但这样就是 webrip (类似翻录)了,不是 webdl (源文件下载解密,不经过二次转码)。

jimmyzang 发表于 2020-4-22 21:14

jixun66 发表于 2020-4-22 20:01
… 但这样就是 webrip (类似翻录)了,不是 webdl (源文件下载解密,不经过二次转码)。

本身widevine就是解码和解密绑定在一起的,不可能不经过二次转码的呀。

89684828 发表于 2020-4-25 14:17

很高深!触不可及{:1_921:}

NiSandy 发表于 2020-4-25 14:53

兄弟你的私聊一下,有个问题请教你,方便加个联系方式吗

九天临兵帝 发表于 2020-4-25 20:09

学习了,想清楚了许多东西,谢谢

cydt0816 发表于 2020-4-25 21:36

学习到了,感谢
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: HTML5视频解密的方法(widevine的破解思路)