前言
本次針對某個翻譯平台的js逆向,同時並不存在惡意,只是本着學習研究為主,同時,在分析期間並未高頻次測試導致該平台服務器不可用
觀察
首先直接體驗下:

抓包查看請求的接口:

然后請求參數有這些:

一看,i應該就是我傳的參數了,常規思維走起來,直接復制這些參數,然后在python里運行:

我把i換成main翻譯試試:

不行了,有點意思哈,為啥復制的english就可以翻譯呢?
原因是這樣的:
像這種翻譯平台,肯定很多人調用,然后同時段用的人肯定很多,加上,有些人有一種習慣,就是在翻譯中途,數據還沒出來的話,就想再點一次,而此時服務器其實翻譯好了正准備返回或者已經返回,這樣,服務器緩存一下,可以節省實際的翻譯部分代碼產生的資源占用,而直接就把翻譯結果返回了,同時可以把這個資源給其他需要翻譯的人用,所以就會有為什么同一個待翻譯字段和同一組請求參數可以用,換一個待翻譯字段用同一組請求參數就不能用了。
有朋友會說了,同一個待翻譯字段,它同一套加密算法,出來肯定是一樣的值啊,所以怎么確定不是同樣的翻譯一次而是你說的緩存翻譯結果直接返回呢,注意哈,請求參數里有個時間戳字段,這個可是每次都會變的啊,所以大概率是我說的那種情況。當然實際情況是這樣的啊,我也不是該平台的實際研發人員,所以具體情況我也不清楚哈
接着我又翻譯幾次之后,發現,就有4個值是一直會變得,其他不會變,就 lts,bv,salt,sign,就這四個,然后i就是我們輸入的待翻譯的字段
好,不廢話,直接看這幾個值在哪生成的,直接全局搜sign:

進入這個js文件,再搜,看到有11處,好,那一個一個看下

第一個:

這個的請求參數不太像,再看下面的,

這個也不太像,我們剛才的請求參數沒有cache這個值,接着再看:

這個有點像,
再接着看:

這個感覺有點像
分析
對上面看到的兩處有點像的,打上斷點看看:

動態調試

輸入get立即就被斷上了,那說明我們打斷點打對地方了
那么我們開始找那4個參數怎么來的了
第一個,ts

不用說了,這個就是個時間戳,然后被轉成了字符串的,先看下它是多少位的時間戳:
用在線的時間戳轉換工具直接看

看來是毫秒級的時間戳了,而我們python默認的time模塊並不是毫秒級的:

這個后面乘以個1000然后取整就可以了,ts搞定了
第二個,bv

看代碼,bv其實就是t,而t 就是navigator.appversion參數再md5之后的字段,先看下navigator.appversion是啥東西,把鼠標放上去,它自己就顯示了

不覺得這個很像個啥嗎,搞爬蟲,應該一眼就能認出來這是啥了,就是ua啊,

然后用第一個【/】作為分隔符把它分割了:
用python做個驗證,這個值果然可以對上

也就是說,只要我們的請求不變,那一直都可以用這個值。
第三個,salt

這個,看代碼就看出來,就是用剛才的時間戳參數,然后乘以了一個隨機的0到1之間的小數,然后再乘以10取整數跟之前的時間戳以字符串拼接起來的,那不就是直接加了一個0-9的隨機整數嘛
第四個,sign
這個就是最重要的了,

而,主要就是看下e和i是啥了,仔細看,
e就是我們待翻譯參數

i就是剛才的salt參數了
這個sign參數每次都是由時間戳+待翻譯參數生成出來的,這也是為什么,剛才我用同一組固定的請求參數翻譯【english】時可以翻譯,但是翻譯【main】不行了
ok,后面就是代碼實現了
python實現
import requests import time from hashlib import md5 import random def get_md5(something): if isinstance(something, str): m = md5() m.update(something.encode('utf-8')) return m.hexdigest() def format_ts(): now = time.time() now = int(now * 1000) return now def format_bv(some_headers): ua = some_headers.get('User-Agent') ua = ua.split('/', 1)[1] # print(1231232, ua)
bv = get_md5(ua) return bv def format_salt(ts): # ts + parseInt(10 * Math.random(), 10)
number = random.randint(0, 9) if not isinstance(ts, str): ts = str(ts) salt = ts + str(number) return salt def format_sign(keywords, salt): # n.md5("fanyideskweb" + keywords + salt + "Tbh5E8=q6U3EXe+&L[4c@")
k = 'fanyideskweb' + keywords + salt + 'Tbh5E8=q6U3EXe+&L[4c@' sign = get_md5(k) return sign def main(keyword): headers = { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36', 'Origin': 'https://xxxx.com', 'Referer': 'https://xxxx.com' } # 獲取結果
url = 'xxxx ' # 保密
data = { 'i': keyword, 'from': 'AUTO', 'to': 'AUTO', 'smartresult': 'dict', 'client': 'fanyideskweb', 'salt': '', 'sign': '', 'lts': '', 'bv': '', 'doctype': 'json', 'version': '2.1', 'keyfrom': 'fanyi.web', 'action': 'FY_BY_REALTlME' } ts = format_ts() ___rl__test__cookies = ts + 3 salt = format_salt(ts) bv = format_bv(headers) sign = format_sign(keyword, salt) if ts and salt and bv and sign: data['lts'] = ts data['sign'] = sign data['salt'] = salt data['bv'] = bv req = requests.post(url, headers=headers, data=data) res = req.content.decode('utf-8') print(2313123, res) return res main('get')
執行:

跟預期的有點不太一樣啊,這個就很騷了,仔細檢查代碼,發現設置的這些值也沒問題啊,到底哪里呢,想下為啥瀏覽器可以,代碼為啥不行呢,有點了解的會立即想到cookie問題,看下是不是呢?
cookie字段
沒事,我們用requests的session對象來請求,它會自動保存cookie的
import requests import time from hashlib import md5 import random def get_md5(something): if isinstance(something, str): m = md5() m.update(something.encode('utf-8')) return m.hexdigest() def format_ts(): now = time.time() now = int(now * 1000) return now def format_bv(some_headers): ua = some_headers.get('User-Agent') ua = ua.split('/', 1)[1] # print(1231232, ua)
bv = get_md5(ua) return bv def format_salt(ts): # ts + parseInt(10 * Math.random(), 10)
number = random.randint(0, 9) if not isinstance(ts, str): ts = str(ts) salt = ts + str(number) return salt def format_sign(keywords, salt): # n.md5("fanyideskweb" + keywords + salt + "Tbh5E8=q6U3EXe+&L[4c@")
k = 'fanyideskweb' + keywords + salt + 'Tbh5E8=q6U3EXe+&L[4c@' sign = get_md5(k) return sign def main(keyword): headers = { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36', 'Origin': 'https://xxxx.com', 'Referer': 'https://xxxx.com' } # # 獲取cookie
session = requests.session() req = session.get('xxxx', headers=headers) # 主站地址,保密
print(session.cookies.items()) # 獲取結果
url = '' # 保密
data = { 'i': keyword, 'from': 'AUTO', 'to': 'AUTO', 'smartresult': 'dict', 'client': 'fanyideskweb', 'salt': '', 'sign': '', 'lts': '', 'bv': '', 'doctype': 'json', 'version': '2.1', 'keyfrom': 'fanyi.web', 'action': 'FY_BY_REALTlME' } ts = format_ts() salt = format_salt(ts) bv = format_bv(headers) sign = format_sign(keyword, salt) if ts and salt and bv and sign: data['lts'] = ts data['sign'] = sign data['salt'] = salt data['bv'] = bv req = session.post(url, headers=headers, data=data) res = req.content.decode('utf-8') print(2313123, res) return res main('name')
再執行看看,翻譯成功:

是不是以為就完了?
但是,我多了個心眼,感覺這個cookie不止這些,一定有貓膩,結果,果然,我上面的代碼還沒請求幾次就返回了個errorcode,那么看看瀏覽器上的cookie到底有哪些:

目前是這幾個值,我再翻譯下,看它會不會有新的變化

翻譯了哈,再過來看這邊的cookie:

這個__rl_test_cookies變了,為了安全起見,得把這個__rl_test_cookies也用python實現下
那么接着來,在js里看看咋來的,同樣的方法接着搜

很快就找到這段邏輯:

也就是這個a就是被賦值給了__rl_test_cookies,而a其實也是個時間戳字段,
要注意的是,先看請求接口時提交的參數:

lts是1625456226916
cookie里是,1625456226915

兩個時間段差距很小,去格式化的時候出來的時間時分秒相等,也就細微的不一樣,那我后續就在lts之上加或者減一個很小的值就可以了,然后有朋友要問,就用lts的時間戳不可以嗎,我試了是可以的,但是,為了不被發現還是盡量別用同一個
同時G = 2147483647 * Math.random()其實也是OUTFOX_SEARCH_USER_ID_NCOO的值,
可以在console里調試下:


至少長度是匹配的,經過我的測試也是可行的。
然后也就OUTFOX_SEARCH_USER_ID是沒有,另外兩個變量都有了,另外OUTFOX_SEARCH_USER_ID是我們請求主站的時候自己返回的。
也就是我們所有的可變的參數lts,bv,salt,sign,OUTFOX_SEARCH_USER_ID,__rl_test_cookies,OUTFOX_SEARCH_USER_ID_NCOO都拿到了,現在就好辦了。
最后的代碼:
import requests import time from hashlib import md5 import random def get_md5(something): if isinstance(something, str): m = md5() m.update(something.encode('utf-8')) return m.hexdigest() def format_ts(): now = time.time() now = int(now * 1000) return now def format_bv(some_headers): ua = some_headers.get('User-Agent') ua = ua.split('/', 1)[1] # print(1231232, ua)
bv = get_md5(ua) return bv def format_salt(ts): # ts + parseInt(10 * Math.random(), 10)
number = random.randint(0, 9) if not isinstance(ts, str): ts = str(ts) salt = ts + str(number) return salt def format_sign(keywords, salt): # n.md5("fanyideskweb" + keywords + salt + "Tbh5E8=q6U3EXe+&L[4c@")
k = 'fanyideskweb' + keywords + salt + 'Tbh5E8=q6U3EXe+&L[4c@' sign = get_md5(k) return sign def main(keyword): headers = { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 'sec-ch-ua-mobile': '?0', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36', 'Origin': 'https://xxx.com', 'Referer': 'https://xxx.com' } # # 獲取cookie
session = requests.session() req = session.get('', headers=headers) print(session.cookies.items()) time.sleep(2) # 獲取結果
url = '' #保密
data = { 'i': keyword, 'from': 'AUTO', 'to': 'AUTO', 'smartresult': 'dict', 'client': 'fanyideskweb', 'salt': '', 'sign': '', 'lts': '', 'bv': '', 'doctype': 'json', 'version': '2.1', 'keyfrom': 'fanyi.web', 'action': 'FY_BY_REALTlME' } ts = format_ts() ___rl__test__cookies = ts + 3 salt = format_salt(ts) bv = format_bv(headers) sign = format_sign(keyword, salt) if ts and salt and bv and sign: data['lts'] = ts data['sign'] = sign data['salt'] = salt data['bv'] = bv cookie = headers.get('Cookie') if not cookie: cookie = ''
for key, value in session.cookies.items(): if key and value: cookie += '%s=%s; ' % (key, value) cookie += '___rl__test__cookies=%s' % ___rl__test__cookies headers['Cookie'] = cookie req = session.post(url, headers=headers, data=data) res = req.content.decode('utf-8') print(2313123, res) return res main('put')
這個也是可以正常翻譯的,而且長期可用。
對了,cookie里的這兩個參數我雖然知道了怎么生成的,我沒有去生成了,看瀏覽器里的過期時間,最早的截止在2023年去了,所以我暫時不管了。

笑死,我2023年的時候還能繼續做程序員還不一定呢,說不定那個時候不知道在哪個工地上搬磚呢,哈哈哈(手動滑稽)
ok,完畢!!!!
另外,我們如果要部署到項目里的話,因為翻譯字段和請求參數是配套的,翻譯一次之后就可以在短期內一直用了,那么就可以做到一定的緩存機制了,也就是一個單詞翻譯過后我就可以再次用之前的請求參數一直翻譯了。當然有個問題就是,也不知道它服務器是怎么緩存的,有可能就存個一周還是多久,肯定不會永久存儲的,所以,我們可以控制在3天左右如果有相同翻譯字段的話就可以緩存下
附言:
看出是哪個平台的朋友請不要評論或者說明是哪個網站,謝謝,為了安全起見哈!
這個平台其實也挺簡單的,代碼都沒加密,也沒有涉及到深入的算法,都是直接md5就完事兒了
20210706更新
今天再看的時候,發現,代碼里面的那段加密字段變了

而且我用舊的key確實沒法得到正常的翻譯結果了,我用新的key替換舊的key,然后得到正常的結果。所以這個還不能寫死,就追溯下這個js文件怎么加載的,通過源頭,再實際的請求js的源地址,再去摳出新的key,運用到代碼里即可
