Error
本文由于浏览器兼容性问题,会导致显示不全,推荐在IE内核浏览器上阅读本文,后续会进行修复。
前言
最近在改博客的音乐插件时,发现其是向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
去百度搜会发现有人写过类似的文章了。
函数很短就整个提过来了
!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
安装。
创建文件,写一个带测试的类
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个数据,
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
库里面看一下就清楚了。
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
的使用就比较简单了,和 python
的 urllib
有的一比。
#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
补充
该版本代码仅仅是个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" 未初始化
-- bugfixerror: no member named 'get_io_service'
-- issues_4636
总结
本文算是个大杂烩,从各方面乱七八糟的讲了一通,算是为以后有移到php需求的时候打个底。 :D
github
本文链接:https://www.holdheart.com/archives/language_syntax/139.html
本文为 faTe 原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
0 条评论