js逆向-网易云音乐爬虫

声明:本文仅供学习交流,请勿暴力爬取数据,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

记录一次完整的js逆向爬虫过程
js混淆后每次请求函数名变量名可能都不同
随便打开一首歌

查看页面源代码,没有评论信息,评论是动态获取的
F12 -> 网络 -> Fetch/XHR刷新抓包。
Ctrl+F查询某个评论,评论在 get?csrf_token= 请求中获取 (后面表示为目标url)

无需登录,去掉csrf_token参数
请求 URL: https://music.163.com/weapi/comment/resource/comments/get
请求方法: POST
查看负载,post中包含两个参数params, encSecKey
观察可知参数被加密了

目标:通过js逆向找到未加密的参数,在python中模拟网页的js程序加密,再post请求

确定参数加密位置

查看请求的请求发起程序调用堆栈
点击栈顶程序进入源代码
alt text

点击后自动定位到向目标url发送请求的那一行
加入断点,刷新网页
alt text
查看作用域,url不是目标url
alt text
恢复脚本执行,执行十几次后,url为目标url
alt text

此时data已经被加密,逐个程序查看
alt text
观察可知数据在t0x/be0x中被加密

查看源代码

1
2
3
4
5
var bVi2x = window.asrsea(JSON.stringify(i0x), bse8W(["流泪", "强"]), bse8W(Qu7n.md), bse8W(["爱心", "女孩", "惊恐", "大笑"]));
e0x.data = j0x.cr0x({
params: bVi2x.encText,
encSecKey: bVi2x.encSecKey
})

params对应bVi2x.encText,encSecKey对应bVi2x.encSecKey
bVi2x由window.asrsea函数生成

1
2
3
4
5
6
7
8
9
10
11
//被加密的参数,其中rid和threadId的尾号明显是对应歌曲的id
i0x = {
"rid": "R_SO_4_1397345903",
"threadId": "R_SO_4_1397345903",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": ""
}

现在只需要确定window.asrsea的四个参数和该函数的执行逻辑

四个参数

第一个参数是json对象i0X转为字符串
观察源代码并在控制台验证
alt text
其它三个参数为定值

1
2
3
e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'

函数的执行逻辑

Ctrl+F查找window.asrsea

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
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
}
//AES加密(a明文,b密钥) d偏移量,模式CBC
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()
}
//核心函数d
function d(d, e, f, g) {
var h = {}
, i = a(16); //a(16)生成16位随机数
return h.encText = b(d, g),
h.encText = b(h.encText, i), //encText与i,i0X有关
h.encSecKey = c(i, e, f), //e,f都为定值,encSecKey完全由i生成
h
}
window.asrsea = d, //window.asrsea就是函数d

加入断点,执行
alt text
alt text
确定i和与i对应的encSecKey(这里用已经生成好的一对(i,encSecKey),也可以模拟c函数进行RSA加密随机生成一对)

1
2
i = "iD3QreYEDyh0VaJS"
encSecKey = "0a0d995ab03722dc085ef73fe0a067b29bcfdf01c7837c4e118380c6ba2aedbdee2dd8f193fce9d0a6fa7bd27246226e57cfcd9555cfc412b3b460022700e7ac0468232661eef7505ad8df3f5bb69687334075fbd32405d1eb4b264c1c39bde86e004c8637cddba3a7c106e5edab21015654a4c14f686ea40720fd541ad7f4a0"

此时已经确定好所有参数,只需要模拟b函数和d函数

完整的python代码实现

这里只获取了一首歌的评论

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
49
50
51
52
53
54
55
56
57
58
59
60
61
import requests
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import rsa
import json

headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"}
url = "https://music.163.com/weapi/comment/resource/comments/get"

#此处可以把4_后面的id替换为对应歌的id
bgdata = {
"rid": "R_SO_4_1397345903",
"threadId": "R_SO_4_1397345903",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token":""
}

# params对应bVi2x.encText encSecKey对应bVi2x.encSecKey
# var bVi2x = d(JSON.stringify(i0x), bse8W(["流泪", "强"]), bse8W(Qu7n.md), bse8W(["爱心", "女孩", "惊恐", "大笑"]));
#bse8W(["流泪", "强"])
e = '010001'
#bse8W(Qu7n.md)
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
#bse8W(["爱心", "女孩", "惊恐", "大笑"])
g = '0CoJUm6Qyw8W8jud'
#a(16) 16位随机数
i = "iD3QreYEDyh0VaJS"
#i对应的encSecKey
encSecKey = "0a0d995ab03722dc085ef73fe0a067b29bcfdf01c7837c4e118380c6ba2aedbdee2dd8f193fce9d0a6fa7bd27246226e57cfcd9555cfc412b3b460022700e7ac0468232661eef7505ad8df3f5bb69687334075fbd32405d1eb4b264c1c39bde86e004c8637cddba3a7c106e5edab21015654a4c14f686ea40720fd541ad7f4a0"

#模拟b函数
def b(data, key):
# 确保密钥长度为16字节
key = key.encode('utf-8')
# 确保数据采用UTF-8编码
data = data.encode('utf-8')
# 使用给定的密钥和静态IV创建AES密码器对象
iv = b'0102030405060708'
cipher = AES.new(key, AES.MODE_CBC, iv)
# 填充数据以确保其长度是16字节的倍数,ASE加密数据必须被填充为16的倍数,填充的字符为chr(缺少的字符数)
padded_data = pad(data, AES.block_size)
# 加密填充后的数据
encrypted_data = cipher.encrypt(padded_data)
# 将加密后的数据编码为base64格式以获得字符串输出
encrypted_data_base64 = base64.b64encode(encrypted_data).decode('utf-8')
return encrypted_data_base64

data = {
#模拟d函数
"params": b(b(json.dumps(bgdata), g), i),
"encSecKey":encSecKey
}

resp = requests.post(url=url, headers=headers,data=data)
comment = resp.json()
print(resp.text)

网易加密底层逻辑 (个人猜想)
生成一个16位随机数
用公钥和 i 进行RSA非对称加密算法生成 encSecKey
i 和原参数进行AES对称加密算法生成 encText
向服务器发送 encSecKeyencText
服务器用私钥对 encSecKey 进行RSA解密得到 i
再用 iencText 进行AES解密得到 原参数