聲明 本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關! 本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權, ...
聲明
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯繫作者立即刪除!
目標
- 目標:數美全家桶,包括:滑塊、文字點選、圖標點選、語序點選、空間推理、無感驗證
- 地址:
// 官網體驗地址
aHR0cHM6Ly93d3cuaXNodW1laS5jb20vdHJpYWwvY2FwdGNoYS5odG1s
// 官方隱藏地址
aHR0cHM6Ly9jYXN0YXRpYy5mZW5na29uZ2Nsb3VkLmNuL3ByL3YxLjAuNC9kZW1vLmh0bWw=
// 某紅書驗證頁面
aHR0cHM6Ly93d3cueGlhb2hvbmdzaHUuY29tL3dlYi1sb2dpbi9jYXB0Y2hh
數美不同類型驗證碼核心的 JS 都是一樣的,只是個別參數有微小差別,主要以滑塊為例來分析,通過 JS 代碼以及官方文檔可以看出數美是有無感驗證的,但是官網體驗地址里並沒有放出來,官方有一個隱藏地址,裡面的 demo 是最全的,包括無感,可以去上面給出的第二個地址里查看;數美的加密參數包含了 DES 加密演算法,參數名以及 DES Key 不定時會變化,本文也會分析如何利用 AST 來獲取動態的參數。
抓包分析
conf
介面,獲取配置,主要是獲取核心的 captcha-sdk.min.js
的地址,請求參數解釋:
參數 | 含義 |
---|---|
organization |
數美分配的公司標識,一般是每個網站唯一,寫死即可 |
appId |
應用標識,區分不同應用,數美後臺可以管理 |
callback |
回調參數 |
lang |
語言,zh-cn 簡體中文、zh-tw 繁體中文、en 英文 |
model |
模式,slide 滑塊、auto_slide 無感驗證、select 文字點選、icon_select 圖標點選、seq_select 語序點選、spatial_select 空間推理 |
sdkver |
這個 sdk 版本是 captcha-sdk.min.js 內部寫死的 |
channel |
推廣渠道,數美後臺可以管理 |
captchaUuid |
32位隨機字元串,與業務方自身埋點數據配合,便於後續定位問題或進行數據統計 |
rversion |
captcha-sdk.min.js 版本號 |
返回結果重點看 captcha-sdk.min.js
文件地址,如下圖所示有個 v1.0.4-171
,本文中我們稱 v1.0.4
為大版本,171
為小版本,小版本不定時會更新,版本號不斷升高。
然後就是 register
介面,不同類型,返回的數據都大同小異,其中 bg
是背景圖片,fg
是滑塊,文字點選、空間推理中 order
是提示信息,k
、l
、rid
三個參數後續會用到。
最後就是 fverify
驗證介面,有類似下圖紅框中的 12 個參數,都是通過 JS 生成的,其參數名會根據 captcha-sdk.min.js
的變化而變化,其中有個最長的類似於下圖的 ep
值,包含了軌跡加密。返回值里參數解釋:
參數 | 含義 |
---|---|
code |
1100 :成功;1901 :QPS超限;1902 :參數不合法;1903 :服務失敗;9101 :無許可權操作 |
riskLevel |
處置建議,PASS :正常,建議直接放行;REJECT :違規,建議直接攔截 |
逆向分析
跟棧會發現核心邏輯在 captcha-sdk.min.js
里,這個 JS 類似於 OB 混淆(以前的文章介紹過,此處不再細說):
這裡可以自己寫 AST 還原一下,為了方便我們直接使用 v_jstools 解混淆:
然後替換掉原來的 captcha-sdk.min.js
,如果你測試的是官網的體驗頁面,使用 Fiddler 替換時要註意可能有跨域問題,需要利用 Filters 功能,設置響應頭 Access-Control-Allow-Origin
欄位值為當前功能變數名稱:
如果你沒註意到這個跨域問題,可能會替換之後發現沒替換成功,原因是數美的資源有四個功能變數名稱,其中一個宕了便會啟用另一個,你替換其中一個報錯了就會自動跳轉另一個,所以看起來你並沒有替換成功:
PS:若替換的 JS 格式化了,那麼你在網頁上滑動也是校驗失敗的,因為 JS 里檢測了格式化,將 JS 壓縮成一行再替換即可,具體檢測的位置後文會講到。
captchaUuid
直接搜索關鍵詞下斷點,經過多次調試會發現第一個出現 captchaUuid
的地方是在 smcp.min.js
,如下圖所示:
這裡的棧並不多,來回跟棧也沒發現是哪裡生成的,此時可以從初始位置也就是 embed.html
初始化驗證碼的地方開始單步跟:
單步跟進去會發現一個 getCaptchaUuid()
的方法,將此方法扣出來即可。
function generateTimeFormat() {
var e = new Date()
, t = function(n) {
return +n < 10 ? "0" + n : n.toString();
};
return ((e.getFullYear().toString() + t(e.getMonth() + 1)) + t(e.getDate()) + t(e.getHours()) + t(e.getMinutes())) + t(e.getSeconds());
}
function getCaptchaUuid() {
var c = "";
var o = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
var s = o.length;
for (var a = 0; a < 18; a++) {
c += o.charAt(Math.floor(Math.random() * s));
}
return generateTimeFormat() + c;
}
12 個加密參數
直接跟棧就很容易找到,如下圖所示的位置,D 就是生成的所有參數,此外,也可以通過搜索關鍵字 getEncryptContent
或者直接搜索參數名稱來定位。
可以發現上圖裡就有四個加密參數,都用到了 getEncryptContent
這個加密方法,加密方法傳入兩個參數,一個是待加密參數,一個是 DES Key,這四個待加密參數分別為 appId
值、channel
值、lang
值和一個 getSafeParams
方法。
重點跟進 getEncryptContent
方法看看,一個控制流,挑幾個重點的講一下,第一步是獲取一個 key
,這個 key
是在前面設置的,後續會講到,實際上這個 key
沒啥用。
然後會有一個 isJsFormat
的格式化檢測函數,正常應該是 false 的,如果你格式化了就為 true,也就會導致 f 的值為時間戳加數美的功能變數名稱,這個 f 值後續是 DES 的 Key,不對的話自然怎麼滑都不會通過。
然後就是 DES 加密了,這個 DES 是標準的加密演算法,下圖中傳入的 1 和 0 表示的是加密,0 和 0 則表示解密,解密的情況也有,後續會遇到,mode
為 ECB
,padding
為 ZeroPadding
,不需要 iv
,可以直接扣代碼,或者直接引庫即可。
var CryptoJS = require("crypto-js")
function DESEncrypt(key, word) {
var key_ = CryptoJS.enc.Utf8.parse(key);
var srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.DES.encrypt(srcs, key_, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.ZeroPadding
});
return encrypted.toString();
}
function DESDecrypt(key, word) {
var key_ = CryptoJS.enc.Utf8.parse(key);
var decrypt = CryptoJS.DES.decrypt(word, key_, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.ZeroPadding
});
return decrypt.toString(CryptoJS.enc.Utf8);
}
這裡的四個值就分析完了,還有八個值是在前面生成的,如下圖所示 x 的值即為其他八個值,往前看是一個函數生成的,往裡面跟即可。
跟進來是一個 getMouseAction
方法,裡面先是挨個取值,後續會對這些值進行 DES 加密,下圖中的 a、c 參數就是 register
介面返回的 k、l 值,s 參數是對 register
介面返回的 k 值進行解密操作:
上圖中 u = this._data
裡面的值,根據滑塊、點選、無感模式的不同,也有所差異,以下代碼中,以 baseData
來表示 this._data
的值,根據模式的不同,可分為三類,大致構成如下:
滑塊(slide
):
/*
track:滑動軌跡(x, y, t),distance:滑動距離,randomNum:生成兩數之間的隨機值,示例:
var track = [[0, -2, 0], [62, 1, 98], [73, 4, 205], [91, 3, 303], [123, -3, 397], [136, 8, 502], [160, 0, 599], [184, 0, 697], [169, 0, 797]]
var distance = 169
*/
var baseData = {}
baseData.mouseData = track
baseData.startTime = 0
baseData.endTime = track[track.length - 1][2] + randomNum(100, 500)
baseData.mouseEndX = distance
baseData.trueWidth = 300
baseData.trueHeight = 150
baseData.selectData = []
baseData.blockWidth = 40
滑塊軌跡生成代碼:
def get_sm_track(distance):
track_length = random.randint(4, 10)
track = [[0, -2, 0]]
m = distance % track_length
e = int(distance / track_length)
for i in range(track_length):
x = (i + 1) * e + m + random.randint(20, 40)
y = -2 + (random.randint(-1, 10))
t = (i + 1) * 100 + random.randint(-3, 5)
if i == track_length - 1:
x = distance
track.append([x, y, t])
else:
track.append([x, y, t])
logger.info("track: %s" % track)
return track
點選類(文字點選 select
、圖標點選 icon_select
、語序點選 seq_select
、空間推理 spatial_select
):
/*
coordinate:點選坐標(x, y),randomNum:生成兩數之間的隨機值,示例:
var coordinate = [[171, 101], [88, 102], [138, 109], [225, 100]]
*/
var baseData = {}
var time_ = new Date().getTime()
coordinate.forEach(function(co) {
co[0] = co[0] / 300
co[1] = co[1] / 150
co[2] = time_
time_ += randomNum(100, 500)
})
baseData.mouseData = coordinate
baseData.startTime = time_ - randomNum(800, 20000)
baseData.endTime = coordinate[coordinate.length - 1][2]
baseData.mouseEndX = 0
baseData.trueWidth = 300
baseData.trueHeight = 150
baseData.selectData = coordinate
baseData.blockWidth = undefined
無感(auto_slide
):
/*
randomNum:生成兩數之間的隨機值
*/
var baseData = {}
baseData.mouseData = [[0, 0, 0]]
baseData.startTime = 0
baseData.endTime = randomNum(100, 500)
baseData.mouseEndX = 260
baseData.trueWidth = 300
baseData.trueHeight = 150
baseData.selectData = []
baseData.blockWidth = 40
這些值生成完了之後,就是挨個通過 getEncryptContent
進行加密,前面已經分析過,實際上就是 DES 加密,可以看到分為點選、滑塊和無感三類,其中 DES Key 也是會每隔一段時間變化的:
再往下走還有三個加密參數,待加密值是定值,然後將 s 的值(也就是前面 register
介面返回的 k 經過 DES 解密後的值賦值給了 this._data.__key
)。
至此所有加密參數就搞完了。
結果驗證
AST 獲取動態參數
前面說了,/v1.0.4-171/captcha-sdk.min.js
文件地址,我們稱 v1.0.4
為大版本,171
為小版本,小版本每隔一段時間會更新,版本號會不斷升高,具體更新周期是多少?這裡推薦一個方法 document.lastModified
,該方法記錄的是物理網頁的最後修改時間,我們直接訪問 JS 地址,就可以直接查看不同版本的 JS 是啥時候更新的了,多對比幾個版本,發現更新間隔時間並沒有太明顯的規律,如下圖所示:
不同版本裡面的 12 個加密參數的名稱和 DES 加密的 Key 都不一樣,我們可以利用 AST 來動態獲取這 12 個參數,經過測試,以下版本均可正常提取:
v1.0.4-148
~v1.0.4-171
v1.0.3-147
~v1.0.3-171
v1.0.1-147
~v1.0.1-171
截止本文發佈,小版本 171
為最新,v1.0.4
小版本從 148
開始,v1.0.3
、v1.0.1
在 147
以前沒有混淆,可自行正則匹配,暫未發現其他大版本,如有遇到不能適配的,可聯繫我瞅瞅,完整的代碼在公眾號 k哥爬蟲
中,有需要的可以點擊下方鏈接。
【驗證碼逆向專欄】數美驗證碼全家桶逆向分析以及 AST 獲取動態參數
PS:此 AST 代碼僅實現對動態參數的提取,並非還原所有的混淆,提取出來的結果是有序、未去重的,後續按索引取就行。