如何使用 JS 破解輕量級(jí)滑塊驗(yàn)證碼

今天在這篇文章里給大家介紹一下怎么使用 JS 破解滑塊驗(yàn)證碼。

文章末尾有源碼鏈接,需要的朋友可以自取,不過(guò)拿走之前記得三連哇!
思路講解
操作瀏覽器打開頁(yè)面
滑塊驗(yàn)證碼是滑動(dòng)驗(yàn)證碼的一種,生成流程至少包含三步:
根據(jù)用戶標(biāo)識(shí),從后臺(tái)獲得驗(yàn)證碼圖片
監(jiān)聽鼠標(biāo)事件并回傳后臺(tái)
后臺(tái)判斷事件的真?zhèn)?,回傳?yàn)證結(jié)果
無(wú)論生成機(jī)制的細(xì)節(jié)如何,滑塊終究是要展示在頁(yè)面上的。直接用 HTTP Request 把頁(yè)面 HTML 請(qǐng)求下來(lái)肯定不行,這里我們需要使用 Puppeteer 打開網(wǎng)頁(yè)進(jìn)行渲染。測(cè)試頁(yè)面就以 react-slider-vertify 的官網(wǎng)為例。
Puppeteer 是一個(gè)通過(guò) DevTools 協(xié)議來(lái)控制瀏覽器行為的庫(kù),只需編寫不多的代碼,就可以操作真實(shí)的瀏覽器搞定諸如爬蟲、自動(dòng)化測(cè)試、網(wǎng)頁(yè)性能分析、瀏覽器擴(kuò)展測(cè)試等功能。使用起來(lái)比較方便,文檔齊全。根據(jù)文檔,打開頁(yè)面前需要啟動(dòng)一個(gè)瀏覽器實(shí)例,然后調(diào)用 newPage 方法創(chuàng)建一個(gè)新頁(yè)面。核心代碼如下。
const?puppeteer?=?require('puppeteer')
puppeteer.launch().then(async?browser?=>?{
??const?page?=?await?browser.newPage()
??await?page.goto('http://h5.dooring.cn/slider-vertify/vertify')
})
正常瀏覽網(wǎng)頁(yè)時(shí),很少會(huì)碰到滑塊驗(yàn)證,因?yàn)樗穆窂椒浅iL(zhǎng),會(huì)需要浪費(fèi)用戶好一會(huì)兒時(shí)間。雖然文案上給你顯示“恭喜 0.9s 打敗了 99% 的用戶”,但從前置腳本請(qǐng)求,到加載圖片,用戶滑動(dòng)滑塊,到回傳驗(yàn)證...前前后后的步驟加起來(lái)可能花了你 9s 不止。
本質(zhì)上它是防御性的手段,一般當(dāng)服務(wù)器限流,或者服務(wù)器已經(jīng)懷疑你是爬蟲的時(shí)候,才會(huì)讓它跳出來(lái)要求做進(jìn)一步驗(yàn)證。所以一般來(lái)說(shuō),爬蟲代碼可以默認(rèn)忽略滑塊驗(yàn)證的,不過(guò)本文代碼我們就假定默認(rèn)驗(yàn)證碼一定存在吧。
接下來(lái),分析一下用戶行為。解決滑塊驗(yàn)證,無(wú)非就是先判斷一下缺口的位置,然后移動(dòng)鼠標(biāo)。這里進(jìn)一步可以細(xì)分為鼠標(biāo)的點(diǎn)擊事件和鼠標(biāo)移動(dòng)事件兩種。代碼邏輯大致如下。
等待驗(yàn)證碼圖片加載完畢
移動(dòng)鼠標(biāo)到滑塊位置
按下鼠標(biāo)
移動(dòng)鼠標(biāo)到缺口位置
松開鼠標(biāo)
等待結(jié)果返回
啊,流程我都知道,可問(wèn)題就在,怎么判斷要把鼠標(biāo)移動(dòng)到哪里?服務(wù)器端返回的是一張帶缺口的圖片,缺口位置指定是不通過(guò)接口傳遞的。
可能還有些同學(xué)會(huì)問(wèn),怎么移動(dòng)鼠標(biāo)呢?如果我一次性就把滑塊給移到指定位置了,那服務(wù)器不是會(huì)立馬把我標(biāo)記為爬蟲嘛... 這兩個(gè)問(wèn)題我逐一解答。
判斷缺口位置
要分析缺口的位置,我們必須先知道這個(gè)缺口是怎么畫上去的。打開控制臺(tái)初步檢查可以發(fā)現(xiàn),頁(yè)面從服務(wù)器先拿到了一張完整的圖片,然后缺口位置是通過(guò) JS 隨機(jī)生成的。啊這...這...這是因?yàn)槲覀冇玫臏y(cè)試頁(yè)面是文檔頁(yè),大家不必在意這些安全方面的小細(xì)節(jié)。

接著剛才的思路繼續(xù),既然缺口是從原圖中挖的一個(gè)洞,那么我們只需要識(shí)別一下圖片中洞的位置就好了。比較簡(jiǎn)單的方案是使用第三方的圖像識(shí)別技術(shù)(或相關(guān)技術(shù)),把圖片上傳至第三方,就直接拿到缺口位置的相對(duì)坐標(biāo)。下圖給大家展示一下阿里云的圖像分割的效果alimagic。

如果想做一個(gè)效果穩(wěn)定一點(diǎn)的驗(yàn)證碼破解工具,我建議大家還是用自己的模型,或者自己寫算法。一來(lái)是第三方要的錢錢不少哇~ 再就是這種圖像識(shí)別并不是專門為驗(yàn)證碼訓(xùn)練的,所以放到爬蟲中還不太成熟,一旦背景圖片復(fù)雜,識(shí)別率下降得老快了。

以下介紹一種新思路。反正我們已經(jīng)有一張完整的圖片了,那就只要不斷地滑動(dòng)滑塊,把結(jié)果和原圖比對(duì)就行。理論上,只要滑倒差不多“那個(gè)點(diǎn)”,肉眼看起來(lái)不會(huì)有很大的違和感時(shí),就搞定了。比如下面這張圖。

截圖嘛可以直接用 Pupeteer 的截圖功能,它提供了對(duì)應(yīng)的 API,可以精準(zhǔn)的截出特定元素。
至于圖片比對(duì),其實(shí)就是給圖片初步處理后,兩張圖一個(gè)像素一個(gè)像素的去比較。兩個(gè)像素如果顏色差異超過(guò)閾值,就認(rèn)為這是兩個(gè)不相同的像素。簡(jiǎn)便起見(jiàn),我們直接用開源庫(kù) rembrandt,它會(huì)給我們返回兩張圖片間的差異。
最后是滑動(dòng)滑塊,既然要模擬人肉操作,那么操作 CSS,用絕對(duì)定位,把滑塊一個(gè)像素一個(gè)像素的向右移;每移動(dòng)一次,把圖像比對(duì)的結(jié)果記錄下來(lái)。
以上三個(gè)流程的核心代碼非常簡(jiǎn)單,只需要以下幾行:
while?(left?<=?maxOffset)?{
??/*?使用?CSS?left?屬性控制懸浮的滑塊的偏移量?*/
??await?page.evaluate(async?($sliderFloat,?left)?=>?{
????$sliderFloat.setAttribute('style',?`left:?${left}px`)
??},?$sliderFloat,?left)
??/*?截圖并和原圖進(jìn)行比對(duì),把結(jié)果存到?results?數(shù)組里?*/
??const?$panel?=?await?page.$('#Vertify-demo-4?.canvasArea')
??const?panelImgBase64?=?await?$panel.screenshot({
????type:?'jpeg'
??})
??const?compareRes?=?await?rembrandt({
????imageA:?panelImgBase64,
????imageB:?rawImage
??})
??results.push({
????left,
????diff:?compareRes.differences
??})
??left?+=?1
}
最后,把 results 扔到里面展示一下(這里給個(gè) ECharts 折線圖示例網(wǎng)址),不出意外能得到這樣一張圖表??吹侥莻€(gè)尖尖的“V”型山谷了嘛,呼哈哈哈,答案很明顯,當(dāng)我們把滑塊從左往右移動(dòng)時(shí),滑塊約接近缺口,那截出來(lái)的圖片就越像原圖,它兩之間像素差異越??;一直往右移動(dòng),滑塊會(huì)逐漸遠(yuǎn)離缺口,截出來(lái)的圖片和原圖相比像素差異又逐漸開始增大。
我們只需要把差異最小的那個(gè)點(diǎn)找到,然后滑動(dòng)滑塊到對(duì)應(yīng)的 left 偏移量就闊以了。

題外話,為什么最大的差異在 3000 左右呢?我們簡(jiǎn)單估算一下。
滑塊的大小為 45*45,再加上外面的圓形,約摸占了 2100 像素;也就是說(shuō)缺口加滑塊,理論上最大會(huì)有 4200 個(gè)像素和原圖不同。不過(guò)滑塊可能和遮住的地方像素有重合,假設(shè)重合了 350 像素,再加上我們的最低點(diǎn)的圖片差異都有 351,減去這些誤差,得 3499。嗚呼,3499 約等于 3000,估算成功(手動(dòng)狗頭)。
速度優(yōu)化技巧
不過(guò)這還沒(méi)完,你要是把代碼跑起來(lái)就會(huì)發(fā)現(xiàn),臥草,太慢了這玩意兒!正常人劃一下驗(yàn)證碼頂多兩秒鐘的事兒,我們一幀一幀截圖得花個(gè) 40s 的時(shí)間才能截完圖算出山谷谷底的值來(lái)。
這里提供幾種思路優(yōu)化效率:
把元素縮小,復(fù)制多份,平鋪開來(lái)展示;這樣只要截一次,然后再裁剪、比對(duì)就好。
放大步長(zhǎng),比方說(shuō)先每次平移 15px,找到局部最優(yōu)解,然后在局部最優(yōu)解附近再回到平移 1px 的方案找最優(yōu)解。
因?yàn)閳D片比對(duì)的結(jié)果類似“V”字,“V”字右半邊其實(shí)是可以不用再計(jì)算的。
使用 1+2+3 我覺(jué)得可以在 3s 內(nèi)搞定最優(yōu)解,不過(guò)代碼復(fù)雜度會(huì)變得很高,文中簡(jiǎn)單起見(jiàn)暫只實(shí)現(xiàn)一下方案 2。
首先是每次移動(dòng) 15px 找局部最優(yōu)解。
//?圖片缺口是不會(huì)給挖在初始附近的,
//?所以?left?從?45?像素開始計(jì)算可以節(jié)約不少計(jì)算量,
let?left?=?45;
const?max15Offset?=?265;
const?res15px?=?[];
while?(left?<=?max15Offset)?{
??await?setLeft(left);
??const?compareRes?=?await?compare();
??res15px.push({
????left,
????diff:?compareRes.differences,
??});
??left?+=?15;
}然后再嘗試每次移動(dòng) 2px 找最優(yōu)解,搜尋的范圍是 15px 步長(zhǎng)最優(yōu)解的 left 偏移量的左右共 20 個(gè)像素。
const?min15pxDiff?=?Math.min(...res15px.map((x)?=>?x.diff));
const?min15pxLeft?=?res15px.find((x)?=>?x.diff?===?min15pxDiff).left;
left?=?min15pxLeft?-?12;
const?max2Offset?=?min15pxLeft?+?8;
const?res2px?=?[];
while?(left?<=?max2Offset)?{
??await?setLeft(left);
??const?compareRes?=?await?compare();
??res2px.push({
????left,
????diff:?compareRes.differences,
??});
??left?+=?2;
}此時(shí)得到的解可以約等于最優(yōu)解了。當(dāng)然,如果你覺(jué)得不穩(wěn)的話,還可以使用 1px 步長(zhǎng)去找。
估算一下,原先需要截 245 次圖片,現(xiàn)在直接降到 1/10,23 次。不過(guò),也別太高興,因?yàn)闇y(cè)試發(fā)現(xiàn)只做優(yōu)化 2,解驗(yàn)證碼的時(shí)候還是要 7s...
操作鼠標(biāo)滑滑塊
缺口位置都搞定了,那移鼠標(biāo)滑滑塊兒還不簡(jiǎn)單嘛~
Puppeteer 已經(jīng)提供了鼠標(biāo)相關(guān)的接口,一共四個(gè):mouse.click、mouse.down、mouse.move、mouse.up,分別是點(diǎn)擊、按下、移動(dòng)和松開。使用 mouse.move 可以直接把鼠標(biāo)位置移動(dòng)到一個(gè)特定的坐標(biāo)上。
假設(shè)我們現(xiàn)在從坐標(biāo)(100,100)花大約 1s 把鼠標(biāo)移動(dòng)到 (200,200),可以使用循環(huán)實(shí)現(xiàn)。
const?now?=?{
??x:?100,
??y:?100
}
const?target?=?{
??time:?1000,
??x:?200,
??y:?200,?
}
const?steps?=?10
const?step?=?{
??x:?Math.floor((target.x?-?now.x)?/?steps),
??y:?Math.floor((target.y?-?now.y)?/?steps),
??time:?target.time?/?steps
}
while?(now.x???await?sleep(step.time)
??now.x?+=?step.x
??now.y?+=?step.y
??await?page.mouse.move(now)
}鼠標(biāo)軌跡優(yōu)化
害,要是打游戲的時(shí)候也像這段代碼一樣,我想我的手點(diǎn)到哪兒它就點(diǎn)到哪兒就好了~
機(jī)器是不會(huì)手抖的,這段代碼和真實(shí)世界的滑動(dòng)效果相差太遠(yuǎn)了!我們看一張我用手滑的效果,尤其是要仔細(xì)觀察滑動(dòng)過(guò)程中鼠標(biāo)的位置。

鼠標(biāo) Y 軸位置總是在變
鼠標(biāo) X 軸位置會(huì)滑過(guò)頭(別笑,你肯定也經(jīng)常劃過(guò)頭)
這里做一波小優(yōu)化,把這兩個(gè)細(xì)節(jié)整合進(jìn)去。
//?獲得一個(gè)隨機(jī)的偏移量
const?getRandOffset?=?(randNegative?=?true,?max?=?3)?=>?{
??const?negative?=?randNeagtive
??????(Math.random()?0.5)???-1?:?1
????:?1
??return?Math.floor(Math.random()?*?max)?*?negative
}
//?先滑過(guò)頭十幾像素,然后再花?100?毫秒的時(shí)間往回滑到正確位置
const?points?=?[
??{
????time:?1000,
????x:?200?+?getRandOffset(false,?15),
????y:?200?+?getRandOffset(false,?15),
????steps:?10
??},
??{
????time:?100,
????x:?200,
????y:?200,
????steps:?3
??}
]
//?注意這里用?for?await?循環(huán)把?points?串起來(lái)執(zhí)行
for?await?(const?target?of?points)?{
??const?step?=?{
????x:?Math.floor((target.x?-?now.x)?/?target.steps),
????y:?Math.floor((target.y?-?now.y)?/?target.steps),
????time:?target.time?/?target.steps,
??}
??let?gap
??while?(gap?=?Math.abs((target.x?-?now.x)),?gap?>?0)?{
????await?sleep(step.time)
????//?最后一步就直接滑動(dòng)到位,不需要隨機(jī)數(shù)了
????const?inOneStep?=?Math.abs(target.x?-?now.x)?<=?Math.abs(step.x);
????if?(inOneStep)?{
??????now.x?=?target.x;
??????now.y?=?target.y;
????}?else?{
??????now.x?+=?step.x?+?getRandOffset();
??????now.y?+=?step.y?+?getRandOffset();
????}
????moveMouseTo(now)
??}
}如何移動(dòng)鼠標(biāo)到這里就解決了,如果要考慮加速度、用戶習(xí)慣等因素,代碼會(huì)復(fù)雜許多,暫時(shí)就不深入討論啦,有興趣的同學(xué)可以自己研究。
最終效果見(jiàn)下圖。

源碼地址在此:CrackTheShield
https://github.com/Lionad-Morotar/crack-the-shield/tree/master/tasks/dooring-slider
作者:仿生獅子
https://juejin.cn/post/7009333291140513799
- EOF -
