人均瑞數系列,瑞數 4 代 JS 逆向分析

K哥爬蟲發表於2022-07-09

宣告

本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整程式碼,抓包內容、敏感網址、資料介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!

本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯絡作者立即刪除!

前言

01

瑞數動態安全 Botgate(機器人防火牆)以“動態安全”技術為核心,通過動態封裝、動態驗證、動態混淆、動態令牌等技術對伺服器網頁底層程式碼持續動態變換,增加伺服器行為的“不可預測性”,實現了從使用者端到伺服器端的全方位“主動防護”,為各類 Web、HTML5 提供強大的安全保護。

瑞數 Botgate 多用於政企、金融、運營商行業,曾一度被視為反爬天花板,隨著近年來逆向大佬越來越多,相關的逆向文章也層出不窮,真正到了人均瑞數的時代了,這裡也感謝諸如 Nanda、懶神等逆向大佬,揭開了瑞數神祕的面紗,總結的經驗讓後來人少走了不少彎路。

過瑞數的方法基本上有以下幾種:自動化工具(要隱藏特徵值)、RPC 遠端呼叫、JS 逆向(硬扣程式碼和補環境),本文介紹的是 JS 逆向硬扣程式碼,儘可能多的介紹各種細節。

瑞數特徵以及不同版本的區別

對於絕大多數使用了瑞數的網站來說,有以下幾點特徵(可能有特殊版本不一樣,先僅看主流的):

1、開啟開發者工具(F12)會依次出現兩個典型的無限 debugger:

02

03

2、瑞數的 JS 混淆程式碼中,變數、方法名大多類似於 _$xx,有眾多的 if-else 控制流,新版瑞數還可能會有 jsvmp 以及眾多三目表示式的情況:

04

3、看請求,會有典型的三次請求,首次請求響應碼是 202(瑞數3、4代)或者 412(瑞數5代),接著單獨請求一個 JS 檔案,然後再重新請求頁面,後續的其他 XHR 請求中,都帶有一個字尾,這個字尾的值是由 JS 生成的,每次都會變化,字尾的值第一個數字為瑞數的版本,比如 MmEwMD=4xxxxx 就是4代瑞數,bX3Xf9nD=5xxxxx 就是5代瑞數:

05

06

07

08

4、看 Cookie,瑞數 3、4 代有以 T 和 S 結尾的兩個 Cookie,其中以 S 開頭的 Cookie 是第一次的 201 那個請求返回的,以 T 開頭的 Cookie 是由 JS 生成的,動態變化的,T 和 S 前面一般會跟 80 或 443 的數字,Cookie 值第一個數字為瑞數的版本(為什麼可以通過第一個數字來判斷版本?難道相同版本第一個數字不會變嗎?這些問題我們在分析 JS 的時候可以找到答案),比如:

  • FSSBBIl1UgzbN7N80T=37Na97B.nWX3....:數字 80 是 http 協議的預設埠號,對應 http 請求,其值第一位為 3,表示 3 代瑞數;
  • FSSBBIl1UgzbN7N443T=4a.tr1kEXk.....:數字 443 是 https 協議的預設埠號,對應 https 請求,其值第一位為 4,表示 4 代瑞數。

09

瑞數 5 代也有以 T 和 S 結尾的兩個 Cookie,但有些特殊的 5 代瑞數也有以 O 和 P 結尾的,同樣的,以 O 開頭的是第一次的 412 那個請求返回的,以 P 開頭的是由 JS 生成的,Cookie 值第一個數字同樣為瑞數的版本,和 3、4 代不同的是,5 代沒有加埠號了,比如:

  • vsKWUwn3HsfIO=57C6DwDUXS.....:以 O 結尾,其值第一位為 5,表示 5 代瑞數;
  • WvY7XhIMu0fGT=53.9fybty......:以 T 結尾,其值第一位為 5,表示 5 代瑞數。

10

11

5、看入口,瑞數有個流程是在虛擬機器 VM 中載入 1w+ 行的程式碼,載入此程式碼的入口,不同版本也不一樣(這個入口具體在哪裡?怎麼定位?在後續逆向分析中再詳細介紹),示例如下:

  • 3 代:_$aW = _$c6[_$l6()](_$wc, _$mo);_$c6 實際上是 eval_$l6() 實際上是 call

12

  • 4 代:ret = _$DG.call(_$6a, _$YK);_$DG 實際上是 eval,有關鍵字 retcall 是明文;

13

  • 5 代:5 代種類比較多了,最初和 4 代的類似,比如 ret = _$Yg.call(_$kc, _$mH);,有關鍵字 ret,call 是明文,也有沒有 ret 關鍵字的版本,比如 _$ap = _$j5.call(_$_T, _$gp);,也有像 3 代那樣全部混淆了的,比如:_$x8 = _$mP[_$nU[15]](_$z3, _$Ec);_$mP 實際上是 eval_$nU[15] 實際上是 call,混淆的 call 與 3 代的區別就是 5 代是在一個陣列裡取值得到的;

14

15

16

當然要想精準區分不同版本,得各個條件結合起來看,最主要的還是得看看內部的實現邏輯,以及頁面的程式碼結構,比如 4 代有一個生成假 Cookie 的步驟,而 5 代沒有,有的特殊版本雖然看起來是 5 代,但是加了 jsvmp 和三目表示式,和傳統的 5 代又有區別,偶爾愚人節啥的突然來個新版本,也會不一樣,各版本在分析一遍之後,就很容易區分了。

Cookie 入口定位

本文案例中瑞數 4 代網站為:aHR0cDovL3d3dy5mYW5nZGkuY29tLmNuL25ld19ob3VzZS9uZXdfaG91c2VfZGV0YWlsLmh0bWw=

首先過掉無限 debugger(過不過其實無所謂,後面的分析其實這個基本上沒影響),直接右鍵 Never pause here 永不在此處斷下即可:

17

定位 Cookie,首選 Hook 來的最快,通過 Fiddler 等抓包工具、油猴指令碼、瀏覽器外掛等方式注入以下 Hook 程式碼:

(function() {
    // 嚴謹模式 檢查所有錯誤
    'use strict';
    // document 為要hook的物件 這裡是hook的cookie
    var cookieTemp = "";
    Object.defineProperty(document, 'cookie', {
        // hook set方法也就是賦值的方法 
        set: function(val) {
                // 這樣就可以快速給下面這個程式碼行下斷點
                // 從而快速定位設定cookie的程式碼
                console.log('Hook捕獲到cookie設定->', val);
                debugger;
                cookieTemp = val;
                return val;
        },
        // hook get 方法也就是取值的方法 
        get: function()
        {
            return cookieTemp;
        }
    });
})();

Hook 發現會有生成兩次 Cookie 的情況,斷下之後往上跟棧,可以看到組裝 Cookie 的程式碼,類似如下結構:

18

仔細觀察這兩次 Cookie 生成的地方,分別往上跟棧,你就會發現兩個 Cookie 分別是經過了兩個不同方法得到的,如下圖所示:

19

20

這裡的程式碼存在於 VM 虛擬機器中,且是 IIFE 自執行程式碼,我們還得往前跟棧看看這些 VM 程式碼是從哪裡載入出來的,跟棧來到首頁(202頁面)帶有 call 的位置:

21

我們在文章開頭介紹的這個位置就是這麼分析得來的,這個位置通常在分析瑞數的時候作為入口,圖中 _$te 實際上是 eval 方法,傳入的第一個引數 _$fY 是 Window 物件,第二個物件 _$F8 是我們前面看到的 VM 虛擬機器中的 IIFE 自執行程式碼。

在知道了瑞數大致的入口之後,我們也可以使用事件監聽中的 Script 斷點,一直下一個斷點(F8)就可以走到 202 頁面,然後搜尋 call 關鍵字就能快速定位到入口,Script 斷點中的兩個選項,第一個表示執行 JS 指令碼的第一條語句時斷下,第二個表示 JS 因為內容安全政策而被遮蔽時斷下,一般選擇第一個就可以了,如下圖所示:

22

檔案結構與邏輯

想要後續分析 Cookie 的生成,我們不得不要觀察一下 202 頁面的程式碼,meta 標籤有個 content 內容,引用了一個類似於 c.FxJzG50F.dfe1675.js 的 JS 檔案,接著跟一個自執行的 JS,如下圖所示:

23

第1部分 meta 標籤的 content 內容,每次都是變化的,第2部分引用的這個外部 JS 在不同頁面也有所差別,但是同一個網站同一個頁面 JS 裡的內容一般是固定不會變的,第3部分自執行程式碼每次變化的只是變數名,整體邏輯不變,後續我們在扣程式碼的時候,也會用到這裡的部分方法。自執行程式碼裡同樣也是有很多 if-else 控制流,開頭的那個陣列,比如上圖中的 _$Dk 就是用來控制後續的控制流的。

引用的 c.FxJzG50F.dfe1675.js 直接開啟看是亂碼的,而自執行 JS 的主要作用是將這 JS 亂碼還原成 VM 裡的 1w+ 行的正常程式碼,並且定義了一個全域性變數 window.$_ts 並賦了許多值,這個變數在後續 VM 中作用非常大,meta 標籤的 content 內容同樣也會在 VM 裡用到。

由於很多值、變數都是動態變化的,肯定不利於我們的分析,所以我們需要固定一套程式碼到本地,打斷點、跟棧都會更加方便,隨便儲存一份 202 頁面的程式碼,以及該頁面對應的外鏈 JS 檔案,如 c.FxJzG50F.dfe1675.js 到本地,使用瀏覽器自帶的 overrides 重寫功能、或者瀏覽器外掛 ReRes、或者抓包工具的響應替換功能(如 Fiddler 的 AutoResponder)進行替換。

24

VM 裡面的程式碼是生成 Cookie 的主要程式碼,包含眾多的 if-else 控制流,無疑增加了我們分析程式碼的成本,這裡就可以使用 AST 技術做一下反混淆,比如 Nanda 就將 if-else 控制流轉換成了 switch-case 的,同一個控制流下的程式碼放在了同一個 case 下,然後在 call 入口那個地方,將 VM 程式碼做一下本地替換,具體可以參考 Nanda 的文章:《某數4代邏輯分析》,感興趣的可以試試,不瞭解 AST 的可以看看以前的文章《逆向進階,利用 AST 技術還原 JavaScript 混淆程式碼》,後續有時間 K 哥再寫寫 AST 還原瑞數程式碼的實戰,本文我們們選擇硬剛!

25

VM 程式碼以及 $_ts 變數獲取

前面我們瞭解了 VM 程式碼和 $_ts 的重要性,所以我們第一步是要想辦法拿到他們,至於在什麼時候有用到,文章後續再說,複製外鏈 JS,即 c.FxJzG50F.dfe1675.js 的程式碼和 202 頁面的自執行程式碼到檔案,本地直接執行即可,需要輕度補一下環境,缺啥補啥,大致補一下 window、location、document 就行了,補的具體內容可以直接在瀏覽器控制檯使用 copy() 命令複製過來,然後 VM 程式碼我們就可以直接 Hook eval 的方式得到,大致的補環境程式碼如下:

var eval_js = ""

window = {
    $_ts:{},
    eval:function (data) {
        eval_js = data
    }
}

location = {
    "ancestorOrigins": {},
    "href": "http://www.脫敏處理.com.cn/new_house/new_house_detail.html",
    "origin": "http://www.脫敏處理.com.cn",
    "protocol": "http:",
    "host": "www.脫敏處理.com.cn",
    "hostname": "www.脫敏處理.com.cn",
    "port": "",
    "pathname": "/new_house/new_house_detail.html",
    "search": "",
    "hash": ""
}

document = {
    "scripts": ["script", "script"]
}

25

觀察 $_ts 的 key 和 value,和瀏覽器中得到的是一樣的:

26

注意事項:c.FxJzG50F.dfe1675.js 外鏈 JS 如果你直接下載下來用編輯器開啟可能會被自動編碼,和原始資料有出入,導致執行報錯,這裡建議直接在瀏覽器線上訪問這個檔案,手動複製過來,或者在抓包軟體裡將響應內容複製過來,觀察以下兩種情況,第一種情況就可能會導致執行出錯,第二種是正常的:

27

扣程式碼

前面說了這麼多,現在終於可以進入主題了,那就是扣程式碼,找個好椅子,準備把屁股坐穿,此時你的鍵盤只有 F11 有用,不斷單步除錯,只需要億點點細節,就完事兒了!

扣程式碼步驟太多,不可能每一步都截圖寫出來,只寫一下比較重要的,如有遺漏的地方,那也沒辦法,首先先在我們替換的 202 頁面裡,自執行程式碼開始的地方手動加個 debugger,一進入頁面就斷下,方便後續的分析:

28

通過前面我們的分析,已經知道了入口在 call 的地方,快速搜尋並下斷點:

29

通過前面我們的分析,我們也知道了有兩次生成 Cookie 的地方,快速搜尋 (5),搜尋結果第二個即為入口:

30

假 Cookie 生成邏輯

首先單步跟假 Cookie,雖然是假的,但是後續生成真 Cookie 中會用到,在跟的時候你會走到這個邏輯裡面:

31

有一步會呼叫 _$8e() 方法,而 _$8e = _$Q9_$Q9 又巢狀在 _$d0 裡的,搜尋一下哪裡呼叫了 _$d0,發現是程式碼開頭:

32

那麼傳入的引數 _$Wn 是啥呢?單步跟入,是一個方法,作用就是取 202 頁面的 content 內容,那麼我們在本地就直接刪掉這個 _$Wn 方法,直接傳入 content 的值即可,如下圖所示:

33

另外,我們發現,程式碼有非常多的在陣列裡面按索引取值的情況,比如上圖中的 _$PV[68] 的值,實際上就是字串 content,很顯然我們要把這個陣列的來源找到,直接搜尋 _$PV = ,可以找到疑似定義和賦值的地方:

34

35

所以我們得看看這個 _$iL 方法,傳入了一個非常長的字串,打斷點進去看看,果然生成了 _$PV,是一個 725 位的陣列:

36

接下來在扣程式碼的過程中,你會經常遇到一個變數,在本文中是 _$sX

37

有沒有很熟悉?這個值就是我們前面拿到的 $_ts 變數,在開頭就可以看到是將 window.$_ts 賦值給了 _$sX

38

繼續走,會走到以下邏輯中:

39

這裡會遇到六個陣列,他們都已經有值了,所以我們得找到他們是咋來的,任意搜尋其中一個陣列名稱,會找到定義和賦值的地方:

40

41

賦值明顯是呼叫了 _$rv 方法,再搜 _$rv 方法,發現是開頭就呼叫了:

42

後續沒有什麼特別的,一直單步,最後有個 join('') 操作,就生成了假 Cookie:

43

接下來是生成 Cookie 的名字 FSSBBIl1UgzbN7N80T,然後將 Cookie 賦值給 document.cookie,然後又向 localStorage 裡面的 $_ck 賦了個值,localStorage 的內容可以直接複製下來,沒有太大影響。

44

真 Cookie 生成邏輯

單步跟真 Cookie,在本文中也就是 _$ZN(768, 1);,可以看到開始進入了無窮無盡的 if-else 控制流:

45

這裡本地應該怎樣處理呢?我的做法是以 _$Hn 和其值命名函式,function _$Hn768(){} 就表示所有走 768 號控制流的方法,繼續跟,生成真 Cookie 的方法基本上在 747 號控制流,後續我們主要以 747 號控制流的各個步驟來看,747 號控制流扣出來的程式碼大致如下:

46

取假 Cookie

單步跟 747 號控制流,會有個進入第 709 號控制流的步驟,會取先前生成的假 Cookie,經過一系列操作之後返回一個陣列:

47

48

至此我們在本地同步扣的程式碼,如果正常的話,返回的陣列也應該是一樣的(後續的資料就不一樣了,有一些時間戳之類的引數參與運算):

49

自動化工具檢測

繼續跟 747 號控制流,會進入 268 號控制流,接著進入 154 號控制流,這裡面會針對自動化工具做一些檢測,如下圖所示:

50

51

這裡定義了一個變數 _$iL,檢測不通過就是1,後續又把這個變數賦值給了 _$aW,所以我們本地保持一致,也為 false 即可(其實我們不用自動化工具的話,這一段檢測就不用管直接返回 false 就行):

52

20 位核心陣列

繼續跟 268 號控制流,會進入 668 號控制流,668 號控制流就兩個操作,一是生成一個 16 位陣列,二是取 $_ts 裡面的 4 個變數,加到前面的 16 位後面,組成一個 20 位陣列,這 20 位陣列的最後 4 位是瑞數核心,其中的對映關係搞錯了請求是通不過的,在五代中這部分的處理邏輯會更加複雜。

53

54

這裡不是單純的取 $_ts 裡的鍵值對,你在扣程式碼的時候,你也許會發現怎麼本地到這裡取值的時候,取出來的不是數字,而是字串呢?就像下面這種情況:

55

實際上我們最開始得到的 $_ts 值,是經過了二次處理的,我們以第一個 _$sX._$Xb 為例,直接搜尋 _$sX._$Xb,可以發現這麼一個地方:

56

很明顯這裡給 _$sX._$Xb 重新賦值了一遍,我們可以看到等號右邊,先取了一次 _$sX._$Xb,其值為 _$Rm,這和我們初始 $_ts 裡面對應的值是一樣的,然後我們就得再看看 _$sX["_$Rm"] 又是何方神聖,直接搜尋發現是開頭賦值了一個方法,通過呼叫這個方法來生成新的值:

57

另外其他三個值也是同樣的套路,賦值的程式碼分別為:

_$sX._$Xb = _$sX[_$sX._$Xb](_$BH, _$DP);
_$sX._$oI = _$sX[_$sX._$oI](_$ZJ, _$DS)
_$sX._$EN = _$sX[_$sX._$EN]();
_$sX._$D9 = _$sX[_$sX._$D9](_$iL);

實際上應該是:

_$sX._$Xb = _$sX["_$Rm"](_$BH, _$DP);
_$sX._$oI = _$sX["_$Nw"](_$ZJ, _$DS)
_$sX._$EN = _$sX["_$Uh"]();
_$sX._$D9 = _$sX["_$ci"](_$iL);

進一步來說,實際上是:

_$sX._$Xb = _$1k(_$BH, _$DP);
_$sX._$oI = _$jH(_$ZJ, _$DS)
_$sX._$EN = _$9M();
_$sX._$D9 = _$oL(_$iL);

靜態分析沒問題,我們可以先固定下來,但是實際應用當中這些值都是動態的,那我們應該怎麼處理呢?先來多看幾個對比一下找找規律:

58

59

可以發現每次對應的位次都不一樣,但是實際上相同位置的方法點進去都是一樣的,也就是說,變的只有方法名和變數名,實現的邏輯是不變的,所以我們只要知道了這四個值分別對應的位置,就能夠拿到正確的值,在本地,我們就可以這樣做:

1、先利用正則匹配出這四個值,如:[_$sX._$Xb, _$sX._$oI, _$sX._$EN, _$sX._$D9]

60

2、再匹配出 VM 程式碼開頭的 20 個賦值的語句,如:_$sX._$RH = _$wI; _$sX._$i5 = _$n5; 等;

61

3、然後通過 $_ts 取這四個值對應的值,相當於:_$sX._$Xb = _$ts._$Xb = _$Rm;然後再找這四個值所定義的方法在 20 個賦值語句中的位置,相當於:查詢 _$sX._$Rm = _$1k; 在 20 個賦值語句中的位置為 7(索引從 0 開始)

62

4、我們知道了這四個方法在 20 個賦值語句中的位置,那麼我們直接匹配本地對應位置的名稱,進行動態替換即可,當然前提是我們們本地已經扣了一套程式碼出來了:

63

64

經過這樣處理後,就能夠保證這四個值的準確性了。

其他用到 $_ts 值的地方

除了上面說的 20 位陣列裡用到了 4 個 $_ts 的值以外,還有其他地方有 7 個值也用到了,直接搜尋就能定位,這 7 個值相對較簡單,每次都是固定取 $_ts 裡面的第 2、3、4、15、16、17、19 位的值,同樣的,找到對應位置,進行動態替換即可:

65

注意事項

特別注意 VM 程式碼開頭,會直接呼叫執行一些方法,某些變數的值就是通過這些方法生成的,當你一步一步跟的時候發現某些引數不對,或者沒有,那麼就得注意開頭這些方法了,可能一開始就已經生成了。

66

字尾 MmEwMD 生成邏輯

後續的其他 XHR 請求中,都帶有一個字尾,這個字尾的值同樣是由 JS 生成的,每次都會變化,當然不同網站,字尾名不一定都是一樣的,本例中是 MmEwMD,先下一個 XHR 斷點,當 XHR 請求中包含了 MmEwMD= 時就斷下,然後重新整理網頁:

67

可以看到後傳入 l.open() 的 URL 還是正常的,斷下後到 l.send() 就帶有字尾了,再看 l.open() 其實就是 xhr.open(),明顯和正常的有區別,同樣這個方法也在 VM 程式碼裡,應該是重寫了方法,可以和正常的做對比:

68

跟到 VM 程式碼裡去看看,經過了 _$sd(arguments[1]) 方法就變成了帶有字尾的完整連結了:

69

跟進 _$sd 方法,前面都是對 url 做一些處理,後面有個進入第 779 號控制流的流程,實際上就是原來我們生成 Cookie 的步驟,跟一下就行了。

70

善用 Watch 跟蹤功能

71

開發者工具的 Watch 功能能夠持續跟蹤某個變數的值,對於這種控制流很多的情況,設定相應的變數跟蹤,能夠讓你知道你現在處於哪個控制流中,以及生成的陣列的變化,不至於跟著跟著不知道到哪一步了。

結果驗證

如果整個流程沒問題,程式碼也扣得正確,攜帶正確的 Cookie 和正確的字尾,就能成功訪問:

72

73

相關文章