慕课网视频下载解密

JAVA herman 918浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:codedq,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:codedq,之前的微信号好友位已满,备注:返现
饿了么大量招人,我内推!Java 方向!薪资不设上限,工作年龄不限!工作地点限魔都,可电话面试!简历,发我微信:codedq
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

这已经是去年的事情了,这件事要从被同事打脸开始说起…

去年我和同事开玩笑,说只要网上你能看到的东西,我就能下载下来让他保存在电脑上。同事摇了摇头,一脸的不相信,然后拿住我的鼠标,打开了众所周知的慕课网,说:“我要这个视频,你能把文件给我吗?”。

我点开浏览器控制台,发现网络请求中一堆的ts文件,之前我又不是没有下载过这样的视频,M3U8而已,怎么能难得住我?我满口就答应了同事,说:“5分钟后给你。”,事后我发现人们总是会低估一件事情的难度,从业者都低估自己所面对的需求,更何况是外行人呢。不过最后我还是用实际行动证明了我的观点,凡是浏览器能播放的东西,都可以保存在自己的电脑上,不管多么的复杂。

虽然我看到了一堆的ts文件,可是我苦苦寻找,并没有找到M3U8文件,那么这传说中的M3U8文件到底去了哪里了呢?

然后我从一套大家都见过的课程开始入手了,慢慢探索这其中的奥秘。

视频的播放地址是:

https://www.imooc.com/video/1430

是个学编程的人都知道后面的1430是个id,那么这个id用来做什么了呢?

看了看网络请求,发现了一个非常有趣的链接:

https://www.imooc.com/course/playlist/1430?t=m3u8&_id=5848c001b3fee30a6c8b51bd&cdn=aliyun1

t=m3u8,嗯?看到了我熟悉的m3u8,那么t可能代表的意思是type喽,后面的_id参数又是什么?毫不犹豫我按下了ctrl + F,搜索一下5848c001b3fee30a6c8b51bd,在搜索结果中我看了了HTML页面上居然有这个东西:

OP_CONFIG.mongo_id="5848c001b3fee30a6c8b51bd";
OP_CONFIG.page="video2.4";

没有问题,就是他了,后面的cdn=aliyun1这个不用想就知道他是用了阿里云的cdn。

链接中的参数都找全了,但是有一个问题,这个链接真的是返回的m3u8文件吗?

看了一眼响应数据,我又傻眼了:

{
    "result":1,
    "data":{
        "info":"MkkqXqhmamoxAUB1PQ...",
        "cdn":["aliyun","aliyun1","letv"]
    },
    "msg":""
}

info中一堆乱七八糟的东西,这堆东西到底是啥??? 明明这个链接看上去好像是要返回M3U8来着,但是为什么返回了这么一堆东西,一定是某种加密方式,不探索出这种加密方式,还怎么往下面进行。可是啊,加密方式的探索,哪里有那么容易。

我想了一会儿,这个链接是哪里发送出来的呢?我就可以从哪里找到他的加密方式啊。我又开始了一顿搜索,在一个js文件中看到了这个链接。

摘录一小部分:

...
"https" === h && (v = "https://www.imooc.com/course/playlist/" + pageInfo.mid + "?t=m3u8&_id=" + OP_CONFIG.mongo_id),
window.thePlayer = mocoplayer($("#video-box"), {
    url: v,
    title: videoTitle,
...

我们可以看到,拼接好了链接字符串后,又调用了mocoplayer方法,那么我一定可以根据这个mocoplayer方法来一步步跟进,得知他的加密算法的。又一顿搜索,得知了,mocoplayer原来在mocoplayer.js中。不错不错。可是难题来了,这个文件好大啊,使用webpack打包的文件,让我怎么看?

苦想了很久,应该用什么思路去面对这么大的文件呢?突然想到一招,我看看上面那个链接,到底是js的哪一行请求的不就行了?

很快我就到了发起链接请求的地方,可是这也太难找了吧,发起了一个链接请求而已,距离解密可能还早着呢!!!

没有办法,既然到这里了,那我就必须要打着断点往后看,毕竟这个链接一旦访问成功,离成功就不远了啊,过了好长一段时间,我看到了代码中有data.info这几个字符,立刻集中了精神,马上要到了,紧接着我就看到了下面的代码:

慕课网视频解密方法

这个被圈起来的部分长得好眼熟,那么就意味着这个destm_1.default(mediadata.data.info);就是解密方法了。

深深的呼吸了一口气,给这个方法打上断点,然后进去看看,果不其然,最终确定这个文件的21919行到22066行是我想要的解密算法。

给这个字符串解密后,我发现,并不是我想的那么简单!!!

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=512000, RESOLUTION=1280x720
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/medium.m3u8?cdn=aliyun1...
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=384000, RESOLUTION=1280x720
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/medium.m3u8?cdn=aliyun1...
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=256000, RESOLUTION=720x480
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/low.m3u8?cdn=aliyun1...

看上去像是M3U8文件,可是文件里面的链接却又是m3u8格式的???

我尝试着访问了这些m3u8地址,于是又看到了熟悉的东西:

{
    "code":200,
    "data":{
        "info":"Gm1kQ7hmVBYXVGoxVGdUVGA9VBwcKnAyF1RmJD1UY1QHVGRrVBxu..."
    },
    "msg":"OK"
}

现实总是那么的出乎意料,这一定又是一个加密后的字符串,我敢确定,一定和上一次用了同样的加密方法,我再次试了试:

m3u8字符串

丝毫没有问题,解密出来了,这就是我想要的m3u8,但是真的有那么简单吗???

我把这些ts文件直接下载下来,可是加着密呢呀!!!

上面M3U8字符串中有一串METHOD=AES-128引起了我的注意,这明显是告诉我,加密方式是用了什么!!! 后面的那一串链接一定是密钥了。

可是我打开链接后,居然与普通的密钥完全不一样,他长这样:

{
    "code":200,
    "data":{
        "info":"I2RqhmmlX1UTHvYGIi4cQKqgARoiHh1rIjIo7WRlwhoT9h1nIg==t01UNGJIRvfj"
    },
    "msg":"OK"
}

历史总是那么惊人的相似,又解了一下密:

解密字符串

看上去,并不是那么回事,后面的参数改成了true,它长成了这样:

解密慕课网视频秘密

看起来像是那么回事了,正好16个字节,正好符合AES-128也就是传说中的AES/CBC/PKCS5PADDING,好了,java解密代码安排起来:

/**
 * AES-CBC加解密,该类用来对每一段视频进行解密
 *
 * @author CY
 */
public class AESCBCDecrypt {

    private static final String AES = "AES";

    private static final String AES_CBC = "AES/CBC/PKCS5PADDING";

    /**
     * 使用AES加密或解密无编码的原始字节数组, 返回无编码的字节数组结果.
     *
     * @param input 原始字节数组
     * @param key   符合AES要求的密钥
     * @param iv    初始向量
     * @param mode  Cipher.ENCRYPT_MODE 或 Cipher.DECRYPT_MODE
     */
    public static byte[] aes(byte[] input, byte[] key, byte[] iv, int mode) {
        try {
            Cipher cipher = Cipher.getInstance(AES_CBC);
            cipher.init(mode, new SecretKeySpec(key, AES), new IvParameterSpec(iv));
            cipher.update(input);
            return cipher.doFinal(input);
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("加解密出现了异常,当前的key:" + Arrays.toString(key) + ",IV:" + Arrays.toString(iv));
        }
    }
}

可是啊,需要的iv呢,他去哪里了,问题又出来了,我得看看,是哪里发起的ts文件请求,发起请求的地方应该会有iv吧!

又如我所料,找到了。

ts文件请求

我跟着断点走了好几个ts文件,发现iv前面15位永远都是0,只是第16位会跟随ts文件的变化而发生变化,会逐渐的加1,我也可以模拟这样的变化,用来创建视频的iv值。

经过了一会,我把上面的东西全部梳理了一遍,解密算法是用js写的,而我使用了java,那么我有两种选择:

  • 把它用java代码重新构建一遍(效率高)
  • 用java来执行js文件(效率低)

java执行js的代码如下:

/**
 * 解码的备用方法,运行JavaScript脚本,速度较慢
 *
 * @author CY
 */
public class ImoocDecoderBackup {

    /**
     * JavaScript解码算法执行器
     */
    private static Invocable invocable;

    /* 读取JS文件,并构造执行器 */
    static {
        InputStream inputStream = ImoocDecoderBackup.class.getClassLoader().getResourceAsStream("attachment/decode.js");
        if (inputStream != null) {
            try {
                String javaScript = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n"));
                ScriptEngine engine = new ScriptEngineManager().getEngineByName("Nashorn");
                engine.eval(javaScript);
                invocable = (Invocable) engine;
            } catch (ScriptException ignored) {
            }
        }
    }

    /**
     * 解码成字符串
     *
     * @param crypto 待解码的字符串
     * @return 解码后的字符串
     */
    public static String decoderString(String crypto) {
        return invoke("decode2String", crypto);
    }

    /**
     * 获取慕课网视频的密匙key
     *
     * @param crypto 待解码的字符串
     * @return 字节数组
     */
    public static byte[] decoderKey(String crypto) {
        return new Gson().fromJson(invoke("decode2Bytes", crypto), byte[].class);
    }

    /**
     * 调用JavaScript方法
     *
     * @param methodName   方法名
     * @param encodeString 待解码的字符串
     * @return 解码后的结果
     */
    private static String invoke(String methodName, String encodeString) {
        try {
            return invocable.invokeFunction(methodName, encodeString).toString();
        } catch (ScriptException | NoSuchMethodException ignored) {
        }
        return null;
    }
}

解密出来之后生成一个m3u8文件,那么我就需要解析这个m3u8了,解析它就要符合HLS协议的规范,我尝试写了一下,并且做了测试,但是我发现,我写的并不那么理想,适用范围较小,因为毕竟HLS协议也不是那么简单的规定,他除了普通的视频流,还有直播流的,我找了找同性交友网站,看见了一个工具open-m3u8,不管它好不好用,反正我不用,既然自己写了一部分,就用自己的。

上面的一切工作准备就绪了,现在已经拥有了,解密,解析,那么接下来就是普普通通的下载了,边下载边进行AES-128解密就可以了,可是下载下来的文件零零散散,我还需要合并的,刚开始的时候使用Java的输出流拼接的方式进行的合并,可是没多久就发现了这个方式并不是那么的理想。因为视频也有头和尾,直接硬生生的拼接在一起,相当于玩了一场人体蜈蚣,播放的时候会造成一定的卡顿现象,于是我就选择了使用FFMpeg的方式进行合并:

./ffmpeg.exe -i "concat:ts文件1|ts文件2" -c copy -bsf:a aac_adtstoasc "输出文件"

最终解密出来了一堆完整的视频,放一张成果图片:

下载慕课网视频

推荐阅读:https://github.com/iheartradio/open-m3u8 和 https://halo.cyblogs.top/archives/decrypt-imooc-video-download.html

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号1:xmtxtt(5000人已满),微信号2:xttblog(5000人已满),微信号3:codedq(超3800)。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 慕课网视频下载解密