某视频网站流媒体初次分析全过程

查看 172|回复 13
作者:ligxi   
开篇
最近呢,学了一点js逆向的知识,一直想找个可以实践一下的平台来试试学习效果。偶然间看到了这个平台,发现很适合拿来练手,不过由于这个网站视频开始播放前都有那种不和谐的广告,所以地址就不能贴了,贴图也做了一些脱敏处理!
需要准备的工具:
  • 一些JavaScript基础知识,能大概看懂就行
  • 浏览器:Chrome浏览器、Edge浏览器等能方便进行调试的,这里我用的是Chrome浏览器
  • 抓包工具:Fiddler,用来替换和保存一些数据
  • 分析工具:Winhex,用来查看内容
  • 下载工具:IDM,用来测试内容是否可以正常下载
  • M3U8下载器:https://www.52pojie.cn/thread-1374045-1-1.html

    第一次分析
    打开目标网站首页,选择任意一个视频信息点击进入后:

    可以直接看到资源下载地址,不过这次目的是来逆向的,所以看不到!看不到!看不到!
    1.1 过控制台检测
    接下来就到了我们的分析时间,点击【在线播放】后尝试使用快捷键F12打开控制台,然后就弹出了以下提示:

    打不开控制台不要紧,选择浏览器右上角的三个小点,从更多工具中打开控制台:

    1.2 过无限debug
    控制台一打开,立马跳出个无限debuuger,看来还是做了一些反调试措施的:

    从右边的堆栈中向下查找入口,从上到下依次点击看看:

    很显然这个并不是,再往下点时,就发现了非常关键的函数调用:

    其实这个无限debugger卡了挺长时间的,一开始开始尝试删除这个关键位置的调用,删除整个debugger的自调用函数,甚至删除整个外层的debugger函数,但是发现这些操作会导致视频无法正常加载,控制台过段时间就会直接卡死等莫名其妙的问题,说明这些函数中有些关键地方还是有用的,那看不懂这些混淆的代码又该怎么办呢?难道此次实战就此终结!!!
    在前思来想去后,突然想到既然debugger关键字会触发断点,那要是我把这个关键字给改了是不是就停不下了?说干就干,把含有无限debugger的js文件在源代码中直接另存为或者从Fiddler另存为一份到本地,方便进行修改。
    从上图可以看到前面的内容为“bugger”,后面的内容为“de”,要修改的关键就在这里里,以文本方式打开对应的js,直接搜“de”,发现有5个地方有这个信息:

    全部替换,这里我替换成“dd”,替换后保存:

    打开Fiddler工具,右边选择自动响应,勾选启用规则和请求传递,填写js网络地址和本地js地址,并保存:

    确认Fiddler能获取浏览器请求后,打开浏览器里的设置,选择清空之前的数据,这样才会重新加载本地替换的js文件:

    重新打开播放页面后,再次打开控制台,发现不仅无限debugger没了,控制台也正常了,视频也能正常加载了,很好!可以愉快的玩耍了。
    1.3 寻找m3u8地址

    打开网络面板,会发现很多的302请求,302表示重定向,这个请求会在响应中返回一个新的地址。开始查找视频文件地址,发现m3u8文件也被重定向了:

    用IDM工具下载这个m3u8文件看看,可以看到返回的居然是一个张图片???

    打开看看,额,居然是二维码,扫了一下返回是视频网址,那m3u8信息哪去了?

    用winhex打开看看,看这个头信息好像就是一个图片

    查找一个关键内容看看,winhex直接搜m3u8:

    发现并不是想要的内容,用winhex前后翻看发现基本都是乱码,那么极有可能m3u8的内容被加密了,存放在这个图片中。尝试在源代码中查找m3u8文件的调用堆栈发现事情并不简单,很多地方的js代码都被混淆了,所以要找到还原方法实属头大,可是没有代码就不能还原出m3u8的内容怎么办?于是想到加载ts文件不是也得从m3u8的内容中读取吗?所以开始尝试查找ts的调用堆栈,这里可以看到ts的请求也是302状态码,先不管:

    把鼠标放在其中一个ts请求的启动器上,点击浮窗中展示的js文件进入调式:

    这里会自动跳转到源代码中js的点击位置,先在这里打上一个断点,免得调试过头了。

    从右下角的
    堆栈从上往下依次点击看看,在每个堆栈位置前后翻翻,然后发现了一个有关m3u8的信息,在这里下断点:

    F5重新加载页面,可以看到m3u8的内容此时已经被加载了:

    尝试把这个m3u8信息保存下来,打开控制台输入了以下代码:
    const data = t.data; // 这里填内容的字符串
    const blob = new Blob([data], {type: "text/plain"});
    const a= document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "demo.m3u8"; // 这里填保存的文件名
    a.click();
    URL.revokeObjectURL(a.href);
    a.remove();

    成功保存,打开看看,正好是m3u8的内容:

    1.4 ts解密
    尝试用IDM访问前面一两个ts链接看看有没有加密:

    怎么返回的又是图片?直接打开看看:

    啥呀这是,一张花屏的图片,用winhex打开看看:

    上下拖动内容后发现并不是常规的图片内容,果然有猫腻,在图片的下面发现了视频相关的头信息,难道这是图片伪装的视频文件?从其它平台找一个正常的ts文件对比看看:

    哦豁,原来后面的内容确实是视频文件,只是前面一小部分是BMP文件的头信息,那简单了,直接删了试试:


    修改后另存为一份,后缀改为ts,避免修改错误导致源文件信息丢失

    打开修改后的ts文件,正常播放:

    PS:后来发现论坛里,逍遥大佬写的m3u8下载器可以自动去除这种不是ts内容的信息:

    至此第一阶段分析结束,我以为也就这样了,但是再尝试打开几个视频后,发发发现居然有不一样的加密!!!
    OK,既然被发现了,那必须得再分析看看=>
    第二次分析
    2.1 获取m3u8和key
    有了前面那些踩坑,这次分析起来就快多了,再次尝试打开视频页面,通过调试再次拿到m3u8文件,拖入m3u8下载器看看:

    发现这次需要key,通过不断的调试终于hook到key的位置,这里先把key拿来用一下,填入key,发现又解码失败了:

    2.2 ts解密
    难道这个ts与众不同?打开m3u8文件后发现ts的链接长度明显变短了,下载ts后再次打开文件看看:

    又是熟悉的花屏图片,用winhex打开看看:

    发现这次怎么也找不到ts头信息了,搜也搜不到了,而且内容非常乱,看起来应该是被加密了,这样只能调试代码看看怎么回事了。这次换个方法找,直接搜,为啥呢,因为调试了半天也跳到解密位置,直接搜碰碰运气:

    看到有很多结果,前面两个js可以忽略,第3、4个js文件很可疑,打开相应的js再次搜索关键字,发现有很多结果,从上到下依次都大概看一下。然后发现这个位置很可疑,其它地方都没混淆代码,偏偏这里有混淆,此地无银三百两,打上断点试试:

    再次F5加载页面或者继续播放,点击格式化后再搜关键字,可以看到代码都被混淆了,但是有些关键信息还是可以看到的:

    统统打上断点看看,F8依次执行,可以看到rsp就是网站加载的原始ts文件,大小一模一样,这里我从Fiddler保存了一份做对比:

    F8继续执行,发现len变量的值为1243440,应该是确定加密范围的:

    点击继续下一个断点,发现rsp变了,长度不是1244642,而是1243440了:

    依葫芦画瓢,这里先保存一下rsp内容,此时的rsp是不能播放的:

    打开本地保存的后缀为.bmp的ts文件和这个enc.ts进行对比,并搜搜看:


    原来是通过一定的算法把前面和后面一部分内容给去掉了,那么剩下的应该就是被加密的视频文件了,删除后大小刚好就是enc.ts的大小。下一个断点,来到uText这个变量,发现这个变量的长度刚好和前面的rsp长度是一样的,这里应该就是做了转换,为解密做准备:

    下一个断点,来到解密函数,这里可以清楚看到加密的内容uTxt、key、iv,虽然看不太懂混淆的代码,但这显然是一个标准的AES-CBC解密操作:

    而最后的newRresponse=new Uint8Array(arr);就是解密好的视频流,当然这里可以用代码来实现,这里的key和iv都是同一个值,控制台输出一下:

    这里用Python代码实现解密:
    from Crypto.Cipher import AES
    import base64
    fr = open(r'enc.ts', 'rb')
    content = fr.read()
    key = iv = base64.b64decode("9Kq58j61Jx42io3e37Qomg==")
    cipher = AES.new(key, AES.MODE_CBC, iv)
    data = cipher.decrypt(content)
    with open(r'dec.ts', 'wb') as fw:
      fw.write(data)
    成果展示

    总结
    通过这次实战见识到了很多花里胡哨的操作,还是相当有收获的。也明白还是需要不断学习才能走的更远,再接再厉!当某种逆向思路不行就得尝试换种思路,还有一点就是逆向一定要有耐心!
    不足
    由于还没学会如何使用AST解这些混淆代码,导致看不懂核心逻辑,所以以下问题暂时没有得到解决
    [ol]
  • m3u8地址下载的文件如何从图片提取并还原成正常的m3u8内容?
  • 第二种加密的每个ts似乎都对应着一个key,那么这些hexkey应该如何计算出来?
  • 第二种ts文件从什么地方开始到结束才是真正需要解密的内容?
  • 嗯,就先这样吧!
    [/ol]

    内容, 发现

  • 漁滒   

  • m3u8地址下载的文件如何从图片提取并还原成正常的m3u8内容?
  • 第二种加密的每个ts似乎都对应着一个key,那么这些hexkey应该如何计算出来?
  • 第二种ts文件从什么地方开始到结束才是真正需要解密的内容?

    第一个问题,代码来自于play.new.js,反混淆后核心代码如下
    [JavaScript] 纯文本查看 复制代码  _0x109702["onload"] = function (e) {
        var buffer = _0x109702["response"];
        var t = new Uint8Array(buffer);
        t = t["slice"](3354);
        var _0x424177 = pako["inflate"](t);
        var res = "";
        var _0x3295a8 = 16384;
        var _0x6ef1a3;
        for (_0x6ef1a3 = 0; _0x6ef1a3
    这是请求成功的回调函数,可以看到先截取3354字节后面的内容,然后使用pako进行解压,python代码如下
    [Python] 纯文本查看 复制代码    response = requests.get(m3u8_1, headers=headers).content
        m3u8 = gzip.decompress(response[3354:]).decode()
    第二个问题和第三个问题,代码来自于hls.min.js,反混淆后核心代码如下
    [JavaScript] 纯文本查看 复制代码                if (t["url"]["indexOf"](".ts") > 0 && "arraybuffer" === t["responseType"]) {
                      try {
                        var buffer = e["response"];
                        var rsp = new Uint8Array(buffer);
                        if (rsp[10] == 138) {
                          let len = arrayToInt(new Uint8Array([rsp[141], rsp[140], rsp[139], rsp[138]]));
                          let hexKey = Array["from"](rsp["slice"](142, 158), _0x144ed7 => _0x144ed7["toString"](16)["padStart"](2, "0"))["join"]("");
                          rsp = rsp["slice"](158, 158 + len);
                          const key = CryptoJS["enc"]["Hex"]["parse"](hexKey, "uft-8");
                          let uTxt = CryptoJS["lib"]["WordArray"]["create"](rsp);
                          let decrypted = CryptoJS["AES"]["decrypt"]({
                            "ciphertext": uTxt
                          }, key, {
                            "iv": key,
                            "mode": CryptoJS["mode"]["CBC"],
                            "padding": CryptoJS["pad"]["Pkcs7"]
                          });
                          let arr = wordToByteArray(decrypted["words"]);
                          if (arr["length"] > 0 && arr[0] == 71 && arr[1] == 64) {
                            newResponse = new Uint8Array(arr);
                          }
                        }
                      } catch (e) {}
                    }
    第二个问题就很明显了,key取的是142字节到158字节
    第三个问题这里首先计算了ts的长度,在138字节处取了一个小端序的int类型,然后从158字节开始截取出真实的ts
    然后就可以使用标准aes解密出ts视频,python代码如下
    [Python] 纯文本查看 复制代码    ts = requests.get(tsurl, headers=headers).content
        if ts[10] == 138:
            key = ts[142:142+16]
            ts = ts[158:158 + struct.unpack("
  • jinzhu160   

    var AAA = Function;
    Function = function (x) {
      if (x != 'debugger') {
        return AAA(x);
      }
      return function () {};
    };
    执行这个函数以后,断点会自动消失哦。
    ligxi
    OP
      


    gamma 发表于 2022-8-18 16:07
    小白提问时间:
    m3u8软件如何设定参数才能使其从某一位置开始读取呢?

    其实没有一个软件可以满足各种需求的,设定参数读取某一个位置的,这种要么软件的作者修改后支持,要么就只能自己写了。
    根据网页的逻辑代码,自己编写出对应的处理代码。逍遥的m3u8软件也是这样处理的,判断流媒体是否是正常的头信息,如果是正常的就不进行处理,如果不是就按网页的逻辑代码来处理。
    如果感兴趣的话可以看看常见的文件头信息,就会懂如何判断了。
    laobj   

    感谢楼主分享原创
    ligxi
    OP
      


    OVVO 发表于 2022-4-8 15:39
    大佬您好,希望能分析下这篇
    【震惊】发现一种极其罕见的m3u8魔改加密
    https://www.52pojie.cn/thread-16 ...

    这个ts加密位置好像和那个阿里是一样的,有一篇阿里的帖子就有提到,不过能不能看懂就得看你自己了。
    chucklee   

    学习一下,谢谢分享
    gblw   

    看得我热血沸腾!
    OVVO   

    大佬您好,希望能分析下这篇
    【震惊】发现一种极其罕见的m3u8魔改加密
    https://www.52pojie.cn/thread-1613157-1-1.html
    (出处: 吾爱破解论坛)
    李玉风我爱你   

    图片类型的可以在hex模式下搜索 49 45 4e 44(IEND)
    49 45 4e 44是png的end标识符 之后的数据就是加密的m3u8地址


    clipboard.png (47.5 KB, 下载次数: 1)
    下载附件
    2022-4-8 15:49 上传

    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部