申请会员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
I D:牛肉丸鸭爪
邮箱:niurow@qq.com
申请通过,欢迎光临吾爱破解论坛,期待吾爱破解有你更加精彩,ID和密码自己通过邮件密码找回功能修改,请即时登陆并修改密码!
登陆后请在一周内在此帖报道,否则将删除ID信息。
ps:登录后请把文章整理发布到脱壳破解区 未报到,账号注销。 版主大大 能再审核一次吗,,,之前根本没收到通知啊 游客 222.76.37.x 发表于 2020-5-7 19:50
版主大大 能再审核一次吗,,,之前根本没收到通知啊
只能申请一次,这里都是主动来查,不会通知,你可以等开放注册。
页:
[1]