在分析网易云音乐网页端的请求时,注意到很多地方都用到了 paramsencSecKey 两个参数,但是这两个参数是怎么来的呢?

Form Data


比如我想获取一个用户的所有歌单,就找到了一个 playlist 请求,方法为 POST,其带了一个 URL 参数 csrf_token 不过是空的,只有在登录账号之后才会使用该参数,所以直接无视它就好了,重点还是下面 Form Data 中的 paramsencSecKey

note-2020-05-20-11-21-29

点开 Initiator,可以看到该请求的发起源均为一个名为 core_c2b706c894803fe234ab747cbb6cd4c4.js 的 JS 文件。

note-2020-05-20-11-24-53

打开它,在里面搜索一下 encSecKey,找到了一个 BYf9W 变量和 window.asrsea 函数,接下来就是分析这个函数的 4 个参数了。

note-2020-05-20-11-27-17

打上断点然后刷新页面,因为这个函数被调用了多次,所以我们要找到它发起 playlist 请求时的参数。

几次调试后,找到了该参数,发现第一个参数中的 i2x 值如图,其中的 uid 正是要获取歌单的用户 ID,其它参数之后再分析。

note-2020-05-20-11-31-15

继续看 window.asrsea 的后三个参数,发现他们都是常量。

note-2020-05-20-11-35-34

将鼠标放在 window.asrsea 函数名上,点击跳转到了一个 d 函数,window.asrsea 的本质就是这个 d 函数。

note-2020-05-20-11-37-30

观察一下,d 函数又分别调用了 a b c 三个函数,那就一个一个来看。

note-2020-05-20-11-40-28

首先是 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

note-2020-05-20-11-42-05

再看 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)

note-2020-05-20-11-53-25

最后看 c 函数,这个函数的作用为 RSA 加密。我在上一篇博客里详细说明了 RSA 加密的原理。

首先公钥的 exponent(也就是俗称的 e)为 bqR1x(["流泪", "强"]) 的值,modulus(也就是俗称的 N)为 bqR1x(QM5R.md),这里均为 16 进制的数。

note-2020-05-20-12-04-48

而在加密函数 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

整个 paramsencSecKey 的生成过程结束,经过测试,最开始我们获取到的参数 i2x 中的值,limit 为返回的数量限制,offset 是翻页的偏移量。如果只是像我一样获取所有歌单的话,其实 JSON 只要两项就够了。

{"uid":"16224530","limit":"1000"}

最后还有一个小问题,我们可以看到整个参数的变化都是依靠 a 函数的随机 16 位字符串的,所以理论上我们用同一个参数反复发起请求应该也没有问题,就看网易云的服务器会不会 ban 了。

If you think my article is useful to you, please feel free to appreciate