聲明
本文章中所有内容僅供學習交流使用,不用于其他任何目的,不提供完整代碼,抓包内容、敏感網址、資料接口等均已做脫敏處理,嚴禁用于商業用途和非法用途,否則由此産生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導緻的任何意外,作者均不負責,若有侵權,請在公衆号【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 的時候可以找到答案),比如:
-
:數字 80 是 http 協定的預設端口号,對應 http 請求,其值第一位為 3,表示 3 代瑞數;FSSBBIl1UgzbN7N80T=37Na97B.nWX3....
-
:數字 443 是 https 協定的預設端口号,對應 https 請求,其值第一位為 4,表示 4 代瑞數。FSSBBIl1UgzbN7N443T=4a.tr1kEXk.....
瑞數 5 代也有以 T 和 S 結尾的兩個 Cookie,但有些特殊的 5 代瑞數也有以 O 和 P 結尾的,同樣的,以 O 開頭的是第一次的 412 那個請求傳回的,以 P 開頭的是由 JS 生成的,Cookie 值第一個數字同樣為瑞數的版本,和 3、4 代不同的是,5 代沒有加端口号了,比如:
-
:以 O 結尾,其值第一位為 5,表示 5 代瑞數;vsKWUwn3HsfIO=57C6DwDUXS.....
-
:以 T 結尾,其值第一位為 5,表示 5 代瑞數。WvY7XhIMu0fGT=53.9fybty......
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,call 是明文,也有沒有 ret 關鍵字的版本,比如ret = _$Yg.call(_$kc, _$mH);
,也有像 3 代那樣全部混淆了的,比如:_$ap = _$j5.call(_$_T, _$gp);
,_$x8 = _$mP[_$nU[15]](_$z3, _$Ec);
實際上是_$mP
,eval
實際上是_$nU[15]
,混淆的call
與 3 代的差別就是 5 代是在一個數組裡取值得到的;call
當然要想精準區分不同版本,得各個條件結合起來看,最主要的還是得看看内部的實作邏輯,以及頁面的代碼結構,比如 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 功能能夠持續跟蹤某個變量的值,對于這種控制流很多的情況,設定相應的變量跟蹤,能夠讓你知道你現在處于哪個控制流中,以及生成的數組的變化,不至于跟着跟着不知道到哪一步了。