【瑞數】維普期刊搜索接口逆向_1_簽名篇


前言

我所用的方法基於瀏覽器環境的,非硬解(頭禿ing😶),文章較長,建議收藏。

這是我第一次接觸瑞數加密,比較難,不過學到的東西也是挺多的,也是因為我第一次解瑞數,所以文章寫得比較詳細甚至是啰嗦,這篇文章大致是以我逆向的思路去寫的,應該適合像我這樣從未接觸過瑞數的朋友。

這次逆向總結,估計會寫3到4篇文章。

  1. 接口簽名的生成與獲取
  2. cookie的生成與獲取
  3. 基於瀏覽器環境的爬蟲如何部署?
  4. 關於本次瑞數解密的總結

本文中也會有一些調試技巧夾在其中,如有問題或更好的建議歡迎提出!

本文以維普期刊的高級檢索接口作為示例,地址:http://qikan.cqvip.com/Qikan/Search/Advance?from=index


正文開始。


過debugger

定時器debugger

拿到網站后,一如既往的直接打開審查工具,在這一步直接被debugger卡住了。

image-20210524191531900

這是一個定時器無限debugger,如果兩次時間差大於100,那么就會一直讓我們處於debugger狀態。

當遇到這種反調試的手法時,可在進入debugger狀態后,在console中輸入以下代碼,以此跳過。

for (var i = 1; i < 99999; i++)window.clearInterval(i);

參考:爬蟲漫游指南:瑞數的反調試陷阱

死循環debugger

過了這一步后,當我回到網頁,又會直接進入debugger狀態。這個debugger是在一個判斷下,這個比較簡單我們直接右鍵選着“永不在此處暫停”

image-20210524192101416

注意:以上步驟都是在谷歌瀏覽器中調試的,使用火狐情況會不一致,建議使用谷歌瀏覽器。

這些if判斷都是在一個while(1)循環內,使用火狐會一直在循環內,谷歌只要設置了不在此處暫停就沒事,具體為啥不知道。

分析搜索接口

輸入關鍵詞,點擊檢索。很容易找到請求路徑為SearchList?...的鏈接就是數據接口。

查看這個請求,需要搜索參數searchParamModel和簽名G5tA5iQ4

image-20210524193554482

查看這個請求的調用棧

image-20210524195302725

這里可以先去看看window.advSearch這個方法。這里的urldata組合后,並不存在這個字段G5tA5iQ4

所以這個字段的值不是在這里構建,這個字段對應的值就是簽名,也是我們必須要解決的。

image-20210524195352293

先這里提前解釋下,為什么這里明明沒有設置G5tA5iQ4的值,卻在請求發送時,含有這個簽名。

原因很簡單,XMLHttpRequestsend方法被修改了。下圖是兩者的對比。

image-20210526153930804

簽名在何處生成?

進入第一個函數

image-20210524193236051

在這里打上斷點,再去點擊檢索按鈕,進入調試。

image-20210524200135688

注意看這個arguments,這是請求的參數。

由於點擊一次檢索按鈕會有好幾個請求,所以這里會有多個請求經過這里。

我們需要調試的是SearchList這個接口,所以只要不是SearchList的接口參數,直接跳過即可。

結合上文的接口分析,searchParamsModel就是搜索參數。

image-20210524200530360

不過,不要忘了。我們的目的是找簽名在何處生成。

小提示:上文說過XMLHttpRequestsend方法被修改過,實際上,上圖的_$hp就是所謂的send方法。

此時查看this

image-20210524200905790

繼續往下找,查看_$a5,(變量名每次請求都不同,知道是它就行),展開並查看它的第一個作用域。

image-20210524201405988

可以看到,在此處的_$q3的值就是一個帶有簽名的鏈接。

_$q3屬於_$M3,那么就可以按Ctrl+F搜索關鍵字_$M3

注意:_$M3是一個函數,所以你應該找到是類似下面這樣的函數

image-20210524201922829

我們得知_$q3作為參數傳進了_$M3,所以在這個函數內打上斷點,重新調試

當我們進來的時候,查看_$q3,發現_$q3只是請求鏈接,所以由此可知,簽名就是在_$M3中生成並拼接到_$q3上的。

image-20210524202226754

為了方便調試,我們可以將這里的斷點換成條件斷點,即只調試_$q3==="/Search/SearchList"這種情況。

image-20210524202523045

將內部函數折疊,可以看到_M$3內部只是調用了一下_$Vq函數。

image-20210524202719617

展開_$Vq函數,分析代碼。在這個_$Vq函數里面,多折騰幾下總能找到簽名是何處生成的。

當然也可以用點小技巧,我們知道簽名最終會拼接到_$q3上,所以必然存在類似這樣的代碼:

_$q3 += 
或者
_$q3 +

image-20210524203035177

斷點調試到此處,可知簽名是調用_$5Q(_$hp, _$M_, _$No)得來的。

查看這個函數的三個參數,可知它們分別是0大小為16的整數數組以及一個undefined

image-20210524203208234

在這里,_$M_是整數數組,我們可以向上尋找,查看_$M_是如何生成的。

可以找出整數數組是將/Search/SearchList作為參數傳入某個函數而生成的。

image-20210524203502074

中場休息

分析到這里,我們知道了簽名生成的流程如下:

  1. 當用戶點擊搜索按鈕,觸發點擊事件;
  2. 構建請求對象(請求對象的參數沒有簽名關鍵字);
  3. 由於send方法被修改,所以調用send方法時,簽名就在這個過程中被生成;

簽名代碼來源分析

其實你應該發現了,分析了這么久的JS代碼,卻不知道這大段JS存放在哪里?

像下圖這樣,JS來源顯示為VM+數字的形式,這就說明這些JS代碼是后來加載進引擎的。

換句話說就是,這些JS代碼並不是存在一個JS文件里的,實際上是通過eval函數將一大堆字符串加載進了內存。

image-20210526154956627

此時就需要尋找以上JS代碼是如何加載進內存。

這個也是瑞數加密的一大特色,這些加載JS代碼的代碼本身就是被混淆的,並且存在於Html頁面中。

查看搜索頁面源碼:view-source:http://qikan.cqvip.com/Qikan/Search/Advance?from=index

Tips:如果是加載是空白,那么你需要先正常訪問一次搜索頁面再查看源代碼。這個是Cookie的原因,具體的生成機制及解決辦法會在下一章講解到。

image-20210526161702620

這一行代碼的后面,就是一堆JS代碼,我們可以將整個網頁代碼拷貝至本地編輯器。

將html代碼格式化后查看。

一開始就加載了一個JS文件,為了調試方便,可以將這個JS文件下載到本地

http://qikan.cqvip.com/NJDrTcXo8msX/leE4DkIasHMb.f22c526.js

image-20210526162149084

查看leE4DkIasHMb.f22c526.js,一堆雜亂的字符,其主要的作用就是為window.$_ts賦值。

image-20210526162420017

html代碼引入leE4DkIasHMb.f22c526.js后,緊接着就是一個自調用的被混淆的JS代碼。

這段代碼的核心作用就是將leE4DkIasHMb.f22c526.js中的雜亂字符串通過特定方式還原為代碼並加載進內存。

此時的主要工作就是找到,雜亂字符串變成規則字符串代碼的位置。

為什么要這么做,在這里舉個簡單的例子。


舉例時間到!

現在這樣一串字符串, Y29uc29sZS5sb2coJ2hpaGloaWhpLi4uJyk=

這很常見,這是通過base64編碼后得到的字符串,那么我們可以通過base64解碼得到本來的字符串,然后使用eval函數執行即可。

動畫1

以上是原本的功能,打印輸出了hihihihi...

現在我們需要修改這個代碼輸出的內容,而這可以通過字符串替換方式實現,就像這樣。動畫2

那么下面的流程圖,我想應該可以理解了。

image-20210526171516534

上方的例子是因為我們知道編碼方式是base64,所以可以輕松的將密文轉為明文。

對於不常見的加密方式,我們就只有去調試找出明文生成的位置,再加上JS代碼本事就是混淆的,所以難度就有所提升。

舉例結束。


簽名代碼在何處加載到內存?

仔細想一想,一段字符串想以js代碼的形式加載進內存,必定會使用eval方法。

所以,我們只需要找到哪里使用了調用eval即可。

搜索關鍵詞eval,運氣很好可以直接搜索到,下面賦值的操作執行即,_$vo就是eval

image-20210526182806058

此時搜索關鍵詞_$vo,發現有93個匹配項

image-20210526183100317

_$vo作為函數,如果被調用,那么調用的寫法可能是這樣_$vo()

則搜索關鍵詞_$vo(,匹配項為0個。

image-20210526183242443

函數的調用還有一種函數名.call的方式,所以不妨搜索_$vo.call試試看。

image-20210526183429659

找到一個匹配結果,在這里打上斷點,調試過來看看傳入的參數是什么。

可見,_$kw的值就是代碼字符串了。

image-20210526183540925

我們在eval執行前注入自己的代碼即可達成目的。

注入代碼

為了大家方便閱讀,我將上一步得到的明文代碼字符串稱為簽名代碼

雖然這代碼還有其他的功能,但對我們來說,只想通過這段代碼獲取簽名,僅此而已。

注入代碼是為了讓我們可以更方便的獲取到簽名,最簡單的辦法就是將簽名設置為一個全局變量。

除此之外,為了方便后續調試還可以剔除其中煩人的debugger

設置簽名為全局變量

如果有些忘記了簽名是如何生成的,可以先翻到第三節回憶回憶。

由於代碼的變量是變化的,所以我們不能直接使用replace,而是應該用正則匹配的方式去替換或插入代碼。

image-20210526185806409

image-20210526190013277

以上是兩次請求簽名生成的代碼行。

在變化中找不變,_$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循環中跑,這是瑞數的一大特色。

image-20210526195421150

且if語句比較的值是沒有變化的,都是變量小於256,這為我們注入代碼提供了方便。

我們以注入的方式,while內的第一個if語句的上方插入以下代碼:

console.log(_$Mx)

這個_$Mx是一直做比較的,通過層層的if else,最終執行某段代碼。

當進入調試工具后,只要進入了此循環,就會打印_$Mx

而當進入了定時器debugger,此時循環停止,通過最后輸出的數字,就可以找到進入定時器debugger的入口

image-20210526195902790

Tips:這一步可能會卡着,稍微等等。

注入后的代碼

image-20210526201251851

進入定時器debugger后

image-20210526200611677

最后的數字是388,記着這個388,一路跟着if走就可以看到類似如下代碼:

image-20210526201801875

這個代碼就是進入定時器debugger的入口,那么我們只需要將這行代碼注釋或者刪除即可

簽名代碼.replace(/(<389\){)[^}]+/gm, `$1`);

至此所有的debugger都已去除。

小結

上方的所有代碼注入都是在html源碼上進行的。

這里先理一理我們的流程:

  1. 請求搜索頁面,獲得頁面html源碼
  2. python對html源碼進行修改
  3. 將html放入瀏覽器運行
  4. 調用簽名方法獲取簽名

上方的注入是在html源碼中進行的。實際情況是使用python來完成代碼注入。

畫個圖來說明下,即使用Python修改html源碼,使得html中的js代碼能過將目標代碼注入到簽名代碼中

image-20210526204401228

代碼注入示例:

# -*- 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請求,可供我們手動調用。

簽名測試

  1. 打開搜索頁面
  2. 右鍵查看搜索頁面源碼
  3. 使用python腳本注入代碼,生成新的html文件
  4. 在新的html文件同目錄下,啟動簡單的web服務
python -m http.server 9000
  1. 訪問http://localhost:9000/pure.html
  2. 打開審查工具,調用searchList方法,接着訪問genUrl變量

  1. 在瀏覽器中獲取cookie
  2. 將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

動畫3


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM