美文网首页爬虫工具
解析网易云音乐的加密方式

解析网易云音乐的加密方式

作者: 努力努力再努力_y | 来源:发表于2019-07-22 17:13 被阅读0次
    写在前面

    前段时间使用VSCode时,看到一堆神奇的插件,其中包括VSC Netease Music,经研究发现该作者参考的是node版本接口实现,本着对技术的渴望,研究一波加密方式,并复写Java版本随手记录一下。

    准备工作
    • 环境:Win10
    • 工具:Fiddler 4,Chrome浏览器
    以音乐歌曲评论数据获取接口为例,进行分析

    一、查找API

    打开歌曲详情页面,F12打开DevTools工具页面,找到接口如下


    查看该请求的详细内容,request Header如下所示:


    我们分析一下这个请求,先看它的url,请求多次之后发现R_SO_4_在请求评论时是固定的,1377544581则是歌曲的id,url还有一个参数csrf_token,看这个名字像是防止跨站攻击的,但是它一直是空的。然后就是POST里面的参数params和encSecKey,这两个参数是关键,接下来我们要重点分析它。

    从当前的值可以看出,这是加密后的内容,毫无疑问肯定是通过js加密的。而且,我们可以从上图的Initiator可以看出这两个参数是通过core.js这个js文件算出来。因此,我们下一步计划就是分析core.js的内容。

    二、分析Core.js

    文件另存下来后查看是压缩过的,需要格式化后大概四万多行。但是没关系,我们需要的只是部分数据。
    在这个js文件中搜索params和encSecKey,可以找到这里


    问题就变成得到这个bXY6S,它是由window.asrsea这个函数得到的,可以看到有4个参数,如果研究每个参数肯定是痛苦的,也没有必要,可以先把它们输出来看一下,使用Fiddler线上调试js,原理就是将本地的js替换线上加载的js文件,这样就可以调试输出这4个参数值。本地js文件加上几行代码,如图所示:


    打开Fiddler,找到autoResponder,添加Rule,导入本地js文件最终页面如下图所示:


    然后就成功找到了i0x,如图



    可以根据不同的歌曲和翻译页数多试几次,可以发现rid就是R_SO_4_加上歌曲的id(其实这个参数也是可以没有的),offset就是(评论页数-1) * 20,total在第一页是true,其余是false。
    按这样的方式可以得到其余三个参数


    • 010001
    • 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
    • 0CoJUm6Qyw8W8jud

    三、加密方式分析

    现在我们只要知道函数window.asrsea如何处理的就可以了,定位到这个函数发现它其实是一个叫d的函数


    (1)params分析:
    1. 函数a可以看出返回的是一个长度为16的随机字符串

    2. 函数b是一个AES加密,经过了两次加密,第一次对d也就是那个json加密,key是第四个参数,第二次对第一次加密结果进行加密,key是i。在b函数中可以看到密钥偏移量iv是0102030405060708,模式是CBC

    (2)encSecKey分析

    你会发现在我们这种情境下,这里传入c的三个参数i是16个F,e是第二个参数,f是第三个参数,全部是固定的值,那么无论歌曲id或评论页数如何变化,这个encSecKey都不随之发生变化,所以这个encSecKey对我们来说就是个常量,抄一个下来就是可以使用的。
    秉承着完美解决,我实在不想写死,接着再分析

    这个参数通过RSA算法生成,其中i作为message,e,f是加密时用到的参数。

    在这里稍微解释一下RSA算法,算法选取2个很大的质数p,q,得到它们的乘积n,然后选取e,d满足e*d = 1 mod (p-1)(q-1),加密时text=(msge)%n,解密时msg=(textd)%n,在这个函数里e就相当于算法里的e,f相当于算法里的n。
    还有一点需要注意,encSecKey是一个完全由16进制数组成,但是在加密模块中一般都是返回byte流,然后通过base64编码(长度是原来的4/3),而像这种的应该是把byte流通过16进制表示出来(长度是原来的2倍)。

    这里面有个小坑(当时懵逼很久)


    通过代码可以看出,c数组是b字符串转成的数组,然后在for循环中,c数组从左到右是从低位加到高位的,比如123456,1是加在低位,6是加在高位,这和平常有些不一样。

    即需要先将加密的消息翻转,再进行加密

    四、最终实现(Java版)

    核心加密如下
        /**
         * AES加密
         * 此处使用AES-128-CBC加密模式,key需要为16位
         *
         * @param content 加密内容
         * @param sKey    偏移量
         * @return
         */
        public static String aesEncrypt(String content, String sKey) throws Exception {
            byte[] encryptedBytes;
            byte[] byteContent = content.getBytes("UTF-8");
            // 获取cipher对象,getInstance("算法/工作模式/填充模式")
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            // 采用AES方式将密码转化成密钥
            SecretKeySpec secretKeySpec = new SecretKeySpec(sKey.getBytes(), "AES");
            // 初始化偏移量
            IvParameterSpec iv = new IvParameterSpec(ivParameter.getBytes());
            // cipher对象初始化 init(“加密/解密,密钥,偏移量”)
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
            // 数据处理
            encryptedBytes = cipher.doFinal(byteContent);
            // 此处使用BASE64做转码功能,同时能起到2次加密的作用
            return new String(Base64Utils.encode(encryptedBytes), "UTF-8");
        }
    
        /**
         * RSA 加密
         *
         * @param secKey 随机16位字符串
         * @return
         */
        public static String rsaEncrypt(String secKey) {
            // 需要先将加密的消息翻转,再进行加密
            secKey = new StringBuffer(secKey).reverse().toString();
            // 转十六进制字符串
            String secKeyHex = stringToHexString(secKey);
            // 指定基数的数值字符串转换为BigInteger表示形式
            BigInteger biText = new BigInteger(secKeyHex, 16);
            BigInteger biEx = new BigInteger(pubKey, 16);
            BigInteger biMod = new BigInteger(modulus, 16);
            // 次方并求余(biText^biEx mod biMod is ?)
            BigInteger bigInteger = biText.modPow(biEx, biMod);
            return zfill(bigInteger.toString(16), 256);
        }
    

    五、这哥们更会玩

    总是有那么些大牛平时没事干就喜欢琢磨这些事情,通过破解这些程序来证明自己。还有的是为了喜欢的女孩,比如下面这位:(这是一个悲伤的故事!)


    这位同学的代码分析能力很强,他提供的方法属于另辟蹊径。其他的大牛都是通过分析js加密算法,然后自己写出来,实现对传输参数的加密,大部分都是使用Python,这位作者使用的是纯Java写的加密程序。通过java内置的ScriptEngine调用js引擎,实现对js中的方法调用,这个我也是第一次听说,在JavaSE6中提供的功能。什么是ScriptEngine,请看博客:https://www.cnblogs.com/zouhao/p/3644788.html或者
    http://blog.csdn.net/u012660667/article/details/49821811
    作者通过对core.js的核心文件分析,将两万行的代码删减成一千多行,不得不说作者很有耐心啊!最后就简单了,直接在java代码中调用js的方法就可以对参数进行加密了。

      public class JSSecret {
    
        private static Invocable inv;
        public static final String encText = "encText";
        public static final String encSecKey = "encSecKey";
    
        /**
         * 从本地加载修改后的 js 文件到 scriptEngine
         */
        static {
            try {
                // 文件读取
                String pathResources = ResourceUtils.getURL("classpath:").getPath();
                pathResources = pathResources + "file/core.js";
                pathResources = pathResources.substring(1, pathResources.length());
                Path path = Paths.get(pathResources);
                byte[] bytes = Files.readAllBytes(path);
                String js = new String(bytes);
                ScriptEngineManager factory = new ScriptEngineManager();
                // 查找并创建一个ScriptEngine
                ScriptEngine engine = factory.getEngineByName("JavaScript");
                // js代码放入到eval中当做参数就可以执行相应的js代码
                engine.eval(js);
                // 调用js中的方法
                inv = (Invocable) engine;
                System.out.println("Init completed");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static ScriptObjectMirror get_params(String paras) throws Exception {
            ScriptObjectMirror so = (ScriptObjectMirror) inv.invokeFunction("myFunc", paras);
            return so;
        }
    
        public static HashMap<String, String> getData(String paras) {
            try {
                ScriptObjectMirror so = (ScriptObjectMirror) inv.invokeFunction("myFunc", paras);
    
                Set<Map.Entry<String, Object>> entries = so.entrySet();
                for (Map.Entry<String, Object> map : entries) {
                    System.out.println("key:" + map.getKey());
                    System.out.println("value:" + map.getValue());
                }
    
                HashMap<String, String> data = new HashMap<>();
                data.put("params", so.get(JSSecret.encText).toString());
                data.put("encSecKey", so.get(JSSecret.encSecKey).toString());
                return data;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
    

    详细分析见:https://blog.csdn.net/qq_31673689/article/details/78615448


    写在最后

    Github已有较为完善的node版本及各种分析文章,但分析尝试并不是很顺利,还是要多学点东西,生命不息,折腾不止!!!

    参考文章

    ever_hu
    平胸小仙女
    我是你妹她哥
    Mi_Chong
    darknessomi
    TheAlgorithms

    参考接口

    node版本(史上最全)

    最后奉上源码:

    如果此项目对你有所帮助,麻烦给****Star****吧。感谢!!!(本项目仅供学习参考,侵权删)

    相关文章

      网友评论

        本文标题:解析网易云音乐的加密方式

        本文链接:https://www.haomeiwen.com/subject/fngwlctx.html