爬蟲之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": "[email protected]; 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": "[email protected]; 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": "[email protected]; 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"])