爬蟲篇之JS逆向破解
爬蟲中很重要的一個(gè)點(diǎn)就是JS的逆向破解加密,今天我們來淺析一下。
背景
先簡(jiǎn)單介紹一下為什么要有JS解密,目前大部分網(wǎng)頁都是采用的前后端分離的方式,所以呢,爬蟲的一般破解之道都是從后端接口來做文章,進(jìn)行突破。
不過道高一尺,魔高一丈,網(wǎng)頁開發(fā)會(huì)對(duì)API接口請(qǐng)求參數(shù)進(jìn)行加密,來增加爬蟲抓取的門檻。為此可以通過js逆向來分析破解加密方式,模擬瀏覽器發(fā)送請(qǐng)求獲取接口數(shù)據(jù)。
當(dāng)然,先說明,這篇文章并不是非常專業(yè)的JS解密,因?yàn)镴S的解密涉及很多種,多種行為的解密,本文只是對(duì)其中一種情況進(jìn)行簡(jiǎn)單的介紹。
來吧,讓我們一起簡(jiǎn)單學(xué)習(xí)一下。
上面這個(gè)圖是請(qǐng)求翻譯的全過程。
我們能清晰的看到這是直接以表單的形式提交的數(shù)據(jù)到后端API層,然后API來執(zhí)行翻譯的作用。
接下來我們用python模擬以下這個(gè)過程。
一定要注意的是,請(qǐng)求頭寫全,包括cookie和user-agent這些,還有下面的params一定要按照網(wǎng)頁中的來。
代碼給到大家。
import requests
#請(qǐng)求頭
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": "255",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "OUTFOX_SEARCH_USER_ID_NCOO=1992896419.125546; OUTFOX_SEARCH_USER_ID=1708647615@10.108.162.133; fanyi-ad-id=306808; fanyi-ad-closed=1; DICT_UGC=be3af0da19b5c5e6aa4e17bd8d90b28a|; JSESSIONID=abcJJxrChyTjz_26EmBgy; ___rl__test__cookies=1656205889631",
"Host": "fanyi.youdao.com",
"Origin": "http://fanyi.youdao.com",
"Referer": "http://fanyi.youdao.com/",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
}
#提交參數(shù)
params = {
"i": "love you , my baby",
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": "16562058896377",
"sign": "f85458213e7db4207f135599c7ddfac7",
"lts": "1656205889637",
"bv": "bdc0570a34c12469d01bfac66273680d",
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_REALTlME",
}
url = "http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"
#發(fā)起POST請(qǐng)求
response = requests.post(url=url,headers=headers,data=params).json()
print(response)
一部分是header信息,一部分是params信息
我們可以看到,參數(shù)params中除了我們要傳遞的參數(shù)翻譯內(nèi)容之外,還有好多我們不認(rèn)識(shí)的參數(shù),如果這里錯(cuò)了會(huì)怎么樣呢,隨便改一下其中的一個(gè)參數(shù),我們看看效果。
可以看到,直接返回錯(cuò)誤了,很明顯被禁止了,或者說是校驗(yàn)沒通過,屬于非法請(qǐng)求。
我們?cè)賮砜匆幌?,只改變翻譯的內(nèi)容,靠這些鹽和簽名是不是能夠成功翻譯呢?
結(jié)果發(fā)現(xiàn),我們只改變了要翻譯的內(nèi)容,結(jié)果還是不行,很明顯生成這些校驗(yàn)參數(shù)的過程是和要翻譯的內(nèi)容是相關(guān)的。
搜索不同的關(guān)鍵詞,請(qǐng)求body參數(shù)如下,分析發(fā)現(xiàn)除了我們要傳遞的翻譯內(nèi)容外還有4個(gè)參數(shù)是變量:
"salt": "16562058896377",
"sign": "f85458213e7db4207f135599c7ddfac7",
"lts": "1656205889637",
"bv": "bdc0570a34c12469d01bfac66273680d",
這些就是屬于請(qǐng)求鹽和校驗(yàn)參數(shù),有對(duì)應(yīng)的加密格式,接下來我們圍繞這四個(gè)參數(shù)來進(jìn)行破解。
接下來我們打開控制臺(tái),打開我們要分析的JS程序,直接ctrl+f全局搜索salt關(guān)鍵字。
找到我們要分析的地方,然后在打上斷點(diǎn),重新請(qǐng)求一遍。
F10往下一步一步的執(zhí)行。
當(dāng)執(zhí)行到如圖所示的位置的時(shí)候,我們把鼠標(biāo)移動(dòng)到r這個(gè)對(duì)象的位置上去,為什么要看這個(gè)對(duì)象呢,因?yàn)槟憧聪旅娴膕alt、sign、lts、bv這些參數(shù)都是屬于r這個(gè)對(duì)象的屬性。
我們能夠看到此時(shí)r對(duì)象的這幾個(gè)屬性已經(jīng)被賦予了值了。
接著看看這個(gè)r到底是什么。
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 + "Ygy_4c=r#e#4EX^NUGUc5")
}
};
進(jìn)一步分析發(fā)現(xiàn):
- r:當(dāng)前的時(shí)間戳。
- i:當(dāng)前的時(shí)間戳+(0到10的隨機(jī)數(shù))。
- salt:salt=i。
- e:搜索關(guān)鍵字。
- sign:md5("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5")。
至此完成簽名算法的實(shí)現(xiàn),接下來可以通過python來實(shí)現(xiàn)。
代碼如下:
import requests
from hashlib import md5
import time
import random
#請(qǐng)求地址
url = "http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"
appVersion = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
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": "244",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": "OUTFOX_SEARCH_USER_ID=-1506602845@10.169.0.82; JSESSIONID=aaaUggpd8kfhja1AIJYpx; OUTFOX_SEARCH_USER_ID_NCOO=108436537.92676207; ___rl__test__cookies=1597502296408",
"Host": "fanyi.youdao.com",
"Origin": "http://fanyi.youdao.com",
"Referer": "http://fanyi.youdao.com/",
"user-agent": appVersion,
"X-Requested-With": "XMLHttpRequest",
}
def r(e):
# bv
t = md5(appVersion.encode()).hexdigest()
# lts
r = str(int(time.time() * 1000))
# i
i = r + str(random.randint(0,9))
return {
"ts": r,
"bv": t,
"salt": i,
"sign": md5(("fanyideskweb" + e + i + "Ygy_4c=r#e#4EX^NUGUc5").encode()).hexdigest()
}
def fanyi(word):
data = r(word)
params = {
"i": word,
"from": "AUTO",
"to": "AUTO",
"smartresult": "dict",
"client": "fanyideskweb",
"salt": data["salt"],
"sign": data["sign"],
"lts": data["ts"],
"bv": data["bv"],
"doctype": "json",
"version": "2.1",
"keyfrom": "fanyi.web",
"action": "FY_BY_REALTlME",
}
response = requests.post(url=url,headers=headers,data=params)
#返回json數(shù)據(jù)
return response.json()
if __name__ == "__main__":
while True:
word = input("請(qǐng)輸入要翻譯的語句:")
result = fanyi(word)
#對(duì)返回的json數(shù)據(jù)進(jìn)行提取,提取出我們需要的數(shù)據(jù)
r_data = result["translateResult"][0]
print(r_data[0]["src"])
print(r_data[0]["tgt"])