原本需要爬取七麦数据,该网站(https://api.qimai.cn/rank/index?analysis=ezB5Qnt3fkVqXnpcOWBfQgV%2FDENqYQ0NcBMfEVZdXktRW1JVSHATAQIJVwQGAVcNBg8BcBMB&brand=free&device=iphone&country=cn&genre=5000)的analysis参数是js加密之后的,需要对原代码进行逆向分析才能解密爬取。
js逆向分析的学习,参考B站关于评论爬取的视频(https://www.bilibili.com/video/BV1Mf4y1s7ds?p=42),七麦网的分析,参考博客https://blog.csdn.net/weixin_43582101/article/details/122456609
这里以music评论为例记录下js逆向分析的流程。
一,接口分析:
1,首先F12 → Network → Fetch/XHR 找到评论对应的Request URL: (https://music.163.com/weapi/comment/resource/comments/get?csrf_token=) (如图,通过Preview找到内容为评论的请求),且请求方式为POST请求:
2,新版谷歌点击Payload(旧版谷歌在Headers下滑到最底部),查看Form Data,得到请求时的参数,可以看到params 和 encSecKey 两个参数都是加密参数,那么本次分析的重点就是分析出这两个参数是怎么加密的,之后解密即可正常请求得到数据。
二,逆向分析
3,点击Initiator,查看调用的栈,点击最上方的栈进入:
4,点击左下方{}或者在上方弹窗选择【Pretty-print】使得代码规范显示,更方便查看,之后看到是一个send()方法进行的该请求,在该方法处打断点:
5,打断点之后刷新网页,重新请求,网页在断点处暂停(Paused in debugger),查看右边的Local处reque下的数据,可以看到url并不是评论对应的url(get?csrf_token=):
6,点击F8或者直接点击蓝色按钮,放开,查看url,如果不是继续点击调试,直到url是评论的链接(当前时间点击两次调试就得到目的url):data对应的无序代码即是加密部分
7,点击右方的Call Stack,点击b7g.blm8e栈进入,美观查看,可以看到是点d7e这个参数,再在右边查看data,是加密状态:
8,再在Call Stack往下找,直到传参的data不再是加密状态:data: “rid=R_SO_4_1325905146&threadId=R_SO_4_1325905146&pageNo=1&pageSize=20&cursor=-1&offset=0&orderType=1”
9,所以回到上一个堆栈,去打断点,调试查看加密方式:
10,在i7b参数处设置断点(同时取消之前的send()方法的断点),刷新网页,查看Y8Q对应的url链接,如果不是评论的链接,则点击F8或者调试按钮继续调试,直到右边Y8Q对应的url链接是评论链接:
11,然后多加几个断点,点击下一步按钮或者F10,一步步往下走,这里过程可能比较漫长,一步步调试,直到看到方法内的bVj7c被重新赋值成encText和encSeckey,在右侧显示为两个加密参数,而data则由bVj7c的两个参数组合而成:
data =( params=encText) & (encSeckey =encSeckey)
12,所以可以判断,加密过程在这两行代码内:
var bVj7c = window.asrsea(JSON.stringify(i7b), bsR1x(["流泪", "强"]), bsR1x(Xp4t.md), bsR1x(["爱心", "女孩", "惊恐", "大笑"]));
e7d.data = j7c.cq8i({
params: bVj7c.encText,
encSecKey: bVj7c.encSecKey
})
三,逆向解密
1,请求方式是POST,找到data的真实参数,即i7b:
data = { #即 i7b
"csrf_token": "",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"pageNo": "1",
"pageSize": "20",
"rid": "R_SO_4_1325905146",
"threadId": "R_SO_4_1325905146"
}
2,处理加密过程:找到window.asrsea()方法,在代码里面直接全局搜索,除window.asrsea(JSON.stringify(i7b), bsR1x([“流泪”, “强”]), bsR1x(Xp4t.md), bsR1x([“爱心”, “女孩”, “惊恐”, “大笑”]));外,仅剩window.asrsea = d,可以判断加密处理过程主要在以下代码段:
3,分析d方法(即window.asrsea()方法)的4个传参:d即为data,需要继续分析;e,f,g,点击console,输入都是定值:
# d = window.asrsea(JSON.stringify(i7b), bsR1x(["流泪", "强"]), bsR1x(Xp4t.md), bsR1x(["爱心", "女孩", "惊恐", "大笑"]));
function d(d, e, f, g) { # d:JSON.stringify(i7b),i7b即data;
var h = {}
, i = a(16); # i在后面分析
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
# d:JSON.stringify(i7b),i7b即data;
# e:控制台输入bsR1x(["流泪", "强"])得到:'010001'
# f:console输入bsR1x(Xp4t.md)得到很长的定值:'00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
# g:console输入bsR1x(["爱心", "女孩", "惊恐", "大笑"])得到:'0CoJUm6Qyw8W8jud'
4,继续分析d方法里面的h 和 i:
"""
function a(a) { # a=16 ,返回随机的16位字符串
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1) # 循环16次
e = Math.random() * b.length, # 随机数,假设位2.234
e = Math.floor(e), # 取整 ,2.234取整之后为2
c += b.charAt(e); # 取字符串中的xxx位置,字符串中2位置的值为c
return c
}
function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b) # b是密钥
, d = CryptoJS.enc.Utf8.parse("0102030405060708") # 这里告诉了偏移量iv的值
, e = CryptoJS.enc.Utf8.parse(a) # e是数据
, f = CryptoJS.AES.encrypt(e, c, { # c 加密的密钥
iv: d, # iv 偏移量
mode: CryptoJS.mode.CBC # 用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) { # d:JSON.stringify(i7b),i7b即data; e:'010001' f:很长的定值
var h = {}
, i = a(16); # 通过a方法的分析,i就是一个16位的随机数
return h.encText = b(d, g), # 改写为h.encText = b(d, g),
h.encText = b(h.encText, i), # h.encText返回的就是params
h.encSecKey = c(i, e, f), # h.encSecKey得到的就是encSecKey e和f是定制,i是随机的,所以c方法主要根据i来得到值,c的值和i的值对应,假设i取一个定值,c的值是固定的
h # 改写为 return h
}
"""
5,在d方法内的h.encText = b(h.encText, i),处设置断点,刷新网页,查看i有值时,对应的encSecKey 和 encText值分别是多少:
6,从而得到i= "rXGtWFn56Kk6oJCU"时,c的定值为:“0028176d4c322cd27302597aadf221ba1f94557f63bdaaf20735c30823e0c4beafcf0e4ee0f56f0779f91aeab9ca31e88863c30e3f6d33319b46836d34188de62e0002f586bc14ee338b8d6a6ecc383c9c2f139dcc2bb15b7e5839992727ff54d0ec534ab6c61238e3ed773d29b8ec1acc3b242764a0d095b7d21158c92c8ebf”,即encSecKey的值。
# 再次加密:数据+g => 第一次加密+i => b=params
h.encText = b(d, g), # g是密钥
h.encText = b(h.encText, i), # i也是b方法中的密钥
7,继续编写解密代码:
from Crypto.Cipher import AES
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
data = {
"csrf_token": "",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"pageNo": "1",
"pageSize": "20",
"rid": "R_SO_4_1325905146",
"threadId": "R_SO_4_1325905146"
}
e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'
i = 'rXGtWFn56Kk6oJCU'
def get_encSecKey():
return "0028176d4c322cd27302597aadf221ba1f94557f63bdaaf20735c30823e0c4beafcf0e4ee0f56f0779f91aeab9ca31e88863c30e3f6d33319b46836d34188de62e0002f586bc14ee338b8d6a6ecc383c9c2f139dcc2bb15b7e5839992727ff54d0ec534ab6c61238e3ed773d29b8ec1acc3b242764a0d095b7d21158c92c8ebf"
def get_params(data): #默认这里收到的是字符串
first = enc_params(data,g)
secend = enc_params(first,i)
return second
def enc_params(data,key):
aes = AES.new(key=key.encode("utf-8"),IV=iv.encode("utf-8"),mode=AES.MODE_CBC) # 创建加密器
bs = aes.encrypt(data.encode("utf-8")) # 加密,加密的内容的长度必须是16的倍数
return str(b64encode(bs),"utf-8") # 转换成字符串返回
完整代码:
import json
import requests
from Crypto.Cipher import AES # pip install pycryptodome
from base64 import b64encode
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
# 请求方式是POST
data = { #即 i7b
"csrf_token": "",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"pageNo": "1",
"pageSize": "20",
"rid": "R_SO_4_1325905146",
"threadId": "R_SO_4_1325905146"
}
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
e = "010001"
i = "gjsKgpBKWhQGJVYJ" # encSecKey的值和i的值是对应的,将i的值固定死之后,随之得到encSecKey的值
def get_encSecKey():
return "34e795a0c95ca5c4513555b806fa5041bd85a9e3a90aaaa2bbf41d4dc4c649cb1baa12bf3d17a0463701341f15477723f3d27a71f9d7ea5deeb69cd7c6d75777f5e1f0b06e7eda8bef996ea92acd172792fc1caab619d369ab7b4b2411b588a10f331d646af12c9ab44a0d3d331c6d8807be66cd5d00948a49c6c437d88687b1"
def get_params(data): # 默认这里收到的是字符串
first = enc_params(data,g)
second = enc_params(first,i)
return second
def to_16(data):
pad = 16 - len(data) % 16
data += chr(pad) * pad
return data
def enc_params(data,key): # 加密过程
iv = "0102030405060708"
data = to_16(data)
aes = AES.new(key=key.encode("utf-8"),IV=iv.encode("utf-8"),mode=AES.MODE_CBC) # 创建加密器
bs = aes.encrypt(data.encode("utf-8")) # 加密,加密的内容的长度必须是16的倍数
return str(b64encode(bs),"utf-8") # 转换成字符串返回
# 处理加密过程
# d = window.asrsea(JSON.stringify(i7b), bsR1x(["流泪", "强"]), bsR1x(Xp4t.md), bsR1x(["爱心", "女孩", "惊恐", "大笑"]));
"""
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) # b是密钥
, d = CryptoJS.enc.Utf8.parse("0102030405060708") # 这里告诉了偏移量iv的值
, e = CryptoJS.enc.Utf8.parse(a) # e是数据
, f = CryptoJS.AES.encrypt(e, c, { # c 加密的密钥
iv: d, # iv 偏移量
mode: CryptoJS.mode.CBC # 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) { # d:JSON.stringify(i7b),i7b即data; e:'010001' f:很长的定值
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
"""
resp = requests.post(url,data={
"params":get_params(json.dumps(data)),
"encSecKey":get_encSecKey()
})
print(resp.text)
print(resp.status_code)