宣告
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整程式碼,抓包內容、敏感網址、資料介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯絡作者立即刪除!
前言
瑞數動態安全 Botgate(機器人防火牆)以“動態安全”技術為核心,通過動態封裝、動態驗證、動態混淆、動態令牌等技術對伺服器網頁底層程式碼持續動態變換,增加伺服器行為的“不可預測性”,實現了從使用者端到伺服器端的全方位“主動防護”,為各類 Web、HTML5 提供強大的安全保護。
瑞數 Botgate 多用於政企、金融、運營商行業,曾一度被視為反爬天花板,隨著近年來逆向大佬越來越多,相關的逆向文章也層出不窮,真正到了人均瑞數的時代了,這裡也感謝諸如 Nanda、懶神等逆向大佬,揭開了瑞數神祕的面紗,總結的經驗讓後來人少走了不少彎路。
過瑞數的方法基本上有以下幾種:自動化工具(要隱藏特徵值)、RPC 遠端呼叫、JS 逆向(硬扣程式碼和補環境),本文介紹的是 JS 逆向硬扣程式碼,儘可能多的介紹各種細節。
瑞數特徵以及不同版本的區別
對於絕大多數使用了瑞數的網站來說,有以下幾點特徵(可能有特殊版本不一樣,先僅看主流的):
1、開啟開發者工具(F12)會依次出現兩個典型的無限 debugger:
2、瑞數的 JS 混淆程式碼中,變數、方法名大多類似於 _$xx
,有眾多的 if-else
控制流,新版瑞數還可能會有 jsvmp 以及眾多三目表示式的情況:
3、看請求,會有典型的三次請求,首次請求響應碼是 202(瑞數3、4代)或者 412(瑞數5代),接著單獨請求一個 JS 檔案,然後再重新請求頁面,後續的其他 XHR 請求中,都帶有一個字尾,這個字尾的值是由 JS 生成的,每次都會變化,字尾的值第一個數字為瑞數的版本,比如 MmEwMD=4xxxxx
就是4代瑞數,bX3Xf9nD=5xxxxx
就是5代瑞數:
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 代瑞數。
瑞數 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 代瑞數。
5、看入口,瑞數有個流程是在虛擬機器 VM 中載入 1w+ 行的程式碼,載入此程式碼的入口,不同版本也不一樣(這個入口具體在哪裡?怎麼定位?在後續逆向分析中再詳細介紹),示例如下:
- 3 代:
_$aW = _$c6[_$l6()](_$wc, _$mo);
,_$c6
實際上是eval
,_$l6()
實際上是call
;
- 4 代:
ret = _$DG.call(_$6a, _$YK);
,_$DG
實際上是eval
,有關鍵字ret
,call
是明文;
- 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 代是在一個陣列裡取值得到的;
當然要想精準區分不同版本,得各個條件結合起來看,最主要的還是得看看內部的實現邏輯,以及頁面的程式碼結構,比如 4 代有一個生成假 Cookie 的步驟,而 5 代沒有,有的特殊版本雖然看起來是 5 代,但是加了 jsvmp 和三目表示式,和傳統的 5 代又有區別,偶爾愚人節啥的突然來個新版本,也會不一樣,各版本在分析一遍之後,就很容易區分了。
Cookie 入口定位
本文案例中瑞數 4 代網站為:aHR0cDovL3d3dy5mYW5nZGkuY29tLmNuL25ld19ob3VzZS9uZXdfaG91c2VfZGV0YWlsLmh0bWw=
首先過掉無限 debugger(過不過其實無所謂,後面的分析其實這個基本上沒影響),直接右鍵 Never pause here
永不在此處斷下即可:
定位 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 的程式碼,類似如下結構:
仔細觀察這兩次 Cookie 生成的地方,分別往上跟棧,你就會發現兩個 Cookie 分別是經過了兩個不同方法得到的,如下圖所示:
這裡的程式碼存在於 VM 虛擬機器中,且是 IIFE 自執行程式碼,我們還得往前跟棧看看這些 VM 程式碼是從哪裡載入出來的,跟棧來到首頁(202頁面)帶有 call 的位置:
我們在文章開頭介紹的這個位置就是這麼分析得來的,這個位置通常在分析瑞數的時候作為入口,圖中 _$te
實際上是 eval 方法,傳入的第一個引數 _$fY
是 Window 物件,第二個物件 _$F8
是我們前面看到的 VM 虛擬機器中的 IIFE 自執行程式碼。
在知道了瑞數大致的入口之後,我們也可以使用事件監聽中的 Script 斷點,一直下一個斷點(F8)就可以走到 202 頁面,然後搜尋 call 關鍵字就能快速定位到入口,Script 斷點中的兩個選項,第一個表示執行 JS 指令碼的第一條語句時斷下,第二個表示 JS 因為內容安全政策而被遮蔽時斷下,一般選擇第一個就可以了,如下圖所示:
檔案結構與邏輯
想要後續分析 Cookie 的生成,我們不得不要觀察一下 202 頁面的程式碼,meta 標籤有個 content 內容,引用了一個類似於 c.FxJzG50F.dfe1675.js
的 JS 檔案,接著跟一個自執行的 JS,如下圖所示:
第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)進行替換。
VM 裡面的程式碼是生成 Cookie 的主要程式碼,包含眾多的 if-else
控制流,無疑增加了我們分析程式碼的成本,這裡就可以使用 AST 技術做一下反混淆,比如 Nanda 就將 if-else
控制流轉換成了 switch-case
的,同一個控制流下的程式碼放在了同一個 case
下,然後在 call
入口那個地方,將 VM 程式碼做一下本地替換,具體可以參考 Nanda 的文章:《某數4代邏輯分析》,感興趣的可以試試,不瞭解 AST 的可以看看以前的文章《逆向進階,利用 AST 技術還原 JavaScript 混淆程式碼》,後續有時間 K 哥再寫寫 AST 還原瑞數程式碼的實戰,本文我們們選擇硬剛!
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"]
}
觀察 $_ts
的 key 和 value,和瀏覽器中得到的是一樣的:
注意事項:c.FxJzG50F.dfe1675.js
外鏈 JS 如果你直接下載下來用編輯器開啟可能會被自動編碼,和原始資料有出入,導致執行報錯,這裡建議直接在瀏覽器線上訪問這個檔案,手動複製過來,或者在抓包軟體裡將響應內容複製過來,觀察以下兩種情況,第一種情況就可能會導致執行出錯,第二種是正常的:
扣程式碼
前面說了這麼多,現在終於可以進入主題了,那就是扣程式碼,找個好椅子,準備把屁股坐穿,此時你的鍵盤只有 F11 有用,不斷單步除錯,只需要億點點細節,就完事兒了!
扣程式碼步驟太多,不可能每一步都截圖寫出來,只寫一下比較重要的,如有遺漏的地方,那也沒辦法,首先先在我們替換的 202 頁面裡,自執行程式碼開始的地方手動加個 debugger,一進入頁面就斷下,方便後續的分析:
通過前面我們的分析,已經知道了入口在 call 的地方,快速搜尋並下斷點:
通過前面我們的分析,我們也知道了有兩次生成 Cookie 的地方,快速搜尋 (5)
,搜尋結果第二個即為入口:
假 Cookie 生成邏輯
首先單步跟假 Cookie,雖然是假的,但是後續生成真 Cookie 中會用到,在跟的時候你會走到這個邏輯裡面:
有一步會呼叫 _$8e()
方法,而 _$8e = _$Q9
,_$Q9
又巢狀在 _$d0
裡的,搜尋一下哪裡呼叫了 _$d0
,發現是程式碼開頭:
那麼傳入的引數 _$Wn
是啥呢?單步跟入,是一個方法,作用就是取 202 頁面的 content 內容,那麼我們在本地就直接刪掉這個 _$Wn
方法,直接傳入 content 的值即可,如下圖所示:
另外,我們發現,程式碼有非常多的在陣列裡面按索引取值的情況,比如上圖中的 _$PV[68]
的值,實際上就是字串 content,很顯然我們要把這個陣列的來源找到,直接搜尋 _$PV =
,可以找到疑似定義和賦值的地方:
所以我們得看看這個 _$iL
方法,傳入了一個非常長的字串,打斷點進去看看,果然生成了 _$PV
,是一個 725 位的陣列:
接下來在扣程式碼的過程中,你會經常遇到一個變數,在本文中是 _$sX
:
有沒有很熟悉?這個值就是我們前面拿到的 $_ts
變數,在開頭就可以看到是將 window.$_ts
賦值給了 _$sX
:
繼續走,會走到以下邏輯中:
這裡會遇到六個陣列,他們都已經有值了,所以我們得找到他們是咋來的,任意搜尋其中一個陣列名稱,會找到定義和賦值的地方:
賦值明顯是呼叫了 _$rv
方法,再搜 _$rv
方法,發現是開頭就呼叫了:
後續沒有什麼特別的,一直單步,最後有個 join('')
操作,就生成了假 Cookie:
接下來是生成 Cookie 的名字 FSSBBIl1UgzbN7N80T
,然後將 Cookie 賦值給 document.cookie
,然後又向 localStorage
裡面的 $_ck
賦了個值,localStorage
的內容可以直接複製下來,沒有太大影響。
真 Cookie 生成邏輯
單步跟真 Cookie,在本文中也就是 _$ZN(768, 1);
,可以看到開始進入了無窮無盡的 if-else
控制流:
這裡本地應該怎樣處理呢?我的做法是以 _$Hn
和其值命名函式,function _$Hn768(){}
就表示所有走 768 號控制流的方法,繼續跟,生成真 Cookie 的方法基本上在 747 號控制流,後續我們主要以 747 號控制流的各個步驟來看,747 號控制流扣出來的程式碼大致如下:
取假 Cookie
單步跟 747 號控制流,會有個進入第 709 號控制流的步驟,會取先前生成的假 Cookie,經過一系列操作之後返回一個陣列:
至此我們在本地同步扣的程式碼,如果正常的話,返回的陣列也應該是一樣的(後續的資料就不一樣了,有一些時間戳之類的引數參與運算):
自動化工具檢測
繼續跟 747 號控制流,會進入 268 號控制流,接著進入 154 號控制流,這裡面會針對自動化工具做一些檢測,如下圖所示:
這裡定義了一個變數 _$iL
,檢測不通過就是1,後續又把這個變數賦值給了 _$aW
,所以我們本地保持一致,也為 false 即可(其實我們不用自動化工具的話,這一段檢測就不用管直接返回 false 就行):
20 位核心陣列
繼續跟 268 號控制流,會進入 668 號控制流,668 號控制流就兩個操作,一是生成一個 16 位陣列,二是取 $_ts
裡面的 4 個變數,加到前面的 16 位後面,組成一個 20 位陣列,這 20 位陣列的最後 4 位是瑞數核心,其中的對映關係搞錯了請求是通不過的,在五代中這部分的處理邏輯會更加複雜。
這裡不是單純的取 $_ts
裡的鍵值對,你在扣程式碼的時候,你也許會發現怎麼本地到這裡取值的時候,取出來的不是數字,而是字串呢?就像下面這種情況:
實際上我們最開始得到的 $_ts
值,是經過了二次處理的,我們以第一個 _$sX._$Xb
為例,直接搜尋 _$sX._$Xb
,可以發現這麼一個地方:
很明顯這裡給 _$sX._$Xb
重新賦值了一遍,我們可以看到等號右邊,先取了一次 _$sX._$Xb
,其值為 _$Rm
,這和我們初始 $_ts
裡面對應的值是一樣的,然後我們就得再看看 _$sX["_$Rm"]
又是何方神聖,直接搜尋發現是開頭賦值了一個方法,通過呼叫這個方法來生成新的值:
另外其他三個值也是同樣的套路,賦值的程式碼分別為:
_$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);
靜態分析沒問題,我們可以先固定下來,但是實際應用當中這些值都是動態的,那我們應該怎麼處理呢?先來多看幾個對比一下找找規律:
可以發現每次對應的位次都不一樣,但是實際上相同位置的方法點進去都是一樣的,也就是說,變的只有方法名和變數名,實現的邏輯是不變的,所以我們只要知道了這四個值分別對應的位置,就能夠拿到正確的值,在本地,我們就可以這樣做:
1、先利用正則匹配出這四個值,如:[_$sX._$Xb, _$sX._$oI, _$sX._$EN, _$sX._$D9]
;
2、再匹配出 VM 程式碼開頭的 20 個賦值的語句,如:_$sX._$RH = _$wI; _$sX._$i5 = _$n5;
等;
3、然後通過 $_ts
取這四個值對應的值,相當於:_$sX._$Xb = _$ts._$Xb = _$Rm
;然後再找這四個值所定義的方法在 20 個賦值語句中的位置,相當於:查詢 _$sX._$Rm = _$1k;
在 20 個賦值語句中的位置為 7(索引從 0 開始)
4、我們知道了這四個方法在 20 個賦值語句中的位置,那麼我們直接匹配本地對應位置的名稱,進行動態替換即可,當然前提是我們們本地已經扣了一套程式碼出來了:
經過這樣處理後,就能夠保證這四個值的準確性了。
其他用到 $_ts 值的地方
除了上面說的 20 位陣列裡用到了 4 個 $_ts
的值以外,還有其他地方有 7 個值也用到了,直接搜尋就能定位,這 7 個值相對較簡單,每次都是固定取 $_ts
裡面的第 2、3、4、15、16、17、19 位的值,同樣的,找到對應位置,進行動態替換即可:
注意事項
特別注意 VM 程式碼開頭,會直接呼叫執行一些方法,某些變數的值就是通過這些方法生成的,當你一步一步跟的時候發現某些引數不對,或者沒有,那麼就得注意開頭這些方法了,可能一開始就已經生成了。
字尾 MmEwMD 生成邏輯
後續的其他 XHR 請求中,都帶有一個字尾,這個字尾的值同樣是由 JS 生成的,每次都會變化,當然不同網站,字尾名不一定都是一樣的,本例中是 MmEwMD
,先下一個 XHR 斷點,當 XHR 請求中包含了 MmEwMD=
時就斷下,然後重新整理網頁:
可以看到後傳入 l.open()
的 URL 還是正常的,斷下後到 l.send()
就帶有字尾了,再看 l.open()
其實就是 xhr.open()
,明顯和正常的有區別,同樣這個方法也在 VM 程式碼裡,應該是重寫了方法,可以和正常的做對比:
跟到 VM 程式碼裡去看看,經過了 _$sd(arguments[1])
方法就變成了帶有字尾的完整連結了:
跟進 _$sd
方法,前面都是對 url 做一些處理,後面有個進入第 779 號控制流的流程,實際上就是原來我們生成 Cookie 的步驟,跟一下就行了。
善用 Watch 跟蹤功能
開發者工具的 Watch 功能能夠持續跟蹤某個變數的值,對於這種控制流很多的情況,設定相應的變數跟蹤,能夠讓你知道你現在處於哪個控制流中,以及生成的陣列的變化,不至於跟著跟著不知道到哪一步了。
結果驗證
如果整個流程沒問題,程式碼也扣得正確,攜帶正確的 Cookie 和正確的字尾,就能成功訪問: