Error

本文由于浏览器兼容性问题,会导致显示不全,推荐在IE内核浏览器上阅读本文,后续会进行修复。

前言

最近在改博客的音乐插件时,发现其是向
https://api.i-meto.com/meting/api
查询网易的mp3地址,考虑到它不刷 listid 缓存,以后可能会有自己实现的需求,遂对网易云音乐分析了一番。

云音乐MP3地址分析

分析请求

打开 fiddler 抓一组包,很容易找到包含音乐地址的包。

0.png

得到获取地址的接口为
https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=

简单分析请求之后,得到一组最简洁的请求格式

1.png

这里使用的 vscodeREST Client 插件。

观察POST数据包的 body 部分,疑似加密,前半部分是典型的 urlencode + Base64 (%30%30)解开是二进制,后半部分直接是二进制。

encSecKey将成为一个突破口。

分析加解密

打开浏览器的 开发人员工具 (F12), 全局搜索(ctrl + shift + f) encSecKey,找到全部与之相关的js,

format 一下,全部下断点,实际上正常应该分析一下上下文,但是总共就 3,4 处,直接全断最方便了。

刷新网页,会断到 core_xxxxxxxxxxxxx.js 中名为 function d(d, e, f, g) 的函数,虽然名称被混淆了,
但是这里确实是加密。
当然为了验证是否是前面那个接口用到的加密,还需要往后跟到发包函数,多看几组包就能确定了。
这个过程我已经做了,由于篇幅问题,不再多说。

拓展一下,不从encSecKey分析也是可以的,搜 csrf_token 下断点,往上回溯几层,
你会跟到 window.asrsea = d,实际上拿 window.asrsea 去百度搜会发现有人写过类似的文章了。

函数很短就整个提过来了

!function() {
    function a(a) {
        var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
        for (d = 0; a > d; d += 1)
            e = Math.random() * b.length,
            e = Math.floor(e),
            c += b.charAt(e);
        return c
    }
    function b(a, b) {
        var c = CryptoJS.enc.Utf8.parse(b)
          , d = CryptoJS.enc.Utf8.parse("0102030405060708")
          , e = CryptoJS.enc.Utf8.parse(a)
          , f = CryptoJS.AES.encrypt(e, c, {
            iv: d,
            mode: CryptoJS.mode.CBC
        });
        return f.toString()
    }
    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }
    function d(d, e, f, g) {
        var h = {}
          , i = a(16);
        return h.encText = b(d, g),
        h.encText = b(h.encText, i),
        h.encSecKey = c(i, e, f),
        h
    }
    function e(a, b, d, e) {
        var f = {};
        return f.encText = c(a + e, b, d),
        f
    }
    window.asrsea = d,
    window.ecnonasr = e
}();

逐个函数看

  • function a(a),看到 a-zA-Z0-9 加个循环里面用 rand,非常典型的随机串生成算法,根据循环条件得知参数为串长度。
  • function b(a, b),它都说了是 AES 了,加密模式是 CBC,怕它不老实,不使用标准实现,随手找个在线加密网站,可以进行验证,测试一遍后能得到填充模式为 PKCS_PADDING。 参数分别是源跟密钥。
  • function c(a, b, c)RSA这个老实说不好在线验证,先放着,后面用代码验证。
  • function d(d, e, f, g) ,主加密流程,多观察几遍就会发现除了 d ,都是固定值,拷贝下来即可,而d中是个 json明文。
  • function e(a, b, d, e) ,看上去似乎是类似流程,不过跟本文无关。

有意思的是 window.asrsea 的后半部分倒过来是 aesrsa,小彩蛋?
rsanonce 对应 RSA Nonce 加密?

从js到python

最近在学习 rust,本来是准备练下手的,只可惜学艺不精,处处跟编译器对着干,就是那种明明知道该怎么写,但是用你就偏偏搞不定的感觉。

写这种小工具,还是python 最快了。
装个基础环境,vscode里面简单配置一下,几分钟就搞定了,顺便装一下 setuptoolspip,再也不用担心找不到依赖库了。
python 本身自带一个 crypt 模块,看起来有点简陋,需要重新找一个密码学库。
pip search crypt 能搜到一些库,但是担心版本太旧不兼容问题,网上搜了一下,说是现在用的比较多的是 pycryptodome

pip install pycryptodome 安装。

创建文件,写一个带测试的类

class MusicDecrypt(unittest.TestCase):
    def __init__(self, methodName='runTest'):
        super(MusicDecrypt, self).__init__(methodName)

if __name__ == "__main__":
    unittest.main()

编写 AES 及测试方法

源算法中用 AES 加密了两次,第一次是固定 key 0CoJUm6Qyw8W8jud,第二次是长度为16的随机串(关于这个后面细说)。

##
# 加到最上面
##
from Crypto.Cipher import AES
import base64

    ##
    # 加到 MusicDecrypt 里
    ##
    #
    # AES
    # CBC
    # pkcs5padding
    # 128bit
    #
    def aes_encrypt(self, s, k, iv="0102030405060708"):
        l = 16 - len(s) % 16
        # pkcs5padding
        s += + l * chr(l)
        self.assertEqual(len(s) % 16, 0)

        return base64.b64encode(AES.new(k.encode("utf-8"), AES.MODE_CBC, iv.encode("utf-8")).encrypt(s.encode("utf-8"))).decode()

    def test_aes_encrypt(self):

        enc = self.aes_encrypt(
            s='{"ids":"[28949499]","level":"standard","encodeType":"aac","csrf_token":""}',
            k="0CoJUm6Qyw8W8jud",
        )
        self.assertEqual(
            enc,
            "g1N6YybxFdV98P/fGY0407hwjh0evx5kPtxXR0nPd/WPPFsi9Lf67vFfjUnM3MDahHpqkyZMS+9goaszbHF+i1fIufNBu+8BbSvBCJSVfEU="
        )
        self.assertEqual(
            self.aes_encrypt(
                s=enc,
                k="GR1dIlooUjX3zmY1",
            ),
            "9R0jh8yE6/JTTwoH4ujCacPMOwJdbXk39BlG3ODTNe+rHMLAOSHDlp/Mza7+15lOi8bvPMtLnA6gCOujDj5iuVBJF2a2DJVkNLtrTtgl+AXpsR5hSh0+EOfuads7lq41B9EpYKktwB72zOy+kafalQ=="
        )

这个是之前调试js时存下来的数据,通过对这组数据测试,就能写出正确的算法, RSA 同理。
只需要将js中的b忠实还原就行了。

编写 RSA 及测试方法

显然js中用的 RSA 并非是平常的用法,最少传进去不是个标准的公钥。

网上搜了一下 RSA 的算法解释。

得知 RSA 的加密算法核心是
c = (p ^ e) mod n
方程中p表示源串,c表示加密串,n就是文件上面说的Modules,e则为Exponent,(n, e)表示PublicKey。

通过调试js能得到3个数据,

  1. GR1dIlooUjX3zmY1
  2. 010001
  3. 一段很长的数据

根据 function c(a, b, c) 的算法,得知 b,c 构成 KeyPair,最后加密的是a,也就是 GR1dIlooUjX3zmY1GR1dIlooUjX3zmY1本身是由 function a(a) 生成的16个字节(char)长度的随机串,曾作为 AES 的 key 使用过。

那么剩下两个对应到算法里就是 en 了。
其实简单看一下就明白 [3]很长的数据 不可能是 e,当然也可以跑一下,一个这么大的幂,根本就算不出来的。
当然跟入 js 库里面看一下就清楚了。

function RSAKeyPair(a, b, c) {
    this.e = biFromHex(a),
    this.d = biFromHex(b),
    this.m = biFromHex(c),
    this.chunkSize = 2 * biHighIndex(this.m),
    this.radix = 16,
    this.barrett = new BarrettMu(this.m)
}
function encryptedString(a, b){// 省略}

e 对应 010001, m 对应很长的串。 encryptedString 就是个大数算法,在运用公式前需要将 GR1dIlooUjX3zmY1 翻转一下。

老实说,原js里面似乎并没有先翻转,整个算法研究了一下与普通的RSA算法一样,
疑似以某种等价的方式置入了 biToHex 中,具体 biToHex 就没有分析为什么会等价了,有兴趣可以自行研究。

接下来就是写验证代码了,由于 python 原生支持大数,直接将 GR1dIlooUjX3zmY1 翻转并转成大数就行了,

这个在 python 里面只需要一句代码,如果不是在 python 中还会涉及大数运算问题,好在 python 原生就支持这个。

int(binascii.hexlify(p[::-1].encode("utf-8")), 16)

[::-1] 翻转,binascii.hexlify 转为 bytes 再通过 int(16代表源数据是16进制) 转为大数即可。

.b'1Ymz3XjUoolId1RG'

b'31596d7a33586a556f6f6c4964315247'
0X31596D7A33586A556F6F6C4964315247

##
# 加到最上面
##
import binascii

    ##
    # 加到 MusicDecrypt 里
    ##
    #
    # reverse p : p = p[::-1]
    # [p^e] % m
    #
    def rsa_encrypt(self, p, e, m):
        return format(int(binascii.hexlify(p[::-1].encode("utf-8")), 16)**int(e, 16) % int(m, 16), 'x').zfill(256)

    def test_rsa_encrypt(self):
        self.assertEqual(
            self.rsa_encrypt(
                p="GR1dIlooUjX3zmY1",
                e="010001",
                m="00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
            ),
            "abc2e11cd93268085180aace6208c0caed01b7c2af641999a79adf362fb778a3fba5117f9c06541a5620d4dccd628085b53c1b22d971068a458e1ac16d831860ab2f1da4c7c8342f8bb815c6ab6c6c335cc797a4273124ff4846c9d58b0015691f933323fe080b8d026836880af99e918c7ace1813356b8bc327a52dcc24050a"
        )

构造body

将两次AES之后 的结果进行 urlencode ,然后与RSA的返回值拼接。

## 加在最前面
import urllib
import random

    randKey = ''

    ## 替换 __init__
    def  __init__(self, methodName='runTest'):
        super(MusicDecrypt, self).__init__(methodName)
        self.randKey = ''.join(random.sample(
            string.ascii_letters + string.digits, 16))

    ## 加入 MusicDecrypt
    def packet_body(self, id):
        enc_text = self.aes_encrypt(
            s='{"ids":"[' + id + ']","level":"standard","encodeType":"mp3","csrf_token":""}',
            k="0CoJUm6Qyw8W8jud",
        )
        enc_text = self.aes_encrypt(
            s=enc_text,
            k=self.randKey,
        )
        enc_sec_key = self.rsa_encrypt(
            p=self.randKey,
            e="010001",
            m="00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
        )

        return ('params=' + urllib.parse.quote(enc_text) +
                '&encSecKey=' + enc_sec_key)

简单整理之后,在 main 中调用

if __name__ == "__main__":
    # unittest.main()

    print(MusicDecrypt()
          .packet_body('1297747757'))

将得到的数据放入 REST-Client 测试是否正常返回。依据这种思路,还能得到图片和歌词的地址,另外中间的参数也可以配置,
例如 encodeType 可以调整为 aac 等。

python版在这里就算告一段落了。

从js到c++

vcpkg

作为微软官方的c++包管理工具,虽然并不是很好用,但总比没有强。

1.安装 vs,本文用的 vs2019
2.安装 vcpkg,很容易,命令行跑一下就行了

看一下基本命令

  • search
  • install --triplet / install xxx:x64-windows

例如 安装 boost 库, : 后面代表的是 x64 的动态库
vcpkg install boost:x64-windows
可以随便输入一个错误信息,它会提示有哪些版本
vcpkg install boost:x

x64-windows

x64-windows-static
x86-windows
x86-windows-static

windows 上开发,大概也就这几个用的比较多了。

cryptopp 与 cpr

本文的加密库选的 cryptopp,事实上我对c版的 libsodium openssl更加熟悉,只是考虑像 python 那样简洁的实现,所以语言上选的 c++,既然用 c++ 还是统一比较好。

另外本文选了 boost.test 做单元测试,偷懒的话可以不测试。

cryptopp开始用的时候还是有点不习惯。对着手册总算是翻完了。

本文为了行文方便,直接全都塞到了头文件,实际开发中应该分开,避免头文件互相引用导致重定义等问题。

编写加解密逻辑

c++部分就从简了,毕竟分析过一次实现逻辑了。

#pragma once

#include <ctime>
#include <cstdlib>
#include <string>
#include <algorithm>

#include <cryptopp/cryptlib.h>
#include <cryptopp/modes.h>
#include <cryptopp/aes.h>
#include <cryptopp/rsa.h>
#include <cryptopp/randpool.h>
#include <cryptopp/osrng.h>
#include <cryptopp/base64.h>
#include <cryptopp/hex.h>

#include <cpr/util.h>



namespace music{

class MusicDecrypt
{
public:
    
    MusicDecrypt() {
        // 构造 e
        std::srand(static_cast<std::uint32_t>(std::time(nullptr)));
        for (std::uint8_t i = 0; i < 16; i++) {
            rsa_p.push_back(original_seq[rand() % original_seq.length()]);
        }
    };
    ~MusicDecrypt() {};

public:

    auto aes_encrypt(std::string s, std::string k = "0CoJUm6Qyw8W8jud", std::string iv = "0102030405060708") {
        std::string cipher_text;

        CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption cbc_enc(reinterpret_cast<const std::uint8_t*>(k.data()), k.length(), reinterpret_cast<const std::uint8_t*>(iv.data()));

        CryptoPP::StringSource _(s, true, 
        new CryptoPP::StreamTransformationFilter(
            cbc_enc,                                              // cbc 编码
            new CryptoPP::Base64Encoder(                          // base64解码 参数2置为false表示不换行
                new CryptoPP::StringSink(cipher_text),false),     // 不需要 delete, 里面包装了 https://www.cryptopp.com/wiki/StringSource 
            CryptoPP::BlockPaddingSchemeDef::PKCS_PADDING)        // PKCS_PADDING 填充
        );
        return cipher_text;
    }

    auto rsa_encrypt(std::string p, std::string e = "", std::string m = "") {
        if (e.empty())
            e = default_e;
        if (m.empty())
            m = default_m;

        std::string ps = "0x";
        std::reverse(p.begin(), p.end());
        CryptoPP::StringSource _(p, true,
            new CryptoPP::HexEncoder(
                new CryptoPP::StringSink(ps),
                true,
                0,
                "0x"
            ) // HexEncoder
        ); // StringSource

        CryptoPP::Integer ni(m.c_str()), ei(e.c_str()), pi(ps.c_str());

        CryptoPP::RSA::PublicKey pubKey;
        pubKey.Initialize(ni, ei);

        CryptoPP::RSAES_OAEP_SHA_Encryptor pub(pubKey);

        return CryptoPP::IntToString(pubKey.ApplyFunction(pi), 16);
    }

    auto paket_body(std::string id, std::string k = "") {
        auto enc_text = u8"{\"ids\":\"[" +
            id +
            "]\",\"level\":\"standard\",\"encodeType\":\"mp3\",\"csrf_token\":\"\"}";

        if (!k.empty())
            rsa_p = k;

        enc_text = aes_encrypt(enc_text);
        enc_text = aes_encrypt(enc_text, rsa_p);
        auto enc_sec_key = rsa_encrypt(rsa_p);

        return "params=" + cpr::util::urlEncode(enc_text) +
            "&encSecKey=" + enc_sec_key;
    }

private:
    std::string rsa_p;

private:
    // 字符序列
    static const std::string original_seq;
    static const std::string default_e;
    static const std::string default_m;
}; // MusicDecrypt

const std::string MusicDecrypt::original_seq = u8"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const std::string MusicDecrypt::default_e = u8"0x010001";
const std::string MusicDecrypt::default_m = u8"0x00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";

}  // music

编写网络模块

cpr 的使用就比较简单了,和 pythonurllib 有的一比。

#pragma once
#include <cpr/cpr.h>

namespace music {

class MusicJson {
public:
    MusicJson() {}
    ~MusicJson() {}

public:
    auto get_json(std::string packet_body) {
        auto r = cpr::Post(cpr::Url{ "https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=" },
            cpr::Body{ packet_body },
            cpr::Header{ {"Content-Type", "application/x-www-form-urlencoded"},{"Connection", "close"} });

        return  r.text;
    }
}; // MusicJson

}  // music

编写测试

需要注意,测试和上面的编写实际上是同步进行的

#include "config.h"

#if _TEST_MODULE
#define BOOST_TEST_MODULE music_test
#include <boost/test/included/unit_test.hpp>
#include <boost/log/trivial.hpp>

#include "music_decrypt.h"
#include "music_get_json.h"

// 全局测试夹具
class global_fixture
{
public:
    global_fixture() {
        std::cout << "开始准备测试数据------->" << std::endl;
    }
    virtual~global_fixture() {
        std::cout << "清理测试环境<---------" << std::endl;
    }
};

BOOST_GLOBAL_FIXTURE(global_fixture);
BOOST_AUTO_TEST_CASE(TestCase_4_aes_encrypt)
{
    auto enc_text = music::MusicDecrypt().aes_encrypt(u8"{\"ids\":\"[28949499]\",\"level\":\"standard\",\"encodeType\":\"aac\",\"csrf_token\":\"\"}");

    BOOST_LOG_TRIVIAL(info) << "开始测试 aes_encrypt";
    BOOST_TEST(enc_text == "g1N6YybxFdV98P/fGY0407hwjh0evx5kPtxXR0nPd/WPPFsi9Lf67vFfjUnM3MDahHpqkyZMS+9goaszbHF+i1fIufNBu+8BbSvBCJSVfEU=");
    
    BOOST_TEST(music::MusicDecrypt().aes_encrypt(enc_text, u8"GR1dIlooUjX3zmY1") ==
        "9R0jh8yE6/JTTwoH4ujCacPMOwJdbXk39BlG3ODTNe+rHMLAOSHDlp/Mza7+15lOi8bvPMtLnA6gCOujDj5iuVBJF2a2DJVkNLtrTtgl+AXpsR5hSh0+EOfuads7lq41B9EpYKktwB72zOy+kafalQ=="
    );
    BOOST_LOG_TRIVIAL(info) << "结束测试 aes_encrypt";
}

BOOST_AUTO_TEST_CASE(TestCase_4_rsa_encrypt)
{
    BOOST_LOG_TRIVIAL(info) << "开始测试 rsa_encrypt";
    BOOST_TEST(music::MusicDecrypt().rsa_encrypt(u8"GR1dIlooUjX3zmY1") ==
        "abc2e11cd93268085180aace6208c0caed01b7c2af641999a79adf362fb778a3fba5117f9c06541a5620d4dccd628085b53c1b22d971068a458e1ac16d831860ab2f1da4c7c8342f8bb815c6ab6c6c335cc797a4273124ff4846c9d58b0015691f933323fe080b8d026836880af99e918c7ace1813356b8bc327a52dcc24050a"
    );
    BOOST_LOG_TRIVIAL(info) << "结束测试 rsa_encrypt";
}

BOOST_AUTO_TEST_CASE(TestCase_4_paket_body)
{
    BOOST_LOG_TRIVIAL(info) << "开始测试 paket_body";
    BOOST_TEST(music::MusicDecrypt().paket_body(u8"28949499", u8"GR1dIlooUjX3zmY1") == 
        "params=9R0jh8yE6%2fJTTwoH4ujCacPMOwJdbXk39BlG3ODTNe%2brHMLAOSHDlp%2fMza7%2b15lOi8bvPMtLnA6gCOujDj5iuT1EbsfNRzJrzZm6oIqgWhVsWcO%2bhrLFCHDHgyIYGdXOBmuWBRRiDCyMUFJAq3yrVw%3d%3d&encSecKey=abc2e11cd93268085180aace6208c0caed01b7c2af641999a79adf362fb778a3fba5117f9c06541a5620d4dccd628085b53c1b22d971068a458e1ac16d831860ab2f1da4c7c8342f8bb815c6ab6c6c335cc797a4273124ff4846c9d58b0015691f933323fe080b8d026836880af99e918c7ace1813356b8bc327a52dcc24050a"
    );
    BOOST_LOG_TRIVIAL(info) << "结束测试 paket_body";
}

BOOST_AUTO_TEST_CASE(TestCase_4_get_json)
{
    BOOST_LOG_TRIVIAL(info) << "开始测试 get_json";
    BOOST_TEST(music::MusicJson().get_json(
        "params=9R0jh8yE6%2fJTTwoH4ujCacPMOwJdbXk39BlG3ODTNe%2brHMLAOSHDlp%2fMza7%2b15lOi8bvPMtLnA6gCOujDj5iuT1EbsfNRzJrzZm6oIqgWhVsWcO%2bhrLFCHDHgyIYGdXOBmuWBRRiDCyMUFJAq3yrVw%3d%3d&encSecKey=abc2e11cd93268085180aace6208c0caed01b7c2af641999a79adf362fb778a3fba5117f9c06541a5620d4dccd628085b53c1b22d971068a458e1ac16d831860ab2f1da4c7c8342f8bb815c6ab6c6c335cc797a4273124ff4846c9d58b0015691f933323fe080b8d026836880af99e918c7ace1813356b8bc327a52dcc24050a"
    ) != "");
    BOOST_LOG_TRIVIAL(info) << "结束测试 get_json";
}

#endif

main 和 config

#include "config.h"

#if !_TEST_MODULE
#include <iostream>
#include "music_decrypt.h"
#include "music_get_json.h"


int main()
{
    DEBUG_EXPR(std::cout << music::MusicJson().get_json(music::MusicDecrypt().paket_body("31830011")) << std::endl;);
}

#endif // _TEST
#pragma once
#define _TEST_MODULE 1


#ifdef _DEBUG
#define DEBUG_EXPR(x) do{x}while(0)
#else
#define DEBUG_EXPR(x)
#endif

补充

2.png
该版本代码仅仅是个Demo,可以考虑 enum + map 支持一下获取歌词图片啥的,再引入json解析库做个下载器啥的。

趟坑 cpp-netlib

开始用的 cpp-netlib,毕竟支持同时客户端服务端,直到我的膝盖中了一箭。

vcpkg 默认当前安装的是 boost 1.7.0 + 版本,
当前的 cpp-netlib 1.3.0 只能用 boost 1.6.9 以下版本,具体哪个版本我也没翻到。

Error:

  1. error C3536: "delegate" 未初始化 -- bugfix
  2. error: no member named 'get_io_service' -- issues_4636

总结

本文算是个大杂烩,从各方面乱七八糟的讲了一通,算是为以后有移到php需求的时候打个底。 :D


github