爬虫之js加密破解
一:JS加密简介
我们爬取数据时想要破解JS加密,首先要了解什么是JS加密,它是如何加密的,了解了它的原理后我们才能迅速,准确的破解它。
(一):JS加密原理
JS全称JavaScript,是一种前端语言。就如同我们学的Python一样是一门计算机语言,只不过应用领域不同而已。通过这门语言可以在前端定义函数,进行数据和逻辑的计算,这也是JS能够加密的重要原因。当我们爬取一些简单的网站时,首先是向服务器发送携带参数的url请求,服务器根据我们的请求以及参数直接返回给我们数据。而进过JS加密的网站会在我们我们发送url请求时,首先对我们url中携带的参数在一个JS文件中进行一系列的运算,然后将运算后的值作为参数代替原本的参数发送到服务器进行请求。
(二):JS加密破解要点
知道了原理后,我们就明白了破解JS加密的要点:只要我们弄清了JS文件处理参数的过程,那么我们就能在本地用python进行同样处理,这样得到的参数就是服务器想要的参数了,到此我们的JS加密破解就完成了。
二:JS加密破解举例
(一):爬取有道翻译的译文
- 我们先在翻译框中输入一个待翻译单词,以as为例,获取到它的译文,借此了解它的参数结构
import requests
# 定义请求头
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Content-Length": "236",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "OUTFOX_SEARCH_USER_ID=-1798855308@10.108.160.102; JSESSIONID=aaapIu5Rsmjnk55eGXiyx; OUTFOX_SEARCH_USER_ID_NCOO=904567522.0777537; ___rl__test__cookies=1606464861023",
"Host": "fanyi.youdao.com",
"Origin": "http://fanyi.youdao.com",
"Referer":"http://fanyi.youdao.com/",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
# 定义参数字典
# get请求定义参数时用的是params,而post请求定义参数时用的是data
# data可以从请求头中获得,这就是我们发送post请求时携带的参数,是我们接下了研究的重点
data = {
"i": "as",
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": "16064648610367",
"sign": "f3ef68d97477fa61bcd2d3849b1260f9",
"lts":" 1606464861036",
"bv": "85a1eb4c1b6f458fda5e7e81446e33f5",
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_REALTlME"
}
# 请求有道翻译
response = requests.post(url="http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule",
headers=headers,
data=data
)
# 将json数据转换为python类数据,并获取我们想要的结果
print(response.json()["translateResult"][0][0]["tgt"])
- 我们进行自定义单词输入,看看会有什么结果
import requests
# 定义请求头
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Content-Length": "236",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "OUTFOX_SEARCH_USER_ID=-1798855308@10.108.160.102; JSESSIONID=aaapIu5Rsmjnk55eGXiyx; OUTFOX_SEARCH_USER_ID_NCOO=904567522.0777537; ___rl__test__cookies=1606464861023",
"Host": "fanyi.youdao.com",
"Origin": "http://fanyi.youdao.com",
"Referer":"http://fanyi.youdao.com/",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
# 自定义单词输入
word = input("请输入要翻译的单词:")
# 定义参数字典
# get请求定义参数时用的是params,而post请求定义参数时用的是data
# data可以从请求头中获得,这就是我们发送post请求时携带的参数,是我们接下了研究的重点
data = {
"i": word,
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": "16064648610367",
"sign": "f3ef68d97477fa61bcd2d3849b1260f9",
"lts":" 1606464861036",
"bv": "85a1eb4c1b6f458fda5e7e81446e33f5",
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_REALTlME"
}
# 请求有道翻译
response = requests.post(url="http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule",
headers=headers,
data=data
)
# 将json数据转换为python类数据,并获取我们想要的结果
print(response.json()["translateResult"][0][0]["tgt"]) # 报错,得不到数据
说明只改变翻译单词是行不通的,我们去看看不同单词的参数有什么不一样
- 在翻译框中重新输入一个单词,以hello为例,拿出它的data参数,与as参数对比,寻找出不同点,可自行输入多个单词进行对比,能够更加准确的定义到参数不同点,这里我就以两个对比
# as 参数
i: as
from: AUTO
to: AUTO
smartresult: dict
client: fanyideskweb
salt: 16066298991539
sign: eb3e15819f8d3581a0c7eb9ce8f020b5
lts: 1606629899153
bv: 85a1eb4c1b6f458fda5e7e81446e33f5
doctype: json
version: 2.1
keyfrom: fanyi.web
action: FY_BY_REALTlME
# hello 参数
i: hello
from: AUTO
to: AUTO
smartresult: dict
client: fanyideskweb
salt: 16066321109501
sign: 895347ca623f4079fb5f39ae69f5afab
lts: 1606632110950
bv: 85a1eb4c1b6f458fda5e7e81446e33f5
doctype: json
version: 2.1
keyfrom: fanyi.web
action: FY_BY_REALTlME
通过两个不同单词的参数对比,我们可以发现有以下几个参数不同:
salt:看起来像是时间戳,但不确定
sign:看起来像是一串加密数值
lts :与salt很像,仅仅是比salt少了一位数
-
找到不同参数后,我们就要尝试去破解它,首先要找到相应的js文件,搞清楚这些值是经过了怎样的运算得到的
破解步骤:
-
找到相应的js文件:f12 —> Network —> XHR —> Initiator,如果只有一个js文件那么就肯定是这个文件了,如果有多个就需要我们一个个去找了。
-
进去文件后可先点击下面的{}进行格式整理,然后Ctrl+f打开搜索框,搜索 salt
-
发现有多个salt,这时需要筛选出我们需要的那个salt,我们只需要单独的salt单词,故先排除包含salt的单词。在剩下的单词中使用调试模式寻找,即在找到的salt处打一个断点,然后在翻译框中随便输入内容,如果屏幕变暗被锁定那么就说明我们找到了salt。
-
找到后,将js函数剪切下来进行分析
var r = function(e) { var t = n.md5(navigator.appVersion) , r = "" + (new Date).getTime() , i = r + parseInt(10 * Math.random(), 10); return { ts: r, bv: t, salt: i, sign: n.md5("fanyideskweb" + e + i + "]BjuETDhU)zqSxf-=B#7m") } };
-
我们分析salt的值,它是i的值,分析i的值,i中包含r的值,我们把r的值也进行分析
# i的值即为salt的值,r比i少一位,即r的值就是lts的值 i = r + parseInt(10 * Math.random(), 10) r = "" + (new Date).getTime() # r分析,得到的是一个时间戳字符串,js中的时间戳单位是毫秒 # 使用python模仿运算(解密) import time lts = str(round(time.time * 1000)) # i分析,parseINt为舍去小数取整,Math.random为获取一个[0,1)之间的随机小数,10代表10进制 # 整体意思为获取一个0~9的一个随机数,包括9 # 使用python模仿运算 import random salt = r + random.randint(0,9)
-
现在只剩下sign的值了,我们来分析它,可以看出,sign 就是一个字符串的md5加密值
sign: n.md5("fanyideskweb" + e + i + "]BjuETDhU)zqSxf-=B#7m") # 字符串由4部分组成,唯一没有确定的就是e这个变量了,我们将鼠标放在e上面,就会发现e原来是我们要翻译的字符串, # 使用Python模仿运算 import hashlib # 定义md5加密函数 def encryption(sign): md5 = hashlib.md5() md5.updata(sign.encode("utf-8")) return md5.hexdigest() sign = encryption("fanyideskweb" + word + salt + "]BjuETDhU)zqSxf-=B#7m")
- 至此,有道翻译的破解过程已完成,只需将上面的代码整合即可,整合代码如下
import requests
import time
import random
import hashlib
# 定义请求头
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Content-Length": "236",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "OUTFOX_SEARCH_USER_ID=-1798855308@10.108.160.102; JSESSIONID=aaapIu5Rsmjnk55eGXiyx; OUTFOX_SEARCH_USER_ID_NCOO=904567522.0777537; ___rl__test__cookies=1606464861023",
"Host": "fanyi.youdao.com",
"Origin": "http://fanyi.youdao.com",
"Referer":"http://fanyi.youdao.com/",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
# 自定义输入单词
word = input("输入单词:")
# 定义md5加密函数
def encryption(sign):
# 初始化md5
md5 = hashlib.md5()
# 加密
md5.update(sign.encode("utf-8"))
# 返回加密数据
return md5.hexdigest()
# 获取lts的值
lts = str(round(time.time() * 1000))
# 获取salt的值
salt = lts + str(random.randint(0, 9))
# 获取sign的值
sign = encryption("fanyideskweb" + word + salt + "]BjuETDhU)zqSxf-=B#7m")
# 定义参数字典
data = {
"i": word,
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": salt,
"sign": sign,
"lts": lts,
"bv": "85a1eb4c1b6f458fda5e7e81446e33f5",
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_REALTlME"
}
# 请求有道翻译
response = requests.post(url="http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule",
headers=headers,
data=data
)
# 向将json数据转换为python类数据,并获取我们想要的结果
print(response.json()["translateResult"][0][0]["tgt"])
(二):爬取百度翻译的译文
爬取百度翻译的译文相比于有道翻译难度有所增加,但原理是一样的,难在它的js运算过程多了一些。
- 比较不同单词的post参数有什么不同
# 单词为hello时的参数
from: en
to: zh
query: hello
simple_means_flag: 3
sign: 54706.276099
token: c607abf49334fb1ca7f9987a5d86447c
domain: common
# 单词为world时的参数
from: en
to: zh
query: world
simple_means_flag: 3
sign: 335290.130699
token: c607abf49334fb1ca7f9987a5d86447c
domain: common
经过对比我们可以发现,query和sign不同,而query为我们的输入值,所以我们只需要解密sign即可
- 找到js中关于sign的加密函数
var f = this
, n = this.processQuery(n)
, h = {
from: p.fromLang,
to: p.toLang,
query: n,
transtype: r,
simple_means_flag: 3,
sign: y(n),
token: window.common.token,
domain: w.getCurDomain()
};
-
分析sign,sign是一个函数的返回值,我们将鼠标放在y(n)上,发现函数名称并不是y,而是e,其实在这里e函数代表的就是y函数,我们点击进入到e函数中,截取出e函数的代码。我们需要分析这段js代码,然后找出它的运算规律。
但是这段代码有点儿复杂,而我们需要的仅仅是它的结果,所以我们在这里创建一个js文件,让python去运行这些代码,我们只需要得到加密的结果即可
# 创建一个js文件,命令为sign.js,并将以下代码放进去
function e(r) {
var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === o) {
var t = r.length;
t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
} else {
for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
"" !== e[C] && f.push.apply(f, a(e[C].split(""))),
C !== h - 1 && f.push(o[C]);
var g = f.length;
g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
}
var u = void 0
, l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
u = null !== i ? i : (i = window[l] || "") || "";
for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
var A = r.charCodeAt(v);
128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
S[c++] = A >> 18 | 240,
S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
S[c++] = A >> 6 & 63 | 128),
S[c++] = 63 & A | 128)
}
for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
p += S[b],
p = n(p, F);
return p = n(p, D),
p ^= s,
0 > p && (p = (2147483647 & p) + 2147483648),
p %= 1e6,
p.toString() + "." + (p ^ m)
}
- 接下来我们就通过python来运行这些代码,需要下载一个第三方模块
(1) 下载pyexecjs模块
pip install pyexecjs
(2) 创建一个测试文件,用来测试js代码,起名为test.py
# 导入模块
import execjs
# 读取js文件
js_content = open("sign.js","r",encoding="utf-8").read()
# 编译js文件
js_data = execjs.compile(js_content)
# 执行js文件
# 第一个参数为要执行的js函数名
# 第二个参数为要执行的js函数中的参数
sign = js_data.call("e","hello")
print(sign)
发现错误:
第一个错误:TypeError: ‘i’ 未定义
解决:我们页面中找到这一段js函数中的 i,打开调试模式,让代码一行一行的向下运行,查看 i 的值, 当执行完 i 后,我们将鼠标放在 i 上,可以发现 i 其实就是一个字符串。换不同的单词进行验证,发现 i 的值是不变的。故我们在sign.js文件中对 i 进行定义
# sign.js文件
function e(r) {
// 此处对i进行定义
// ---------------------------------------------
var i ="320305.131321201"
// ---------------------------------------------
var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === o) {
var t = r.length;
t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
} else {
for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
"" !== e[C] && f.push.apply(f, a(e[C].split(""))),
C !== h - 1 && f.push(o[C]);
var g = f.length;
g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
}
var u = void 0
, l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
u = null !== i ? i : (i = window[l] || "") || "";
for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
var A = r.charCodeAt(v);
128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
S[c++] = A >> 18 | 240,
S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
S[c++] = A >> 6 & 63 | 128),
S[c++] = 63 & A | 128)
}
for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
p += S[b],
p = n(p, F);
return p = n(p, D),
p ^= s,
0 > p && (p = (2147483647 & p) + 2147483648),
p %= 1e6,
p.toString() + "." + (p ^ m)
}
第二个错误:TypeError: 缺少对象
解决:在js中,对象可能是一个类似于python中的字典,也可能是一个函数对象。所以我们在js代码中 找到未定义的对象,这个是需要我们一行一行去找的,最终发现未定义的对象为 p = n(p, F)中的n。我 们在页面中进入到n函数中,将其截取下来放到js文件中,再次运行test.py文件。
# sign.js文件
function e(r) {
var i ="320305.131321201" // 此处对i进行定义
var o = r.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === o) {
var t = r.length;
t > 30 && (r = "" + r.substr(0, 10) + r.substr(Math.floor(t / 2) - 5, 10) + r.substr(-10, 10))
} else {
for (var e = r.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), C = 0, h = e.length, f = []; h > C; C++)
"" !== e[C] && f.push.apply(f, a(e[C].split(""))),
C !== h - 1 && f.push(o[C]);
var g = f.length;
g > 30 && (r = f.slice(0, 10).join("") + f.slice(Math.floor(g / 2) - 5, Math.floor(g / 2) + 5).join("") + f.slice(-10).join(""))
}
var u = void 0
, l = "" + String.fromCharCode(103) + String.fromCharCode(116) + String.fromCharCode(107);
u = null !== i ? i : (i = window[l] || "") || "";
for (var d = u.split("."), m = Number(d[0]) || 0, s = Number(d[1]) || 0, S = [], c = 0, v = 0; v < r.length; v++) {
var A = r.charCodeAt(v);
128 > A ? S[c++] = A : (2048 > A ? S[c++] = A >> 6 | 192 : (55296 === (64512 & A) && v + 1 < r.length && 56320 === (64512 & r.charCodeAt(v + 1)) ? (A = 65536 + ((1023 & A) << 10) + (1023 & r.charCodeAt(++v)),
S[c++] = A >> 18 | 240,
S[c++] = A >> 12 & 63 | 128) : S[c++] = A >> 12 | 224,
S[c++] = A >> 6 & 63 | 128),
S[c++] = 63 & A | 128)
}
for (var p = m, F = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(97) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(54)), D = "" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(51) + ("" + String.fromCharCode(94) + String.fromCharCode(43) + String.fromCharCode(98)) + ("" + String.fromCharCode(43) + String.fromCharCode(45) + String.fromCharCode(102)), b = 0; b < S.length; b++)
p += S[b],
p = n(p, F);
return p = n(p, D),
p ^= s,
0 > p && (p = (2147483647 & p) + 2147483648),
p %= 1e6,
p.toString() + "." + (p ^ m)
}
function n(r, o) {
for (var t = 0; t < o.length - 2; t += 3) {
var a = o.charAt(t + 2);
a = a >= "a" ? a.charCodeAt(0) - 87 : Number(a),
a = "+" === o.charAt(t + 1) ? r >>> a : r << a,
r = "+" === o.charAt(t) ? r + a & 4294967295 : r ^ a
}
return r
}
将运行结果与sign值进行对比,发现测试文件运行结果即为我们要找的sign的值。这时将测试文件中的代码复制到执行文件中即可。
- 最终代码
import requests
import execjs
# 定义请求头
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36",
"cookie": "BIDUPSID=7D798573C260F654BA4EFE460E859695; PSTM=1606443252; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; REALTIME_TRANS_SWITCH=1; FANYI_WORD_SWITCH=1; HISTORY_SWITCH=1; SOUND_PREFER_SWITCH=1; SOUND_SPD_SWITCH=1; H_PS_PSSID=; delPer=0; PSINO=1; BDRCVFR[7Wj9V7qhHGf]=E9r8WGTE_ZnTAnzn1fdQhP8; ZD_ENTRY=baidu; BAIDUID=3565866098ABC8C0BF8E4E19805DDEA1:FG=1; BAIDUID_BFESS=3565866098ABC8C0721258A207B135B6:FG=1; Hm_lvt_64ecd82404c51e03dc91cb9e8c025574=1606463241,1606464629,1606487357,1606635536; BA_HECTOR=al248l81058085a5qk1fs6lo00q; Hm_lpvt_64ecd82404c51e03dc91cb9e8c025574=1606638236; __yjsv5_shitong=1.0_7_c0ee1fd992c31ae0ae6c1660faf22b1f7dea_300_1606638347832_121.69.97.22_9e5c1475; yjs_js_security_passport=edce489b85bc1c854013aa7ffa198cf269063fb9_1606638349_js",
}
# 自定义输入单词
word = input("输入要翻译的汉字:")
# 读文件
content = open("sign.js", "r", encoding="utf-8").read()
# 编译
js_data = execjs.compile(content)
# 执行
# 第一个参数代表要执行的js函数
# 第二个参数代表js函数中要传入的参数
sign = js_data.call("e", word)
print(sign)
# 定义data参数
# from参数为"zh",to为"en"时为中译英,互换则为英译中
data = {
"from": "zh",
"to": "en",
"query": word,
"simple_means_flag": "3",
"sign": sign,
"token": "c607abf49334fb1ca7f9987a5d86447c",
"domain": "common"
}
# 访问url
response = requests.post(
url="https://fanyi.baidu.com/v2transapi?from=en&to=zh",
headers=headers,
data=data
)
print(response.json()["trans_result"]["data"][0]["dst"])