解析网易云音乐的加密方式

找到参数的加密方法

首先我们先看评论的加载方式,打开一首音乐的主页,然后打开开发工具的Network选项,点击评论的翻页按钮,可以看到第一个请求就是请求下一页的评论:

comment.png

我们分析一下这个请求,先看它的url,请求多次之后发现R_SO_4_在请求评论时是固定的,483671599则是歌曲的id,url还有一个参数csrf_token,看这个名字像是防止跨站攻击的,但是它一直是空的。然后就是POST里面的参数paramsencSecKey,这两个参数是关键,接下来我们要重点分析它。
我们在开发工具对encSecKey进行全局搜索,发现它只出现在一个文件中:
search.png

点击搜索结果,打开文件并美化后发现,这2处地方,一个只是简单对结果赋值,params通过bAQ8I.encText而来,encSecKey通过bAQ8I.encSecKey而来,而另一个则是有具体函数调用,而这个就是我们的突破口。

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
}

我们先简单分析一下这几个函数,可以看到最后的赋值是在d(d,e,f,g)这个函数内完成的,它首先调用了a(a),可以看出这个函数的作用是生成一个长度为16的随机字符串;然后encText这个参数通过2次调用b(a,b)完成,这个函数的作用是进行AES加密;最后encSecKey是调用c(i,e,f)完成,这个函数的作用是进行RSA加密。
通过上面的代码可以看出,params的生成需要d, g,i这3个参数,前2个是函数传进来的,最后一个是随机生成的。而encSecKey的生成则需要e, f,i这3个参数,前2个是函数传进来的,最后一个和前面相同。
所以理论上我们知道了d,e,f,g这4个参数就可以构造请求了,我们在d函数加断点,继续点击下一页,可以在断点处调试,看到传入的参数:

param.png

试了几次后我们发现,无论是同一会话的新请求,还是新会话中的请求,e,f,g的值都是不变的,所以可以初步断定这3个值是固定的,唯一有改变的就是d的值,所以我们只需要在请求时构造好就行了。

参数i的生成

只需要简单的生成16位随机字符串即可

import random
from string import ascii_letters, digits

_charset = ascii_letters + digits

def rand_char(num=16):
    return ''.join(random.choice(_charset) for _ in range(num))

params的生成

从代码可以看出,2次AES加密中,初始向量都是0102030405060708,加密模式都是CBC加密,不同的是第一次加密中,d作为message,g作为key来加密;第二次加密中,把第一次加密结果作为message,i作为key来加密。我们可以通过Crypto.Cipher中的AES实现,

import base64
from Crypto.Cipher import AES


def aes_encrypt(msg, key, iv='0102030405060708'):
    def padded(msg):
        pad = 16 - len(msg) % 16
        return msg + pad * chr(pad)

    msg = padded(msg)
    cryptor = AES.new(key, IV=iv, mode=AES.MODE_CBC)
    text = cryptor.encrypt(msg)
    text = base64.b64encode(text)
    return text


def gen_params(d, g, i):
    text = aes_encrypt(d, g)
    text = aes_encrypt(text, i)
    return text

encSecKey的生成

这个参数通过RSA算法生成,其中i作为message,e,f是加密时用到的参数。
在这里稍微解释一下RSA算法,算法选取2个很大的质数p,q,得到它们的乘积n,然后选取e,d满足e*d = 1 mod (p-1)(q-1),加密时text=(msg^e)%n,解密时msg=(text^d)%n,在这个函数里e就相当于算法里的ef相当于算法里的n
还有一点需要注意,encSecKey是一个完全由16进制数组成,但是在加密模块中一般都是返回byte流,然后通过base64编码(长度是原来的4/3),而像这种的应该是把byte流通过16进制表示出来(长度是原来的2倍)。
下面就是用python实现的时候了,我们可以通过Crypto.PublicKeyRSAconstruct方法实现。

# 错误版本
import binascii
from Crypto.PublicKey import RSA

cryptor = RSA.construct((0x00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7, 0x10001L))
text = cryptor.encrypt(msg, '')[0]
text = binascii.b2a_hex(text)  # byte流转为16进制

但是这时候问题出现了,上面的代码加密出来的结果和实际不符合,这样看来网易云的RSA加密和标准的有些不同,所以我们要深入到encryptedString这个方法进行调试。

function encryptedString(a, b) {
  for (var f, g, h, i, j, k, l, c = new Array, d = b.length, e = 0; d > e; )
    c[e] = b.charCodeAt(e),
    e++;
  for (; 0 != c.length % a.chunkSize; )
    c[e++] = 0;
  for (f = c.length,
  g = "",
  e = 0; f > e; e += a.chunkSize) {
    for (j = new BigInt,
    h = 0,
    i = e; i < e + a.chunkSize; ++h)  // here
      j.digits[h] = c[i++],
      j.digits[h] += c[i++] << 8;
    k = a.barrett.powMod(j, a.e),
    l = 16 == a.radix ? biToHex(k) : biToString(k, a.radix),
    g += l + " "
  }
  return g.substring(0, g.length - 1)
}

通过代码可以看出,c数组是b字符串转成的数组,然后在for循环中,c数组从左到右是从低位加到高位的,比如123456,1是加在低位,6是加在高位,这和平常有些不一样。
这样看来似乎需要把要加密的消息先翻转一下,然后再进行加密,测试之后发现也确实如此,实现如下:

import binascii
from Crypto.PublicKey import RSA

def rsa_encrypt(msg):
    cryptor = RSA.construct((0x00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7, 0x10001L))
    text = cryptor.encrypt(msg[::-1], '')[0]
    text = binascii.b2a_hex(text)
    return text

事实上,也可以自己来实现它的加密方式text=(msg^e)%n,只是自己实现的方式效率会比较低

def rsa_encrypt2(msg):
    msg = binascii.b2a_hex(msg[::-1])
    msg = int(msg, 16)
    text = 1
    for _ in range(0x10001):
        text *= msg
        text %= 0x00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
    return format(text, 'x')
compare.png

最终实现

import base64
import binascii
import json
import random
import requests

from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from string import ascii_letters, digits

_charset = ascii_letters + digits


def rand_char(num=16):
    return ''.join(random.choice(_charset) for _ in range(num))


def aes_encrypt(msg, key, iv='0102030405060708'):
    def padded(msg):
        pad = 16 - len(msg) % 16
        return msg + pad * chr(pad)

    msg = padded(msg)
    cryptor = AES.new(key, IV=iv, mode=AES.MODE_CBC)
    text = cryptor.encrypt(msg)
    text = base64.b64encode(text)
    return text


def gen_params(d, i):
    text = aes_encrypt(d, '0CoJUm6Qyw8W8jud')
    text = aes_encrypt(text, i)
    return text


def rsa_encrypt(msg):
    cryptor = RSA.construct((0x00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7, 0x10001L))
    text = cryptor.encrypt(msg[::-1], '')[0]
    text = binascii.b2a_hex(text)
    return text


def encrypt(query):
    query = json.dumps(query)
    rand_i = rand_char(16)
    params = gen_params(query, rand_i)
    enc_sec_key = rsa_encrypt(rand_i)
    data = {
        'params': params,
        'encSecKey': enc_sec_key
    }
    return data

if __name__ == '__main__':
    music_id = '483671599'
    url = 'http://music.163.com/weapi/v1/resource/comments/R_SO_4_{}?csrf_token='.format(music_id)
    headers = {
        'Accept': '*/*',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7',
        'Connection': 'keep-alive',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Host': 'music.163.com',
        'Origin': 'http://music.163.com',
        'Referer': 'http://music.163.com/',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',
    }
    query = {
        'rid': 'R_SO_4_{}'.format(music_id),
        'offset': '0',
        'total': 'true',  # 第一页时为true,其他页为false
        'limit': '20',
        'csrf_token': ''
    }
    data = encrypt(query)

    r = requests.post(url, data=data, headers=headers)
    print(r.content)
    for item in r.json()['comments']:
        print(item['content'])
result.png

一个套路

通过代码我们可以看见encSecKey是由i决定的,但是这个参数是浏览器这边随机生成的,所以其实是可以写死的,这样一来encSecKey就成了一个固定值,只需要处理params这个参数,当然,会不会因为encSecKey总是不变而被封IP什么的我就不知道了

其它

由于RSA是非对称加密,我们无法通过encSecKey解密出i,没有i也就无法解密params,所以也就只能对每个接口进行断点调试,观察请求的构造,这里提供几个常用接口的参数

  1. 歌曲评论

url:http://music.163.com/weapi/v1/resource/comments/R_SO_4_483671599?csrf_token=
d: {"rid":"R_SO_4_483671599","offset":"20","total":"false","limit":"20","csrf_token":""}

  1. 歌曲歌词

url:http://music.163.com/weapi/song/lyric?csrf_token=
d:{"id":"483671599","lv":-1,"tv":-1,"csrf_token":""}

  1. 歌单评论

url:http://music.163.com/weapi/v1/resource/comments/A_PL_0_2003824512?csrf_token=
d:{"rid":"A_PL_0_2003824512","offset":"0","total":"true","limit":"20","csrf_token":""}

  1. 搜索

url:http://music.163.com/weapi/cloudsearch/get/web?csrf_token=

搜索单曲:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}

搜索歌手:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"100","offset":"0","total":"true","limit":"90","csrf_token":""}

搜索专辑:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"10","offset":"0","total":"true","limit":"75","csrf_token":""}

搜索MV:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"1004","offset":"0","total":"true","limit":"20","csrf_token":""}

搜索歌词:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"1006","offset":"0","total":"true","limit":"30","csrf_token":""}

搜索歌单:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"1000","offset":"0","total":"true","limit":"30","csrf_token":""}

搜索主播电台:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"1009","offset":"0","total":"true","limit":"30","csrf_token":""}

搜索用户:{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"爱","type":"1002","offset":"0","total":"true","limit":"30","csrf_token":""}

最后,文章仅供学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • # 一度蜜v3.0协议 --- # 交互协议 [TOC] ## 协议说明 ### 请求参数 下表列出了v3.0版协...
    c5e350bc5b40阅读 640评论 0 0
  • 用过很多播放器,之前一直是酷我,偶尔QQ。但是网易云音乐出来后毅然变成了他的忠实用户。精确推荐和乐评都很赞!安利了...
    听城阅读 2,417评论 1 5
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,852评论 6 13