前言
我所用的方法基於瀏覽器環境的,非硬解(頭禿ing😶),文章較長,建議收藏。
這是我第一次接觸瑞數加密,比較難,不過學到的東西也是挺多的,也是因為我第一次解瑞數,所以文章寫得比較詳細甚至是啰嗦,這篇文章大致是以我逆向的思路去寫的,應該適合像我這樣從未接觸過瑞數的朋友。
這次逆向總結,估計會寫3到4篇文章。
- 接口簽名的生成與獲取
- cookie的生成與獲取
- 基於瀏覽器環境的爬蟲如何部署?
- 關於本次瑞數解密的總結
本文中也會有一些調試技巧夾在其中,如有問題或更好的建議歡迎提出!
本文以維普期刊的高級檢索接口作為示例,地址:http://qikan.cqvip.com/Qikan/Search/Advance?from=index
正文開始。
過debugger
定時器debugger
拿到網站后,一如既往的直接打開審查工具,在這一步直接被debugger
卡住了。
這是一個定時器無限debugger,如果兩次時間差大於100,那么就會一直讓我們處於debugger狀態。
當遇到這種反調試的手法時,可在進入debugger狀態后,在console中輸入以下代碼,以此跳過。
for (var i = 1; i < 99999; i++)window.clearInterval(i);
死循環debugger
過了這一步后,當我回到網頁,又會直接進入debugger
狀態。這個debugger是在一個判斷下,這個比較簡單我們直接右鍵選着“永不在此處暫停”。
注意:以上步驟都是在谷歌瀏覽器中調試的,使用火狐情況會不一致,建議使用谷歌瀏覽器。
這些if
判斷都是在一個while(1)
循環內,使用火狐會一直在循環內,谷歌只要設置了不在此處暫停就沒事,具體為啥不知道。
分析搜索接口
輸入關鍵詞,點擊檢索。很容易找到請求路徑為SearchList?...
的鏈接就是數據接口。
查看這個請求,需要搜索參數searchParamModel
和簽名G5tA5iQ4
。
查看這個請求的調用棧
這里可以先去看看window.advSearch
這個方法。這里的url
與 data
組合后,並不存在這個字段G5tA5iQ4
所以這個字段的值不是在這里構建,這個字段對應的值就是簽名,也是我們必須要解決的。
先這里提前解釋下,為什么這里明明沒有設置G5tA5iQ4
的值,卻在請求發送時,含有這個簽名。
原因很簡單,XMLHttpRequest
的send
方法被修改了。下圖是兩者的對比。
簽名在何處生成?
進入第一個函數
在這里打上斷點,再去點擊檢索按鈕,進入調試。
注意看這個arguments
,這是請求的參數。
由於點擊一次檢索按鈕會有好幾個請求,所以這里會有多個請求經過這里。
我們需要調試的是SearchList
這個接口,所以只要不是SearchList
的接口參數,直接跳過即可。
結合上文的接口分析,searchParamsModel
就是搜索參數。
不過,不要忘了。我們的目的是找簽名在何處生成。
小提示:上文說過XMLHttpRequest
的send
方法被修改過,實際上,上圖的_$hp
就是所謂的send
方法。
此時查看this
繼續往下找,查看_$a5
,(變量名每次請求都不同,知道是它就行),展開並查看它的第一個作用域。
可以看到,在此處的_$q3
的值就是一個帶有簽名的鏈接。
_$q3
屬於_$M3
,那么就可以按Ctrl+F
搜索關鍵字_$M3
注意:_$M3
是一個函數,所以你應該找到是類似下面這樣的函數
我們得知_$q3
作為參數傳進了_$M3
,所以在這個函數內打上斷點,重新調試
當我們進來的時候,查看_$q3
,發現_$q3
只是請求鏈接,所以由此可知,簽名就是在_$M3
中生成並拼接到_$q3
上的。
為了方便調試,我們可以將這里的斷點換成條件斷點,即只調試_$q3==="/Search/SearchList"
這種情況。
將內部函數折疊,可以看到_M$3
內部只是調用了一下_$Vq
函數。
展開_$Vq
函數,分析代碼。在這個_$Vq
函數里面,多折騰幾下總能找到簽名是何處生成的。
當然也可以用點小技巧,我們知道簽名最終會拼接到_$q3
上,所以必然存在類似這樣的代碼:
_$q3 +=
或者
_$q3 +
斷點調試到此處,可知簽名是調用_$5Q(_$hp, _$M_, _$No)
得來的。
查看這個函數的三個參數,可知它們分別是0,大小為16的整數數組以及一個undefined
在這里,_$M_
是整數數組,我們可以向上尋找,查看_$M_
是如何生成的。
可以找出整數數組是將/Search/SearchList
作為參數傳入某個函數而生成的。
中場休息
分析到這里,我們知道了簽名生成的流程如下:
- 當用戶點擊搜索按鈕,觸發點擊事件;
- 構建請求對象(請求對象的參數沒有簽名關鍵字);
- 由於
send
方法被修改,所以調用send
方法時,簽名就在這個過程中被生成;
簽名代碼來源分析
其實你應該發現了,分析了這么久的JS代碼,卻不知道這大段JS存放在哪里?
像下圖這樣,JS來源顯示為VM+數字
的形式,這就說明這些JS代碼是后來加載進引擎的。
換句話說就是,這些JS代碼並不是存在一個JS文件里的,實際上是通過eval
函數將一大堆字符串加載進了內存。
此時就需要尋找以上JS代碼是如何加載進內存。
這個也是瑞數加密的一大特色,這些加載JS代碼的代碼本身就是被混淆的,並且存在於Html頁面中。
查看搜索頁面源碼:view-source:http://qikan.cqvip.com/Qikan/Search/Advance?from=index
Tips:如果是加載是空白,那么你需要先正常訪問一次搜索頁面再查看源代碼。這個是Cookie的原因,具體的生成機制及解決辦法會在下一章講解到。
這一行代碼的后面,就是一堆JS代碼,我們可以將整個網頁代碼拷貝至本地編輯器。
將html代碼格式化后查看。
一開始就加載了一個JS文件,為了調試方便,可以將這個JS文件下載到本地
http://qikan.cqvip.com/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js
查看leE4DkIasHMb.f22c526.js
,一堆雜亂的字符,其主要的作用就是為window.$_ts
賦值。
html代碼引入leE4DkIasHMb.f22c526.js
后,緊接着就是一個自調用的被混淆的JS代碼。
這段代碼的核心作用就是將leE4DkIasHMb.f22c526.js
中的雜亂字符串通過特定方式還原為代碼並加載進內存。
此時的主要工作就是找到,雜亂字符串變成規則字符串代碼的位置。
為什么要這么做,在這里舉個簡單的例子。
舉例時間到!
現在這樣一串字符串, Y29uc29sZS5sb2coJ2hpaGloaWhpLi4uJyk=
這很常見,這是通過base64編碼后得到的字符串,那么我們可以通過base64解碼得到本來的字符串,然后使用eval
函數執行即可。
以上是原本的功能,打印輸出了hihihihi...
現在我們需要修改這個代碼輸出的內容,而這可以通過字符串替換方式實現,就像這樣。
那么下面的流程圖,我想應該可以理解了。
上方的例子是因為我們知道編碼方式是base64
,所以可以輕松的將密文轉為明文。
對於不常見的加密方式,我們就只有去調試找出明文生成的位置,再加上JS代碼本事就是混淆的,所以難度就有所提升。
舉例結束。
簽名代碼在何處加載到內存?
仔細想一想,一段字符串想以js代碼的形式加載進內存,必定會使用eval
方法。
所以,我們只需要找到哪里使用了調用eval
即可。
搜索關鍵詞eval
,運氣很好可以直接搜索到,下面賦值的操作執行即,_$vo
就是eval
此時搜索關鍵詞_$vo
,發現有93個匹配項
_$vo
作為函數,如果被調用,那么調用的寫法可能是這樣_$vo()
則搜索關鍵詞_$vo(
,匹配項為0個。
函數的調用還有一種函數名.call
的方式,所以不妨搜索_$vo.call
試試看。
找到一個匹配結果,在這里打上斷點,調試過來看看傳入的參數是什么。
可見,_$kw
的值就是代碼字符串了。
我們在eval
執行前注入自己的代碼即可達成目的。
注入代碼
為了大家方便閱讀,我將上一步得到的明文代碼字符串稱為簽名代碼
雖然這代碼還有其他的功能,但對我們來說,只想通過這段代碼獲取簽名,僅此而已。
注入代碼是為了讓我們可以更方便的獲取到簽名,最簡單的辦法就是將簽名設置為一個全局變量。
除此之外,為了方便后續調試還可以剔除其中煩人的debugger
設置簽名為全局變量
如果有些忘記了簽名是如何生成的,可以先翻到第三節回憶回憶。
由於代碼的變量是變化的,所以我們不能直接使用replace,而是應該用正則匹配的方式去替換或插入代碼。
以上是兩次請求簽名生成的代碼行。
在變化中找不變,_$f_[5]
和_$A4[5]
它們都是取索引值5
所以寫出正則如下:
(_\$[\w\d_$]{2}) \+= _\$[\w\d_$]{2}\[5\] \+ (_\$[\w\d_$]{2}\([^)]+\);)
Tips:這是基於格式化的js代碼寫的正則,實際的簽名代碼是被壓縮的,所以應該把多余的空格刪除。
(_\$[\w\d_$]{2})\+=_\$[\w\d_$]{2}\[5\]\+(_\$[\w\d_$]{2}\([^)]+\);)
匹配出這一段后,我們可以在原本的代碼后面,再加上一句全局變量賦值。
那么可以寫出JS代碼
簽名代碼.replace(/(_\$[\w\d\$]+)\+=_\$[\w\d\$]+\[5\]\+(_\$[\w\d\$]+\([^)]+\);)/gm, `$1="?"+$2window.genUrl=$1;`);
剔除debugger
在最初分析搜索接口時,就遇見了兩個debugger
一個是明文顯示的,這個比較簡單,使用簽名代碼.replace('debugger', '')
剔除即可。
另一個定時器debugger
則稍稍有點麻煩。
經過多次調式,可以發現整個代碼也是在一個while
循環中跑,這是瑞數的一大特色。
且if語句比較的值是沒有變化的,都是變量小於256,這為我們注入代碼提供了方便。
我們以注入的方式,while內的第一個if語句的上方插入以下代碼:
console.log(_$Mx)
這個_$Mx
是一直做比較的,通過層層的if else,最終執行某段代碼。
當進入調試工具后,只要進入了此循環,就會打印_$Mx
而當進入了定時器debugger
,此時循環停止,通過最后輸出的數字,就可以找到進入定時器debugger的入口
Tips:這一步可能會卡着,稍微等等。
注入后的代碼
進入定時器debugger后
最后的數字是388,記着這個388,一路跟着if走就可以看到類似如下代碼:
這個代碼就是進入定時器debugger
的入口,那么我們只需要將這行代碼注釋或者刪除即可
簽名代碼.replace(/(<389\){)[^}]+/gm, `$1`);
至此所有的debugger
都已去除。
小結
上方的所有代碼注入都是在html源碼
上進行的。
這里先理一理我們的流程:
- 請求搜索頁面,獲得頁面html源碼
- python對html源碼進行修改
- 將html放入瀏覽器運行
- 調用簽名方法獲取簽名
上方的注入是在html源碼中進行的。實際情況是使用python來完成代碼注入。
畫個圖來說明下,即使用Python修改html源碼,使得html中的js代碼能過將目標代碼注入到簽名代碼中
代碼注入示例:
# -*- coding: utf-8 -*-
"""
Created on 2021/5/24 17:15
---------
@summary: 注入代碼
1. 去除定時無限debugger
2. 去除死循環debugger
3. 插入searchList方法,用於生成簽名
4. 將得到的簽名提升至全局變量,可通過 `genUrl` 訪問
---------
@author: mkdir700
@email: mkdir700@gmail.com
"""
import re
def purify_html():
with open("raw.html", encoding="utf-8") as f:
text = f.read()
r = re.findall("_\$[\w\d\$]{2}\(79,_\$[\w\d\$]+\);", text)[0]
var_name = re.findall("_\$[\w\d\$]{2}\(79,(_\$[\w\d\$]+)\);", r)[0]
pp = """%(var_name)s = %(var_name)s.replace(/(_\$[\w\d\$]+)\+=_\$[\w\d\$]+\[5\]\+(_\$[\w\d\$]+\([^)]+\);)/gm, `$1="?"+$2window.genUrl=$1;`);%(var_name)s = %(var_name)s.replace(/(<389\){)[^}]+/gm, `$1`);%(var_name)s = %(var_name)s.replace("debugger", "");""" % {
'var_name': var_name}
result = text.replace(r, pp + r)
result = result.replace("/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js",
"http://qikan.cqvip.com/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js")
result = re.sub(r"(/dist)", "http://qikan.cqvip.com/dist", result)
result = result.replace("</body>",
'<script>searchList=function(a){$.ajax({url:"/Search/SearchList",type:"post",dataType:"html",data:{searchParamModel:a},beforeSend:function(){loadding()},complete:function(){loaddingClose()},success:function(){console.log("請求成功")},error:function(){loaddingClose()}})};</script></body>')
# print(result)
with open('pure.html', "w", encoding="utf-8") as f:
f.write(result)
print("HTML頁面代碼注入完成")
Tips:簽名的觸發機制是發送請求,所以注入了一個ajax請求,可供我們手動調用。
簽名測試
- 打開搜索頁面
- 右鍵查看搜索頁面源碼
- 使用python腳本注入代碼,生成新的html文件
- 在新的html文件同目錄下,啟動簡單的web服務
python -m http.server 9000
- 訪問
http://localhost:9000/pure.html
- 打開審查工具,調用
searchList
方法,接着訪問genUrl
變量
- 在瀏覽器中獲取cookie
- 將cookie和簽名帶入,測試請求是否成功,發送請求的腳本如下:
# -*- coding: utf-8 -*-
"""
Created on 2021/5/23 16:38
---------
@summary:
---------
@author: mkdir700
@email: mkdir700@gmail.com
"""
import requests
payload = "searchParamModel=%7B%22ObjectType%22%3A1%2C%22SearchKeyList%22%3A%5B%7B%22FieldIdentifier%22%3A%22M%22%2C%22SearchKey%22%3A%22%E5%8C%97%E5%A4%A7%22%2C%22PreLogicalOperator%22%3A%22%22%2C%22IsExact%22%3A%220%22%7D%5D%2C%22SearchExpression%22%3A%22%22%2C%22BeginYear%22%3A%22%22%2C%22EndYear%22%3A%22%22%2C%22JournalRange%22%3A%22%22%2C%22DomainRange%22%3A%22%22%2C%22PageSize%22%3A%220%22%2C%22PageNum%22%3A%221%22%2C%22Sort%22%3A%220%22%2C%22ClusterFilter%22%3A%22%22%2C%22SType%22%3A%22%22%2C%22StrIds%22%3A%22%22%2C%22UpdateTimeType%22%3A%22%22%2C%22ClusterUseType%22%3A%22Article%22%2C%22IsNoteHistory%22%3A1%2C%22AdvShowTitle%22%3A%22%E9%A2%98%E5%90%8D%E6%88%96%E5%85%B3%E9%94%AE%E8%AF%8D%3D%E5%8C%97%E5%A4%A7%22%2C%22ObjectId%22%3A%22%22%2C%22ObjectSearchType%22%3A%220%22%2C%22ChineseEnglishExtend%22%3A%220%22%2C%22SynonymExtend%22%3A%220%22%2C%22ShowTotalCount%22%3A%220%22%2C%22AdvTabGuid%22%3A%224361c899-1a1a-2b2b-eefe-cf94a80612f7%22%7D"
cookies = input("請鍵入cookies:\r\n")
genUrl = input("請鍵入genUrl:\r\n")
session = requests.Session()
session.headers = {
"Accept": "text/html, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": cookies,
"Host": "qikan.cqvip.com",
"Origin": "http://qikan.cqvip.com",
"Pragma": "no-cache",
"Referer": "http://qikan.cqvip.com/Qikan/Search/Advance?from=index",
"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.212 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
}
resp = session.request(
"POST",
"http://qikan.cqvip.com/Search/SearchList"+genUrl,
data=payload
)
print(resp.url)
print(resp.status_code)
效果展示
狀態碼:成功-200,異常-400