逆向:某酷自研DRM解密分析 时间: 2022-09-30 16:41 分类: JAVA ####前言 其实很早之前就扒了一份解密JS下来,但是最近想下载客户端的4K,经过抓包分析,也是客户端4K也是用的他们自研DRM,应该和网页端一样,都是`AES128`加密的。 但是4K视频是`H265`编码的,之前扒的解密代码完全没有`H265`相关信息,于是想试下最新版的`解密JS`(结果就是最新版的解密JS里的`H265`相关代码应该全是摆设,还没有完善,估计要等到大多数浏览器支持`H265`视频才会去完善了) 之所以认为它也是通过`JS`解密的是因为最新版客户端他们使用了`Electron`,可以参考这篇文章:https://developer.aliyun.com/article/829878 有点像以前比较火的`H5`开发三端APP一样,就是一套代码,多端运行。但遗憾的是某酷的客户端没有用`JS`解密,因为`JS`解密的性能肯定是不及原生`C语言`的,通过`Fiddler`替换掉`JS`里的解密相关代码也可以知道,因为替换掉并不影响播放。 好了,虽然最终没有达到自己的需求,但相比之前扒的代码,这次还是稍微复杂一点,所以记录一下。 ####分析 之前的老版本用的`Worker`,新版本也是沿用了这一技术,主要就是为了反调试,因为每次`Worker`生成的代码打的断点,在浏览器刷新后就会失效,因为整个代码是动态加载的,是一个`Blob`对象。 与播放器交互主要就是通过`message`发送消息进行交互。 关于`Worker`的描述就不再多说了,之前老版本是直接把`Worker`生成的`JS代码`直接复制出来基本上就可以直接用了,现在的新版本还是有些需要注意的地方,那就是如何传参。 好在`Worker`代码没做混淆,新版本的也是直接去控制台的`Source`里面去找,`Top`那个不用管,`Worker`是类似`UUID`的那种名字: ![微信截图_20220930161603.png](https://0o0.me/usr/uploads/2022/09/267671471.png) 直接扒下来就是,可以看到它直接帮你`new`好了,都不用自己去调取模块,与之前某多多分析的时候差不多,也是`webpack`形式的。 既然它已经`new`好了,那我们直接用个变量接收即可: ![微信截图_20220930161925.png](https://0o0.me/usr/uploads/2022/09/3018482746.png) 可以看到,它调取的就是`key`为`39`的模块,但需要注意的是,它返回的是一个`Function`: ``` function ZA(A) { var t = new WA; A.addEventListener("message", (function(e) { var n = e.data; switch (n.type) { case VA.a.MUX: try { var r = t.push(n.data.segmentData, n.data.discontinuity, n.data.sequential, n.data.timestampOffset, n.data.encrypted) , i = r.output.reduce((function(A, t) { return Array.isArray(t) ? t.reduce((function(A, t) { return A.push(t.buffer), A } ), A) : A.push(t.buffer), A } ), []) , o = { type: VA.a.RESULT_RETURN, payload: { streams: r.streams, output: r.output } }; A.postMessage(o, i), delete r.output } catch (e) { A.postMessage({ type: VA.a.ERROR, error: { message: e.message, stack: e.stack } }) } break; case VA.a.INIT: try { t.init(n.data.demuxer, n.data.remuxer) } catch (e) { A.postMessage({ type: VA.a.ERROR, error: { message: e.message, stack: e.stack } }) } break; case VA.a.SET_FILTER: var w = n.data , D = w.pipelineName , P = w.filterMethod , a = w.args , s = w.id; try { t.setDemuxerFilter(D, new Function("","return " + P)(), a, s) } catch (e) { A.postMessage({ type: VA.a.ERROR, error: { message: e.message, stack: e.stack } }) } break; case VA.a.CLEAR_FILTER: try { t.clearFilter(n.data.id) } catch (e) { A.postMessage({ type: VA.a.ERROR, error: { message: e.message, stack: e.stack } }) } break; case VA.a.RESET: try { t.reset() } catch (e) { A.postMessage({ type: VA.a.ERROR, error: { message: e.message, stack: e.stack } }) } } } )) } ``` 实际上我们需要的就是`var t = new WA;`,因为解密相关都是这个对象。 它里面的`A.addEventListener("message", (function(e) {`就是与播放器进行交互的。 由于我们要用`NodeJS`调用,所以`addEventListener`直接去掉,那么稍作修改直接将`t`返回即可。 ``` function ZA(A) { var t = new WA; return t; } ``` 这样一来我们的`ppp`接收到的就是`t`对象,用它来进行解密操作即可。 根据断点一步一步调试可以查看到一些固定变量,直接改造如下: ![微信截图_20220930163337.png](https://0o0.me/usr/uploads/2022/09/2308876920.png) 相比以前老版本,多调了几个函数初始化,也正是因为如此,一开始没有多想,以为跟以前差不多,只抓了`init`部分的参数,`setDemuxerFilter`没有执行,结果一直解密失败,浪费了几个小时。 ####总结 整个代码其实没什么难度,因为连混淆都没有,主要就是`Worker`反调试,播放器与解密代码的交互调试比较麻烦。 大致就是如下流程: > 创建`Worker`,初始化 key > 播放器下载ts切片,通过message传给Worker > Worker 解密,将结果通过message再传回给播放器 标签: 无