网易云音乐 Web 登录算法解密过程分析以及实现
- 测试环境 Ubuntu 18.10 x64
- 软件环境 Chrome 71.0.3578.98
近期需要对网易云音乐的登录和评论点赞功能做一些工具类的软件,记录一下网易云音乐API的解密过程和实现方法
登录分析
首先使用浏览器打开 网易云音乐
抓包
打开网易云音乐后 点击右上角登录 -> 使用手机号登录,此时按下F12,打开开发者工具,并切换到 Network
输入自己的网易云音乐账号,得到登录的包(为了方便测试,密码可以输错)
如图,登录时向
提交了两个字段 params、encSecKey
猜测 encSecKey 为 params 的解密密匙
寻找 encSecKey 算法
在开发者工具切换到 Sources 右键 Top 后点击 Search in all files 随后输入 encSecKey
搜索结果中,只有一个js包含了encSecKey
https://s3.music.126.net/web/s/core_db15e45072c12697d6fed065e7a71f62.js
静态分析算法
从搜索结果中,定位到88行和90行,将其格式化后
88行: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!
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
} ();
90行: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(function() {
var c9h = NEJ.P,
er2x = c9h("nej.g"),
v9m = c9h("nej.j"),
j9a = c9h("nej.u"),
RQ5V = c9h("nm.x.ek"),
l9c = c9h("nm.x");
if (v9m.bl0x.redefine) return;
window.GEnc = true;
var brK2x = function(cxx7q) {
var m9d = [];
j9a.be0x(cxx7q,
function(cxv7o) {
m9d.push(RQ5V.emj[cxv7o])
});
return m9d.join("")
};
var cxt7m = v9m.bl0x;
v9m.bl0x = function(Y0x, e9f) {
var i9b = {},
e9f = NEJ.X({},
e9f),
mt4x = Y0x.indexOf("?");
if (window.GEnc && /(^|\.com)\/api/.test(Y0x) && !(e9f.headers && e9f.headers[er2x.Ae0x] == er2x.Hb2x) && !e9f.noEnc) {
if (mt4x != -1) {
i9b = j9a.hg3x(Y0x.substring(mt4x + 1));
Y0x = Y0x.substring(0, mt4x)
}
if (e9f.query) {
i9b = NEJ.X(i9b, j9a.fR2x(e9f.query) ? j9a.hg3x(e9f.query) : e9f.query)
}
if (e9f.data) {
i9b = NEJ.X(i9b, j9a.fR2x(e9f.data) ? j9a.hg3x(e9f.data) : e9f.data)
}
i9b["csrf_token"] = v9m.gR2x("__csrf");
Y0x = Y0x.replace("api", "weapi");
e9f.method = "post";
delete e9f.query;
var bVs1x = window.asrsea(JSON.stringify(i9b), brK2x(["流泪", "强"]), brK2x(RQ5V.md), brK2x(["爱心", "女孩", "惊恐", "大笑"]));
e9f.data = j9a.cB0x({
params: bVs1x.encText,
encSecKey: bVs1x.encSecKey
})
}
cxt7m(Y0x, e9f)
};
v9m.bl0x.redefine = true
})();
静态分析后,90行处的代码只是进行了简单的赋值,params由bVs1x.encText而来,encSecKey由bVs1x.encSecKey而来,在88行处,可以看到加密的方式算法和过程,因此88行作为我们的突破口
根据 encSecKey 为关键字寻找,发现在 function d 对其进行了赋值
function d:1
2
3
4
5
6
7
8function 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 a,b,c
逐一静态分析各个 function
function a:1
2
3
4
5
6
7
8function 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 a 随机生成一个长度为传入 int 值的字符串
function b:1
2
3
4
5
6
7
8
9
10function 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()
}
从调用的关键字来看,这是一个有关AES的对称加密算法,模式为CBC,iv(向量)为d,也就是“0102030405060708”
function c:1
2
3
4
5
6function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b, "", c),
e = encryptedString(d, a)
}
静态分析来看,这是一个有关于 RSA 的参数,接下来我们使用断点动态分析加密函数
断点调试算法
在下图几个地方打上断点
打上了断点以后我们再一次点击登录
看到js运行到断点时,停止了,第个断点处,传入了参数 a:16 生成一个16位的字符串
运行到第二个断点时,传入参数 c : “fAY0MsZsT3WA0eXg” 这是AES的加密密匙
运行到第三个断点时,传入参数 b : “0CoJUm6Qyw8W8jud” 和一串json a(登录信息)
运行到第四个断点时,传入参数 b : “fAY0MsZsT3WA0eXg”(AES密匙)和一串Base64字符串:1
2a : "2pi8lzed+/YUH63EXHn6d9IwIDeruHJmzW0rjEtUqUh00G+8ElR2XnxRvDOg+eWHcO06ukSrADFNFazIhKGNMJ/fYDsS2bTep4lkTxH42fLDCPm/v+Z/7P3guz9HcZWwNQ1Y1qb2pUNt1nmGpR89n+MiPQb3gbOscmJPXf8V60kDuLF0u0dwHkeqD5hIhgqRa3qZJCeYZcU3GrAEABFMe/cNhfcrUHpGgTOB4JJw=="
(为了保护信息,已经对这里的字符串a进行了删减)
运行到第五个断点时,RSA的加密函数,传入了:
1 |
|
第一次断点结束后,我们再点击一次登录,再次进行一次断点调试,通过第二次断点调试,发现第三次断点时的传入参数 b 和 第五个断点时的参数 b c 是不变的,分别为
1 |
|
动静结合分析算法
根据 function d :1
2
3
4
5
6
7
8function 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
}
在 var h = {}, 处断点 得到d,e,f,g的值
可以看到1
2
3
4d = json a(登录信息)
e = "010001"
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
将参数放入 function d 中,首先是使用第一个密匙”0CoJUm6Qyw8W8jud”对 JSON 登录信息的字符串进行了一次 AES 加密,随后得到的结果,在使用通过 function a 生成的16位字符作为AES的加密密匙对第一次AES加密的json的登录信息结果,再次加密,而得出的加密结果就作为 encText,也就是整个加密的流程为:
验证算法
在摸清了整个算法流程以后,我们使用AES在线加解密,将加密结果解密,看是否能得到最初的json登录信息,只需要params的值
- 使用生成的密匙进行第一次解密 params
- 第一次解密后,我们仍然得到了一串Base64字符串,别着急,在使用默认的密匙”0CoJUm6Qyw8W8jud”对结果进行再次解密
此时已经得到了登录信息的 json 字符串 至此,整个网易云登录的算法分析到此结束
Python 实现登录
- Python 3.6.7
- pip 18.1
安装第三方库
1 |
|
编写加密函数
首先引用 AES 的默认密匙和 RSA 的 MODULUS 和 Pub_Key
1 |
|
生成随机密匙
1 |
|
AES 加密
1 |
|
RSA 加密
RSA 加密采用非常规填充方式,既不是PKCS1也不是PKCS1_OAEP,网易的做法是直接向前补0
这样加密出来的密文有个特点:加密过程没有随机因素,明文多次加密后得到的密文是相同的
算法选取2个很大的质数p,q,得到它们的乘积n,然后选取e,d满足e*d = 1 mod (p-1)(q-1)
因此 RSA 的加密算法应该为
1 |
|
组合登录
1 |
|
这样一看是没有问题了,可是当Run时,返回的登录结果为
{“code”:-460,”msg”:”Cheating”}
是因为缺少了 Headers ,因此添加一个 Headers 并修改为
1 |
|
此时,当账户密码正确时就能返回登录账号的信息了
完整代码
1 |
|