宣告
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整程式碼,抓包內容、敏感網址、資料介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯絡作者立即刪除!
前言
最近有粉絲反饋,在處理業務網站的時候,碰到了某裡的驗證碼,但是又和大夥所熟知的 226、227 不一樣:
其實這是某裡 v2 驗證碼,相較於 228、231 這種旗艦產品,之前使用 v2 的網站並不是很多,不過最近有多起來的趨勢。
部分小夥伴可能看到了某裡系的驗證碼,就認為解決不了,直接放棄,其實都沒有那麼複雜,不說全純算還原,至少補環境是可以嘗試一下的。v2 驗證碼有很多型別,滑動的版本變動最頻繁,1.1.0 一直到現在的 1.1.10,再過兩天可能又是新的,解決流程其實都大差不差,本文就逆向分析一下某裡 v2 滑動驗證碼,僅供參考:
逆向目標
- 目標:某裡 v2 滑動驗證碼
- 網址:想要研究的小夥伴,私聊
抓包分析
本案例的網站,不停地點下一頁就會觸發驗證碼,也可以直接訪問我前文給到的網址(如果響應 HTTP Status 405 – Method Not Allowed
就是指紋黑了)。queryPage 介面正常會返回公告相關資料,觸發了驗證碼,響應內容就會提示需要驗證:
主要需要關注四個介面,前後請求了兩次 4e778xxx.captcha-pro-open.aliyuncs.com
(提交的引數不同)介面,第一次獲取 DeviceConfig、RequestId 引數,用於後續驗證,StaticPath 即版本號。請求引數中有個 SignatureMethod,值為 HMAC-SHA1
,這著就像是有啥引數是經過這個演算法加密的:
- AccessKeyId:固定值,不同介面不一樣;
- UserUserId、UserId、UserCertifyId:queryPage 介面觸發驗證碼時,響應返回;
- DeviceData:裝置資訊;
- SignatureNonce:類 UUID,後文分析;
- Signature:對相關請求引數進行加密、編碼,後文分析。
第二次請求,即最終的驗證碼校驗,獲取校驗結果及相關引數:
- CertifyId:queryPage 介面觸發驗證碼時,響應返回,即 traceid;
- CaptchaVerifyParam:一些校驗引數,包括各種環境、指紋、軌跡等等,後文分析。
驗證失敗,VerifyCode 為 F001、F002:
驗證成功,VerifyCode 為 T001:
中間請求了兩次 device.captcha-open.aliyuncs.com
(Log2、Log3)介面,進行裝置、軌跡驗證(該站校驗了 Log2),Data 引數經過加密處理,後文分析:
v2 接入文件:https://help.aliyun.com/zh/captcha/captcha2-0/user-guide/serv...
逆向分析
SignatureNonce
觸發驗證碼,抓包後會看到,第一個介面 4e778fxxx.aliyuncs.com
是 xhr 型別的,直接去原始碼(Sources)中下個 xhr 斷點,重新整理網頁重新觸發驗證碼即會斷住。此時請求引數都已經生成了:
接著向上跟棧,跟到 AliyunCaptcha.js
檔案中(有幾套,可以固定一下),下圖即 SignatureNonce 的加密位置,跟進到 t[M(i)]
中,將演算法扣下來即可:
Signature
加密位置就在 SignatureNonce 下面,就是 A:
A = $[M(f)]($r, a, c[M(v) + "ET"])
- c[M(v) + "ET"]:加密的金鑰,固定值;
- a:相關請求引數;
- $r:加密函式。
直接跟到加密函式 $r
中去看看,經典的 switch 語句,按照下圖順序,跳來跳去,饒了一大圈,實際上最後一個,才是關鍵的演算法:
控制檯列印一下,關鍵的加密函式為 rn
,傳了兩個引數生成了 Signature 的值;請求引數拼接後,再經過 url 編碼得到的 N;最終的 key 就是 YSKfst7GaVkXwZYvVihJsKF9r89koz&
:
跟進去,程式碼如下:
function rn(t, r) {
var n = 477
, e = 511
, i = Lr
, o = Er()[i(n)](r, t);
return Ir[i(e) + "y"](o)
}
Ir[i(e) + "y"](o)
即 Signature 的值,先跟到 Er()[i(n)]
中去,程式碼如下,HMAC 初始化了一個帶有指定雜湊演算法和秘鑰的物件,n 即金鑰 YSKfst7GaVkXwZYvVihJsKF9r89koz&
,r 為訊息資料,也就是經過編碼後的相關請求引數:
new l.HMAC.init(t, n).finalize(r)
從下圖中可以看到,HMAC 摘要的位元組長度 sigBytes 為 20,印證了前文的猜想,這裡採用的就是 HMAC-SHA1 摘要演算法(HMAC-SHA-256 為 32 位元組,HMAC-SHA-512 為 64 位元組),用 toString() 方法能將其轉成十六進位制的字串,也就是最終的值:
還可以用下面這種方式轉,方法很多:
const hmacResult = {
"words": [-420820728, -1037548830, 1961777390, -1789761326, 721048761],
"sigBytes": 20
};
const hex = hmacResult.words.map(word =>
(word >>> 0).toString(16).padStart(8, '0')
).join('');
console.log(hex); // 輸出對應的十六進位制字串
再去K哥工具庫對比一下,確認是 HMAC-SHA1 了:
線上 HMAC 加密工具:https://www.kgtools.cn/secret/hmac
但是很明顯,這並不是 Signature 的值,也就是還經過了別的處理。最後再跟到 Ir[i(e) + "y"]
中去看看,發現還經過了 base64 編碼,當然,不是直接編碼的 HMAC-SHA1 後的結果,這裡是對 words 陣列(包含了 HMAC 運算的結果)編碼後得到的:
那這該怎麼處理呢?逆向思維一下,先透過金鑰,將字串加密,將加密後的十六進位制字串轉換為 Buffer,寫進 words,再進行上面的計算即可。
引數都處理完,請求第一個介面,若響應如下,需要注意些細節,比如請求引數是否正確,以及編碼問題:
"Message":"Specified signature is not matched with our calculation!"
Timestamp 引數也不能隨便亂寫,不然同樣拿不到正確的結果:
"Message":"Timestamp is illegal!"
扣下來即可:
device.captcha-open.aliyuncs.com
介面也會需要 SignatureNonce 和 Signature,大差不差,只不過請求引數和 key 不一樣而已,流程類似,跟棧就能找到,不贅述了。
Data
裝置認證引數,環境風控點。同樣跟棧下斷點分析,跟到此處時,發現 Data 的值已經生成了:
這裡的 o 物件中明視訊記憶體儲的是相關的請求引數,往上跟 o 就會發現,Data 定義在下圖所示位置:
pe([a, Ps["WEB"], s, bt["APP_VERSION"], "CLOUD", o["GatherCost"], u["Type"], u["Data"]])
部分是定值,WEB_AES_SECRET_KEY
,可以跟一下看看,比如 s,走到演算法裡,插樁列印一下。可以看到,前面兩個引數加密生成的 s,第一個值為 key,AES 解密後得到的,非定值:
其解密的 key 又和 secretKey 有關,插樁觀察,再打條件斷點,發現是第一個介面返回的 DeviceConfig 解密後得到的(還傳了 ip 校驗),套娃,需要注意的是,幾個加解密的 key 並非都是一樣的:
前文截圖部分的日誌,最上面還列印出了一些環境,包括 User-Agent、時區、ip 等等,後面會用到。s 本身就是 AES 加密,iv 之類也好找:
這個 u["Data"] 也並非固定值,需要跟一下:
往上跟棧後會發現,其是由一些環境引數拼接後加密生成:
無非也是 AES 的加解密,key 套 key,都較容易找到。
第二次裝置驗證,Data 值的位置就在第一次的附近,整體和第一次的大差不差:
第一次的 Type 為 501,第二次為 combat,u["Data"] 演算法走的分支不一樣:
往上跟棧就能找到加密位置,第一次校驗了一些環境,第二次校驗的軌跡:
驗證介面的相關加密引數都是在 sg.cdec2e19d71dad5d9c4c.js
檔案中生成。
deviceToken、data
最後的驗證介面,別的都與之前的類似,主要有兩個引數 deviceToken 和 data。同樣,跟棧找一下引數值生成的位置:
deviceToken 就是 T 引數的值,T 定義在 case 0 中,T = Z[o(w)](ie)
,return 處下斷點,斷住後跟到 ie 中去,a 就是 deviceToken 引數的值,繼續往 window.um[o(t)]
中跟,分支別走錯了:
再跟到 YS 函式中去,看看是如何加密的。進去後會發現,同樣的 switch 語句,按 0|1|7|5|4|3|6|2
的順序執行,也是在加密環境相關的引數:
如下圖所示,case 6 時,deviceToken 開始被賦值,也就是 N,N 生成在 case 3 處,最後再跟到 ce 函式中去:
熟悉的配方,熟悉的味道,還是 switch,這個自己跟一下就行了,大差不差,網上的經典圖之一,就出自這裡:
data 就是對軌跡進行加密處理,其生成位置就在 deviceToken 的下面,也就是 B,一樣是 AES 加密。這裡的 key 是經過 AES 解密後得到的,為定值,感興趣的可以去跟一下:
B = Z[o(y)](Et, {
type: Z[o(b)],
key: Be,
data: Z[o(x)](lr, Z[o(_)](Cn(), r))
});
主要來看 B 中的 data,也是經過加密的引數,一層套一層,r 就是經過處理後的軌跡:
TrackList、TrackStartTime、VerifyTime 在下圖處生成,xi 就是原始的軌跡,轉換方式並不複雜,可以考慮自寫:
data: Z[o(x)](lr, Z[o(_)](Cn(), r))
,後面的部分就是將軌跡物件轉換成字串的形式,主要跟進到 lr 函式中去看看。首先,透過 fr 函式中的 TextEncoder 方法,將軌跡字串轉換成了 UTF-8 編碼的二進位制資料:
再往下看,return 處是個自執行函式,將二進位制資料轉換為經過 Base64 編碼的字串。通常都會將二進位制資料壓縮之後再轉換,(0, Un.deflate)(n)
乾的就是這活,Deflate 是一種無損資料壓縮演算法,zlib 格式的資料,用 zlib 模組(C 語言編寫)或者 pako 庫(JavaScript 編寫)實現都可以。至此,演算法部分就分析完畢了。
sg 動態研究
經過持續一段時間觀察,我們發現 sg 版本也是動態變化的,反覆橫跳。從最開始的 1.1.0 陸續動態變化到 1.1.10 了,根據註冊介面返回的 sg 版本號對應驗證介面也需要做動態 Key 的處理,分析方法同上文,可以利用 AST 進行動態 key 的匹配,或者可以去整體分析 sg 程式碼結構,將他加密函式匯出,進行呼叫直接一步到位生成對應的加密引數。
雖說 sg 是變化的,但是在初始化的時候,透過 AliyunCaptcha.js 註冊了很多方法, 配置和動態載入 JavaScript 資源,如下圖:
我們同樣將 js 拉下來一份,透過補環境的方法在本地將這個模組完全跑通。
按上文思路,我們來找到驗證滑塊資料包加密引數的位置,如下:
透過 em 函式傳入軌跡生成最終的加密字典,該函式傳入了 t 函式(ia 函式) 與 軌跡明文,分析可得:
ia 函式最終賦值給了 window.AliyunCaptcha,所以只需將 ia 以及其原型鏈補齊,即可完成 t 的復現:
這些原型鏈裡面包括驗證出現資料包函式的資訊以及滑塊註冊介面解密返回的 key 資訊,userUserId 等等。然後將加密函式 em 匯出給 window,呼叫復現即可:
// AliyunCaptcha.js
// sg.js
window.AliyunCaptcha.prototype = {
"config": config,
"deviceConfig": deviceConfig
}
var t = {
"$element": {},
"onBizResultCallback": undefined,
"verifyFailed": false,
"captcha": {
"verifyFailed": false,
"slideStyle": {
"width": 300,
"height": 40
}
}
}
t.__proto__ = window.AliyunCaptcha.prototype
window.encrypt_(t, track_data)
高版本軌跡區分
對於較高版本,在單獨生成軌跡 data 密文時,傳入的引數發生了變化,在前面會多一段雜湊值,生成位置如下:
經過該函式透過棧操作和遞迴即可修改 A[0] 屬性值,最終拼接完成軌跡新構造:
S.dE(eb, typeof window != g + "d" ? window : window = e.g, 0, [], eg.d, eg.c, void 0, A),
l = A[0] + l
只需將 eb 函式扣下復現即可: