发表于 2020-3-28 16:26

申请会员ID:牛肉丸鸭爪【未报到,已注销】


1、申 请 I D:牛肉丸鸭爪
2、个人邮箱:niurow@qq.com
3、原创技术文章:某酷的drm加密分析

首先声明所有分析用的代码都来自互联网。

某酷视频加密方式有会多种,现在用的最普遍的是某酷自家的DRM加密,用的是AES-ECB加密。这个原理其实是在https://www.320nle.com/vvtoolbox-gui-series/3455.html这个分享内容里得知的,作者也提供了工具。但是某酷在此后就封禁了prompt函数,一使用该函数就会向视频日志发送用户的账户信息,建议不要再使用该函数,可以使用油猴脚本或者其他弹窗函数来查看。

这个播放器源码就在视频页里 xxxxx-player.min.js
首先将代码下载下来,一般释出的代码都是经过压缩的,直接上https://www.sojson.com/js.html 把代码进行解压解密。
搜索了一番发现aes的解密模块是在一个名叫TS_MUXER_SOURCE的模块下,这个模块在运行时是一个web_worker(html5的多线程方式),应该是为了解密的同时防止主线程阻塞。



该模块的几个开放接口:
      switch(type){
          case "INIT":
            tsMuxer.initTransmuxer(data);
            break;
          case "BUFFER_DATA":
            tsMuxer.push(data);
            break;
          case "SET_BASEMEDIA_DECODE_TIME":
            tsMuxer.setBaseMediaDecodeTime(data);
            break;
          case "RESET_INIT_SEGMENT":
            tsMuxer.resetInitSegment();
            break;
          case "SET_DISCONTINUITY":
            tsMuxer.setDiscontinuity(data);
            break;
          case "CHANGE_DECODE_KEY":
            tsMuxer.changeDecodeKey(data)
            break;
          case"CLEAN_WHEN_ERROR":
            tsMuxer.cleanWhenError();
            break;
          case "CLEAN":
            tsMuxer.clean()
            break;

所以以这里为切入口进行分析,同样把这个模块代码下载后解密。
下面开始直接分析这个模块就行了。
首先是initTransmuxer() 这个函数是初始化解析器的参数,其中就包含了解密用的 key。

所以目前key已经找到了。

这几个函数中唯一和数据相关的函数是 tsMuxer.push(data);
这里push进去的数据会通过下面这个回调函数返回:
const tsMuxer = new global.TsMuxer(function(dataSet, transferable) {
    context.postMessage(dataSet, transferable)
});


这里是最外层的调用


首先是ts包的拆分,以188个字节进行拆分处理


然后是对数据包分类处理,这里可以看到只有pmt和pes包会处理


发现PES包处理的a函数有古怪,这里是比较关键的一步,因为这里看见了

基本上到这里就可以了分析出加解密的原理了。

为了看得更清楚把函数a进行了还原。

a = function (payLoadDataList, payloadType, o) {
    var isValidPayload, _buffer, _payloadBuffer = new Uint8Array(payLoadDataList.size),
      payloadStruct = {
            type: payloadType
      },
      count = 0,
      size = 0;
    if (payLoadDataList.data.length && !(payLoadDataList.size < 9)) {
      payloadStruct.trackId = payLoadDataList.data.pid
      for (count = 0; count < payLoadDataList.data.length; count++) {
            _buffer = payLoadDataList.data;
            _payloadBuffer.set(_buffer.data, size);
            size += _buffer.data.byteLength;
      }
      var _payloadBufferCache, _struct, _flag;
      _payloadBufferCache = _payloadBuffer;
      _struct = payloadStruct;
      _struct.packetLength = 6 + (_payloadBufferCache << 8 | _payloadBufferCache);
      _struct.dataAlignmentIndicator = 0 != (4 & _payloadBufferCache);
      192 & (_flag = _payloadBufferCache) && (_struct.pts = (14 & _payloadBufferCache) << 27 | (255 & _payloadBufferCache) << 20 | (254 & _payloadBufferCache) << 12 | (255 & _payloadBufferCache) << 5 | (254 & _payloadBufferCache) >>> 3, _struct.pts *= 4, _struct.pts += (6 & _payloadBufferCache) >>> 1, _struct.dts = _struct.pts, 64 & _flag && (_struct.dts = (14 & _payloadBufferCache) << 27 | (255 & _payloadBufferCache) << 20 | (254 & _payloadBufferCache) << 12 | (255 & _payloadBufferCache) << 5 | (254 & _payloadBufferCache) >>> 3, _struct.dts *= 4, _struct.dts += (6 & _payloadBufferCache) >>> 1));
      _struct.data = _payloadBufferCache.subarray(9 + _payloadBufferCache);
      e.decodeKey && r && (_struct.data = tt(_struct.data, e.decodeKey));
      isValidPayload = "video" === payloadType || payloadStruct.packetLength <= payLoadDataList.size;
      (o || isValidPayload) && (payLoadDataList.size = 0, payLoadDataList.data.length = 0);
      isValidPayload && t.trigger("data", payloadStruct);
    }
};

数据流:188字节的ts包
               ->去掉包头184字节负载
               ->payloadUnitStartIndicator判断是否是负载开始
               ->每一段以负载开始的数据放入队列
               ->每获取完一整段负载就开始进行解密
               ->需要解密的负载去掉 9 + _payloadBufferCache 个字节
               ->开始解密
               ->得到解密后的数据
               ->视频数据进行解码

所以了解的原理就开始动手了。直接放源码吧,我把整个流程进行了简化,去掉了视频解码部分,只进行解密。这个模块是nodejs模块,可以直接用于数据解密。

const path = require("path");
const fse = require("fs-extra");
const atob = require('atob');

let ykDecript = function () {
    var Oe = {
      16: 10,
      24: 12,
      32: 14
    },
      Ne = ,
      je = ,
      Ue = ,
      Be = ,
      Fe = ,
      ze = ,
      He = ,
      Ye = ,
      Ve = ,
      qe = ,
      Ge = ,
      Qe = ,
      We = ,
      Ke = ,
      Xe = ;

    function n(e) {
      return parseInt(e) === e
    }

    function i(e) {
      if (!n(e.length)) return !1;
      for (var t = 0; t < e.length; t++)
            if (!n(e) || e < 0 || 255 < e) return !1;
      return !0
    }

    function o(e, t?) {
      if (e.buffer && "Uint8Array" === e.name) return t && (e = e.slice ? e.slice() : Array.prototype.slice.call(e)), e;
      if (Array.isArray(e)) {
            if (!i(e)) throw new Error("Array contains invalid value: " + e);
            return new Uint8Array(e)
      }
      if (n(e.length) && i(e)) return new Uint8Array(e);
      throw new Error("unsupported array-like object")
    }

    function s(e) {
      for (var t = [], n = 0; n < e.length; n += 4) t.push(e << 24 | e << 16 | e << 8 | e);
      return t
    }

    function r(e) {
      return new Uint8Array(e)
    }

    function a(e, t, n, i?, o?) {
      null == i && null == o || (e = e.slice ? e.slice(i, o) : Array.prototype.slice.call(e, i, o)), t.set(e, n)
    }

    var Je;
    Je = function e(t) {
      if (!(this instanceof e)) throw Error("AES must be instanitated with new");
      Object.defineProperty(this, "key", {
            value: o(t, !0)
      }), this._prepare()
    };
    Je.prototype._prepare = function () {
      var e = Oe;
      if (null == e) throw new Error("invalid key size (must be 16, 24 or 32 bytes)");
      this._Ke = [], this._Kd = [];
      for (var t = 0; t <= e; t++) this._Ke.push(), this._Kd.push();
      var n, i = 4 * (e + 1),
            o = this.key.length / 4,
            r = s(this.key);
      for (t = 0; t < o; t++) n = t >> 2, this._Ke = r, this._Kd = r;
      for (var a, l = 0, u = o; u < i;) {
            if (a = r, r ^= je << 24 ^ je << 16 ^ je << 8 ^ je ^ Ne << 24, l += 1, 8 != o)
                for (t = 1; t < o; t++) r ^= r;
            else {
                for (t = 1; t < o / 2; t++) r ^= r;
                for (a = r, r ^= je ^ je << 8 ^ je << 16 ^ je << 24, t = o / 2 + 1; t < o; t++) r ^= r
            }
            for (t = 0; t < o && u < i;) c = u >> 2, d = u % 4, this._Ke = r, this._Kd = r, u++
      }
      for (var c = 1; c < e; c++)
            for (var d = 0; d < 4; d++) a = this._Kd, this._Kd = Qe ^ We ^ Ke ^ Xe
    }, Je.prototype.encrypt = function (e) {
      if (16 != e.length) throw new Error("invalid plaintext size (must be 16 bytes)");
      for (var t = this._Ke.length - 1, n = , i = s(e), o = 0; o < 4; o++) i ^= this._Ke;
      for (var a = 1; a < t; a++) {
            for (o = 0; o < 4; o++) n = Be >> 24 & 255] ^ Fe >> 16 & 255] ^ ze >> 8 & 255] ^ He] ^ this._Ke;
            i = n.slice()
      }
      var l, u = r(16);
      for (o = 0; o < 4; o++) l = this._Ke, u = 255 & (je >> 24 & 255] ^ l >> 24), u = 255 & (je >> 16 & 255] ^ l >> 16), u = 255 & (je >> 8 & 255] ^ l >> 8), u = 255 & (je] ^ l);
      return u
    }, Je.prototype.decrypt = function (e) {
      if (16 != e.length) throw new Error("invalid ciphertext size (must be 16 bytes)");
      for (var t = this._Kd.length - 1, n = , i = s(e), o = 0; o < 4; o++) i ^= this._Kd;
      for (var a = 1; a < t; a++) {
            for (o = 0; o < 4; o++) n = Ye >> 24 & 255] ^ Ve >> 16 & 255] ^ qe >> 8 & 255] ^ Ge] ^ this._Kd;
            i = n.slice()
      }
      var l, u = r(16);
      for (o = 0; o < 4; o++) l = this._Kd, u = 255 & (Ue >> 24 & 255] ^ l >> 24), u = 255 & (Ue >> 16 & 255] ^ l >> 16), u = 255 & (Ue >> 8 & 255] ^ l >> 8), u = 255 & (Ue] ^ l);
      return u
    };
    var Ze = function e(t) {
      if (!(this instanceof e)) throw Error("AES must be instanitated with new");
      this.description = "Electronic Code Block", this.name = "ecb", this._aes = new Je(t)
    };
    Ze.prototype.encrypt = function (e) {
      if ((e = o(e)).length % 16 != 0) throw new Error("invalid plaintext size (must be multiple of 16 bytes)");
      for (var t = r(e.length), n = r(16), i = 0; i < e.length; i += 16) a(e, n, 0, i, i + 16), a(n = this._aes.encrypt(n), t, i);
      return t
    }, Ze.prototype.decrypt = function (e) {
      if ((e = o(e)).length % 16 != 0) throw new Error("invalid ciphertext size (must be multiple of 16 bytes)");
      for (var t = r(e.length), n = r(16), i = 0; i < e.length; i += 16) a(e, n, 0, i, i + 16), a(n = this._aes.decrypt(n), t, i);
      return t
    };
    let et = null;
    const decript = function (e, t) {
      null != t && (et = new Ze(t)), et = et || new Ze(t);
      var n = e.length,
            i = n % 16;
      if (i) {
            var o = et.decrypt(e.subarray(0, n - i)),
                r = e.subarray(n - i),
                a = new Uint8Array(o.length + r.length);
            return a.set(o, 0), a.set(r, o.length), a
      }
      return et.decrypt(e)
    };

    const Buffer = require("buffer").Buffer;

    let tsProcess;
    tsProcess = function () {
      this._buffer = new Uint8Array(188);
      this._pktLength = 0;
      this.pmtPid = 0;
      this.options = null;
      this.streamCache = [];
      this.streamList = [];
      this.pktList = [];
    }
    tsProcess.prototype.init = function (options) {
      this.options = options;
    }
    tsProcess.prototype.pktProcess = function (pktData) {
      let pktSt = {
            payloadUnitStartIndicator: false,
            pid: 0,
            pmtPid: 0,
      };
      let pktHeadOffset = 4;

      pktSt.payloadUnitStartIndicator = !!(64 & pktData);
      pktSt.pid = 31 & pktData;
      pktSt.pid <<= 8;
      pktSt.pid |= pktData;

      if (1 < (48 & pktData) >>> 4)
            pktHeadOffset += pktData + 1;

      if (0 === pktSt.pid) {
            let payLoadBuffer = pktData.subarray(pktHeadOffset);
            let offset = 0;
            if (pktSt.payloadUnitStartIndicator) {
                offset += payLoadBuffer + 1
            }
            payLoadBuffer = payLoadBuffer.subarray(offset);
            this.pmtPid = (31 & payLoadBuffer) << 8 | payLoadBuffer;
            this.pmtPid;
      }
      else if (pktSt.pid === this.pmtPid) {
      }
      else {
            if (pktSt.payloadUnitStartIndicator) {
                if (this.streamCache.length > 0) {
                  this.streamList.push(this.streamCache.slice(0));
                  this.streamCache = [];
                }
            }
            this.streamCache.push({ head: pktData.subarray(0, pktHeadOffset), data: pktData.subarray(pktHeadOffset) });
            return null;
      }

      return pktData;
    };

    tsProcess.prototype.decriptStream = function () {
      for (let i in this.streamList) {
            let _stream = this.streamList;
            let streamData = _stream.map(_s => _s.data);
            streamData = Buffer.concat(streamData);
            let headOffset = 9 + streamData;
            let decriptHead = streamData.subarray(0, headOffset);
            streamData = decript(streamData.subarray(headOffset), this.options.decodeKey);

            let _index = 0;
            for (let j in _stream) {
                let end = _index + _stream.data.byteLength;
                if (j == "0") {
                  end -= headOffset;
                  _stream.decriptHead = decriptHead;
                }
                _stream.decriptData = streamData.subarray(_index, end);
                _index = end;
            }
      }
    }

    tsProcess.prototype.genPkt = function () {
      for (let i in this.streamList) {
            let _stream = this.streamList;
            for (let j in _stream) {
                let pktData = null;
                if (_stream.decriptHead) {
                  pktData = new Uint8Array(Buffer.concat(.head, _stream.decriptHead, _stream.decriptData]));
                } else {
                  pktData = new Uint8Array(Buffer.concat(.head, _stream.decriptData]));
                }
                this.pktList.push(pktData);
            }
      }
    }

    tsProcess.prototype.go = function (dataBuffer) {
      let _tsBuffer;
      let _pktBegin = 0, _offset = 188;

      _tsBuffer = new Uint8Array(dataBuffer);

      for (; _offset < _tsBuffer.byteLength;) {
            if (_tsBuffer !== 71 || _tsBuffer != 71) {
                _pktBegin++;
                _offset++;
            } else {
                let pktData = this.pktProcess(_tsBuffer.subarray(_pktBegin, _offset));
                if (pktData) this.pktList.push(pktData);
                _pktBegin += 188;
                _offset += 188;
            }
      }

      if (_pktBegin < _tsBuffer.byteLength) {
            this._buffer.set(_tsBuffer.subarray(_pktBegin), 0);
            this._pktLength = _tsBuffer.byteLength - _pktBegin;
      }

      if (this._buffer == 71 && this._pktLength === 188) {
            this.pktProcess(this._buffer);
            this.streamList.push(this.streamCache.slice(0));
            this._pktLength = 0;
      }

      this.decriptStream();
      this.genPkt();
      this.streamList = [];
      this.streamCache = [];
      return Buffer.concat(this.pktList);
    }

    return tsProcess;
}

const tsProcess = ykDecript();

//获取文件状态
function getFileStat(path) {
    return new Promise<any>((resolve, reject) => {
      fse.stat(path, (err, stats) => {
            if (err) {
                reject(err);
            } else {
                resolve(stats);
            }
      })
    })
}

//读取流
async function readStream(path) {
    let fileStat = await getFileStat(path);
    return new Promise((resolve, reject) => {
      // 根据指定的文件创建一个可读流,得到一个可读流对象
      let readStream = fse.createReadStream(path);
      let fileData = new Uint8Array(fileStat.size);

      let pos = 0;
      readStream.on('data', (chunk) => {
            for (let i = 0, len = chunk.length; i < len; i++) {
                fileData = chunk;
                pos++;
            }
      })

      // end 事件监听读写结束
      readStream.on('end', () => {
            resolve(fileData);
      })
    });
}

function calFileDataByDecription(buffer, key) {
    let _tsProcess = new tsProcess;
    _tsProcess.init({ decodeKey: key });
    return _tsProcess.go(buffer);
}

//写入流
async function writeStream(path, u8Array) {
    let fd = await fse.open(path, 'w');
    await fse.write(fd, u8Array);
    await fse.close(fd);
}

function base64ToArray(base64) {
    var binaryString = atob(base64)
    var len = binaryString.length
    var bytes = new Array(len)
    for (var i = 0; i < len; i++) {
      bytes = binaryString.charCodeAt(i)
    }
    return bytes
}

process.on('message', async function (obj) {
    let filePath = obj.path;
    let key = obj.key;
    let writePath = obj.writePath;
    let fileName = obj.name;

    try {
      let _fileBuffer = await readStream(filePath);
      let _decriptedData = await calFileDataByDecription(_fileBuffer, base64ToArray(key));
      await writeStream(path.join(writePath, `${fileName}`), _decriptedData);
      process.send(true);
    } catch (error) {
      console.log(error);
      process.send(false);
    }
});

export {};

另外附上我已经写好的下载器 https://gitee.com/niurow/m3u8Downloader

Hmily 发表于 2020-3-30 18:30

I D:牛肉丸鸭爪
邮箱:niurow@qq.com

申请通过,欢迎光临吾爱破解论坛,期待吾爱破解有你更加精彩,ID和密码自己通过邮件密码找回功能修改,请即时登陆并修改密码!
登陆后请在一周内在此帖报道,否则将删除ID信息。

ps:登录后请把文章整理发布到脱壳破解区

Hmily 发表于 2020-4-8 15:31

未报到,账号注销。

发表于 2020-5-7 19:50

版主大大 能再审核一次吗,,,之前根本没收到通知啊

Hmily 发表于 2020-5-8 10:19

游客 222.76.37.x 发表于 2020-5-7 19:50
版主大大 能再审核一次吗,,,之前根本没收到通知啊

只能申请一次,这里都是主动来查,不会通知,你可以等开放注册。
页: [1]
查看完整版本: 申请会员ID:牛肉丸鸭爪【未报到,已注销】