前言 最近在改博客的音乐插件时,发现其是向https://api.i-meto.com/meting/api
查询网易的mp3地址,考虑到它不刷 listid
缓存,以后可能会有自己实现的需求,遂对网易云音乐分析了一番。
云音乐MP3地址分析 分析请求 打开 fiddler
抓一组包,很容易找到包含音乐地址的包。
得到获取地址的接口为https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=
简单分析请求之后,得到一组最简洁的请求格式
这里使用的 vscode
的 REST 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
去百度搜会发现有人写过类似的文章了。
函数很短就整个提过来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 !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
里面简单配置一下,几分钟就搞定了,顺便装一下 setuptools
和 pip
,再也不用担心找不到依赖库了。 python 本身自带一个 crypt
模块,看起来有点简陋,需要重新找一个密码学库。 用 pip search crypt
能搜到一些库,但是担心版本太旧不兼容问题,网上搜了一下,说是现在用的比较多的是 pycryptodome
pip install pycryptodome
安装。
创建文件,写一个带测试的类 1 2 3 4 5 6 7 class MusicDecrypt (unittest.TestCase): def __init__ (self, methodName='runTest' ): super (MusicDecrypt, self).__init__(methodName) if __name__ == "__main__" : unittest.main()
编写 AES
及测试方法 源算法中用 AES
加密了两次,第一次是固定 key
0CoJUm6Qyw8W8jud
,第二次是长度为16的随机串(关于这个后面细说)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 from Crypto.Cipher import AESimport base64 def aes_encrypt (self, s, k, iv="0102030405060708" ): l = 16 - len (s) % 16 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个数据,
GR1dIlooUjX3zmY1
010001
一段很长的数据
根据 function c(a, b, c)
的算法,得知 b,c
构成 KeyPair
,最后加密的是a
,也就是 GR1dIlooUjX3zmY1
,GR1dIlooUjX3zmY1
本身是由 function a(a)
生成的16个字节(char)长度的随机串,曾作为 AES 的 key 使用过。
那么剩下两个对应到算法里就是 e
与 n
了。 其实简单看一下就明白 3
不可能是 e
,当然也可以跑一下,一个这么大的幂,根本就算不出来的。 当然跟入 js
库里面看一下就清楚了。
1 2 3 4 5 6 7 8 9 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import binascii 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的返回值拼接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import urllibimport random randKey = '' def __init__ (self, methodName='runTest' ): super (MusicDecrypt, self).__init__(methodName) self.randKey = '' .join(random.sample( string.ascii_letters + string.digits, 16 )) 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 中调用
1 2 3 4 5 if __name__ == "__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++部分就从简了,毕竟分析过一次实现逻辑了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 #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 () { 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, new CryptoPP::Base64Encoder ( new CryptoPP::StringSink (cipher_text),false ), CryptoPP::BlockPaddingSchemeDef::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" ) ); 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; }; const std::string MusicDecrypt::original_seq = u8"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ;const std::string MusicDecrypt::default_e = u8"0x010001" ;const std::string MusicDecrypt::default_m = u8"0x00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" ;}
编写网络模块 cpr
的使用就比较简单了,和 python
的 urllib
有的一比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #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; } }; }
编写测试 需要注意,测试和上面的编写实际上是同步进行的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #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
1 2 3 4 5 6 7 8 9 #pragma once #define _TEST_MODULE 1 #ifdef _DEBUG #define DEBUG_EXPR(x) do{x}while(0) #else #define DEBUG_EXPR(x) #endif
补充
该版本代码仅仅是个Demo
,可以考虑 enum
+ map
支持一下获取歌词图片啥的,再引入json解析库做个下载器啥的。
趟坑 cpp-netlib 开始用的 cpp-netlib
,毕竟支持同时客户端服务端,直到我的膝盖中了一箭。
vcpkg
默认当前安装的是 boost 1.7.0 +
版本, 当前的 cpp-netlib 1.3.0
只能用 boost 1.6.9
以下版本,具体哪个版本我也没翻到。
Error:
error C3536: "delegate" 未初始化
– bugfix
error: no member named 'get_io_service'
– issues_4636
总结 本文算是个大杂烩,从各方面讲了一通,算是为以后有移到php需求的时候打个底。 :D