在分析网易云音乐网页端的请求时,注意到很多地方都用到了 params
和 encSecKey
两个参数,但是这两个参数是怎么来的呢?
比如我想获取一个用户的所有歌单,就找到了一个 playlist
请求,方法为 POST
,其带了一个 URL 参数 csrf_token
不过是空的,只有在登录账号之后才会使用该参数,所以直接无视它就好了,重点还是下面 Form Data
中的 params
和 encSecKey
。
点开 Initiator
,可以看到该请求的发起源均为一个名为 core_c2b706c894803fe234ab747cbb6cd4c4.js
的 JS 文件。
打开它,在里面搜索一下 encSecKey
,找到了一个 BYf9W
变量和 window.asrsea
函数,接下来就是分析这个函数的 4 个参数了。
打上断点然后刷新页面,因为这个函数被调用了多次,所以我们要找到它发起 playlist
请求时的参数。
几次调试后,找到了该参数,发现第一个参数中的 i2x
值如图,其中的 uid
正是要获取歌单的用户 ID,其它参数之后再分析。
继续看 window.asrsea
的后三个参数,发现他们都是常量。
将鼠标放在 window.asrsea
函数名上,点击跳转到了一个 d
函数,window.asrsea
的本质就是这个 d
函数。
观察一下,d
函数又分别调用了 a
b
c
三个函数,那就一个一个来看。
首先是 a
函数,它调用时的参数为一个固定值 16
,所以这个函数的作用就是生成了一个 16 位的随机字符串。
转换成 Python,就是这样了:
def a(a):
b = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
c = ''
for _ in range(a):
e = random.random()*len(b)
e = math.floor(e)
c += b[e]
return c
再看 b
函数,可以看到 b
函数的作用是 AES 加密,模式为 CBC,偏移量是一个固定值 0102030405060708
。
而它在 d
函数中一共调用了两次 b
函数,第一次是将包含 UID 的参数 d
作为 message,常量 g
(也就是 bqR1x(["爱心", "女孩", "惊恐", "大笑"])
)作为 key 来加密。之后,再把第一次的加密结果作为 message,而把 a
函数返回的 16 位随机字符串作为 key 进行第二次加密,两次加密后的结果就已经是我们需要的参数 params
。
所以用 Python 来实现一下 AES 加密:
import base64
from Crypto.Cipher import AES
def to_16(key):
while len(key) % 16 != 0:
key += '\0'
return str.encode(key)
def AES_encrypt(text, key, iv):
bs = AES.block_size
def pad2(s): return s + (bs - len(s) % bs) * chr(bs - len(s) % bs)
encryptor = AES.new(to_16(key), AES.MODE_CBC, to_16(iv))
encrypt_aes = encryptor.encrypt(str.encode(pad2(text)))
encrypt_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
return encrypt_text
def b(a, b):
d = '0102030405060708'
return AES_encrypt(a, b, d)
最后看 c
函数,这个函数的作用为 RSA 加密。我在上一篇博客里详细说明了 RSA 加密的原理。
首先公钥的 exponent(也就是俗称的 e)为 bqR1x(["流泪", "强"])
的值,modulus(也就是俗称的 N)为 bqR1x(QM5R.md)
,这里均为 16 进制的数。
而在加密函数 encryptedString
中还需要注意,存储 message 的字符串数组是被倒序处理的,所以我们还要把明文倒序再进行加密。
于是 c
函数就是这样了:
import codecs
def c(text, pubKey, modulus):
text = text[::-1]
rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'),
16) ** int(pubKey, 16) % int(modulus, 16)
return format(rs, 'x').zfill(256)
之后就是用 d
函数把上面的东西串起来:
def d(d, e, f, g):
i = a(16)
encText = b(d, g)
encText = b(encText, i)
encSecKey = c(i, e, f)
return encText, encSecKey
整个 params
和 encSecKey
的生成过程结束,经过测试,最开始我们获取到的参数 i2x
中的值,limit
为返回的数量限制,offset
是翻页的偏移量。如果只是像我一样获取所有歌单的话,其实 JSON 只要两项就够了。
{"uid":"16224530","limit":"1000"}
最后还有一个小问题,我们可以看到整个参数的变化都是依靠 a
函数的随机 16 位字符串的,所以理论上我们用同一个参数反复发起请求应该也没有问题,就看网易云的服务器会不会 ban 了。