浅谈:某酷自研DRM非网页端解密key获取 时间: 2022-10-05 19:02 分类: JAVA ####前言 目前某酷有些剧网页端是`widevine`加密的,但客户端却使用的是`自研DRM`,所以此时是无法通过网页获取客户端`4K`的`自研DRM`的解密`key`的,TV端亦是如此。 下面讲下整个分析过程吧。 ####分析 首先想到的是,能否利用网页端的`JS`来获取客户端的解密`key`,因为网页端与客户端都有使用`自研DRM`,所以生成`key`的算法应该是一样的(但还是有待测试)。 先看网页端`自研DRM` key 的生成过程: ``` A.prototype.getCustomDRM = function(A) { //下面的 r 就是解密`key` var t, e, n = A.customizedLicenseData, r = window._sce_dlgtqred(this.context.upsParams.R1, n.encryptRServer, n.copyrightKey); return t = { agent: new iQ(r), type: A.type }, e = { segmentFilter: function(A) { return A.forEach((function(A) { A.encrypted = !A.info.DRM_NOTENCRYPT } )), A } }, //解决历史key直接暴露在window对象的BUG this.clearMemory("4b4g59543c4h5g29"), this.clearMemory("4b4b4b4b4k4h5f32"), { drm: t, filters: e } } ``` 可以看到`key`的生成调用的是: > window._sce_dlgtqred(this.context.upsParams.R1, n.encryptRServer, n.copyrightKey); `this.context.upsParams.R1`可以很容易找到是通过`this.ctx.upsParams.R1 = window._sce_r_skjhfnck()`生成,实际上可以当做一个随机字符串,说到这,我们再看下`TV端`的反编译代码`R1`的生成方法: ![微信截图_20221005182323.png](https://0o0.me/usr/uploads/2022/10/4247367155.png) 简单粗暴,直接随机数生成,相比`TV端`,网页端的`_sce_r_skjhfnck`其实内部实现非常复杂,让人觉得是某种能逆向解密的字符串,实则不然。 然后看`n.encryptRServer, n.copyrightKey`这两个参数,这两个参数是服务器返回来的:`mtop.youku.play.ups.appinfo.get`接口获取。 说到这个接口,当然还得说下它的`encryptR1`请求参数,不然生成解密`key`的时候`R1`是怎么与`mtop.youku.play.ups.appinfo.get`关联起来的呢。 顾名思义,`encryptR1`就是加密后的`R1`,从这里也可以看出,`R1`就是一个随机字符串用作生成`key`,类似`MD5`的加盐。 `encryptR1`可以找到调用的是: > t.encryptR1 = window._sce_lgtcaygl(this.ctx.upsParams.R1) 以上就是整个网页端生成解密`key`的过程: 1、调用`window._sce_r_skjhfnck()`生成`R1` 2、调用`window._sce_lgtcaygl(this.ctx.upsParams.R1)`生成`encryptR1` 3、将`encryptR1`作为参数请求`mtop.youku.play.ups.appinfo.get`接口 4、接口返回`encryptRServer`与`copyrightKey` 5、通过`window._sce_dlgtqred(this.context.upsParams.R1, n.encryptRServer, n.copyrightKey)`生成解密`key`。 --------------------- 最后就是说下非网页端获取`key`的思路了。 ``` A.prototype.getElectronDRM = function(A, t) { var e, n = { segmentFilter: null }; this.context.config.electronVersion && (e = this.context.config.electronVersion); var r = { r1: "", r2: "", key: "" }; if (A.type == cA.EncryptMethodType.CUSTOMIZED) { var i = A.customizedLicenseData; r.r1 = i.encryptR1 ? i.encryptR1 : this.context.drm.encryptR1, r.r2 = i.encryptRServer, r.key = i.copyrightKey } return e && l.compareVersion(e, "9.0.8") > -1 ? n = { segmentFilter: function(A) { return A.forEach((function(A) { for (var e in r.r1 && r.r2 && r.key && (A.clientR1 = r.r1, A.serverR2 = r.r2, A.drmKey = r.key), t) t.hasOwnProperty(e) && ["standbyH264Url", "videoEnhance", "firstSegUrl", "headers"].indexOf(e) > -1 && (A[e] = t[e]) } )), A } } : window.NPlayerIPC.send("setDrmInfo", r.r1, r.r2, r.key), //关键在这里 { filters: n } } ``` 目前最新版PC客户端使用的`Electron`框架技术,上面是`JS`与播放器交互的代码,可以看到它没有用`JS`生成解密`key`然后传给`播放器`。 显然上面的做法就是防止`key`泄露。 看这句代码: > window.NPlayerIPC.send("setDrmInfo", r.r1, r.r2, r.key) `r2`和`key`就不说了,就是前面说的服务器返回的`encryptRServer`和`copyrightKey`。 主要看`r1`参数,这个`r1`可不是上面讲的那个`R1`,它对应的是`encryptR1`。也就是说它把生成`key`的工作交给播放器去做,而`R1`也不暴露给你,因为防止你通过`JS`去生成`key`,前面说了`JS`生成`key`需要的参数是`R1`。 显然传给播放器`encryptR1`,播放器内部解密出`R1`然后再同`JS`方法生成解密`key`。 而客户端的这个`encryptR1`也不是通过`JS`生成的,因为如果通过`JS`生成,你完全可以拦截到`R1`的值。 获取的代码如下: > window.NPlayerIPC.sendSync("getEncryptR1") 通过播放器的接口来直接获取,就是不给你`R1`,可以说安全工作还是做得挺到位的。 再看看`TV端`的: ![微信截图_20221005184618.png](https://0o0.me/usr/uploads/2022/10/103462169.png) 可以看到这个生成`EncryptR1`的更变态,多了`KeyIndex`参数还有个`var1`,`KeyIndex`实际上应该就是`web01`,在`mtop.youku.play.ups.appinfo.get`接口的参数中有。 从上面看出网页中的`JS`生成`key`的函数,即使你知道`TV端`的`R1`也是无法通用的,`PC端`未知,可自行尝试。 此时,可以说除了逆向客户端播放器没其他办法来获取`key`了。 两个思路: 第一个,这个前提是假如`PC端`的`key`生成方法同网页端,此时,通过`OD`等逆向调试工具跟踪逆向出其`R1`的值,然后再通过`JS`去生成`key`。 第二种思路比较简单暴力:由于我们知道了它的`自研DRM`实际上就是标准的`AES-128-ecb`加密,所以解密肯定也是标准的解密。 查看客户端目录,发现可疑`dll`: 1. libeay32.dll 2. ssleay32.dll 那么很有可能就是调的这两个库去解密的,这是开源的`ssl`库。 此时有两种拦截`key`的方式: 1、自己编译上面的`dll`,在源码里动手脚,找到传`key`的地方进行拦截,编译完后替换客户端目录中的上面两个dll。 2、直接`HOOK`上面`dll`相关函数。 由于我本机没有装`Visual Studio`就懒得去折腾了,这里也就给个思路。 因为以前装过,太吃C盘空间了,虽然很想去玩,但想想还是算了,技术也有限,很久没玩C++了,弄起来费时间。`OD`逆向调试要的技术更高,不是我等玩得来的。 所以就到这吧,完~! 标签: 无