聲明
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據接口等均已做脱敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯繫作者立即刪除!
目標
目標:百 X 網數字九宮格驗證碼逆向分析
網址:aHR0cHM6Ly9iZWlqaW5nLmJhaXhpbmcuY29tL296L3M5dmVyaWZ5X2h0bWw=
抓包分析
本例中的驗證碼不是很難,但網站埋了點兒坑,容易出現識別正確、參數也正確,但仍然請求不成功的情況。訪問主頁響應碼為 307,接着請求了一個 bf.js 和兩個 s.webp 的圖片,然後又跳轉到首頁出現驗證碼。如果你沒有以上步驟,請求主頁直接就是 200 出現驗證碼,則需要清除 cookie 後再訪問,因為第一次 307 到請求 bf.js 再到兩次 s.webp 都是在設置 cookie。
第一次請求主頁,response headers 會設置一個名為 _trackId 和 __city 的 cookie,如下圖所示:
然後帶着這兩個 cookie 請求了一個 bf.js,這個 js 用於後續兩個 s.webp 請求參數的加密,這裏注意,第一個坑,雖然可以直接調試 js 扣算法下來後面直接用就行了,但是這個 js 必須得請求一遍,不然後面請求主頁的時候一直是 307。
然後請求第一個 s.webp,get 請求,有三個參數:cf、s 和 f,明顯是加密得來的,同時請求的 cookie 也多了三個值:c0fc276cce08ba22dc、c1fc276cce08ba22dc 和 bxf,如下圖所示:
然後請求第二個 s.webp,和第一個類似,get 請求也有三個參數:cf、s 和 f,cookie 和第一個一樣,但第二次請求返回了一個名為 sbxf 的新 cookie,其值和 bxf、c1fc276cce08ba22dc 其實是一樣的,如下圖所示:
然後帶上 __trackId、__city、c0fc276cce08ba22dc、c1fc276cce08ba22dc、bxf 和 sbxf 這六個 cookie 再次訪問主頁,就是驗證碼頁面了,返回的 html 裏有個新的 js,很長一串,如下圖所示:
然後觀察這個 js,裏面包含了驗證碼圖片的 URL,以及需要點擊的數字,如下圖所示:
點擊驗證後,會給 verify_url 發一個 get 請求,請求參數主要有一個 data,即點擊座標(這個座標也有講究,有可能你的值是對的,但有時候也不成功,這個後文再細説),cookie 和前面的請求一樣,如果驗證成功,會返回 ret 為 0,且有一個 code 供後續請求使用,如下圖所示:
獲取 cookie
這裏再注意一點,所有的請求,header 只需要 Referer 和 User-Agent 就行了,不要亂加,比如多了個 Host 也有可能導致後續請求不成功。
想要拿驗證碼,得先搞定 cookie,總體流程如下:
- 請求首頁
s9verify_html獲取__trackId和__city,主要是__trackId,__city要不要都行; - 請求
bf.js,這一步不幹啥,但必須得請求,不然 cookie 不能用; - 第一次請求
s.webp,cookie 裏多了c0fc276cce08ba22dc、c1fc276cce08ba22dc和bxf,均為 js 生成; - 第二次請求
s.webp,返回的 cookie 裏多了sbxf,其值和bxf一樣,這一步可以理解為激活 cookie,使其有效。
前兩步倒沒有啥,第 3、4 步都有加密參數 s 和 f,觀察這兩個 s.webp 都是 fetch 請求,所以我們直接一個 fetch 斷點,斷下後可以看到 cb 就是我們需要的兩個參數:
觀察 bf.js 是一個小小的類似 OB 混淆,可以 AST 解一下混淆,但這個邏輯不是很複雜,所以直接硬看也行,關鍵語句 cb = c3['s'](c7, c8),c7 是定值一個字符串 fc276cce08ba22dc,c8 也是定值表示顏色的字符串 rgba(255, 0, 0, 255):
主要是 c3['s']() 這個方法,跟進去,首先會取一下 c0fc276cce08ba22dc、c1fc276cce08ba22dc 和 bxf 三個值,如果有的話,直接返回,如果沒有的話,會生成新的,生成方法主要是 c6 這個函數,如下圖所示:
繼續跟到 c6 方法裏,首先對字符串 rgba(255, 0, 0, 255) 做了一個操作,生成了一張圖片的 base64 字符串:
這裏其實很明顯是 canvas 繪圖的一些操作,跟到 c7 看看確實是這樣的:
這裏對於我們扣算法來説,其實就不需要管了,因為同一台設備的同一個瀏覽器,按照相同的規則繪製的圖片,base64 值是一樣的,所以我們直接忽略 c7 這個方法,直接把生成的 base64 值拿來用就行了。
然後又將 base64 值進行了一個 c3["hash"]() 的操作,根據最終的值,或者跟到方法裏去看,很容易發現這個其實就是個 MD5 的操作:
接着往下看,八個字符串為一組,將 md5 值分為四組,然後四組之間用 0 或者 1 連接,拼接成新的 35 位字符串,拼接的是 0 還是 1,取決於中間的三目語句,判斷是否為 true,支持情況下都是 true,所以扣算法的話根本就沒必要再跟進去看是怎麼判斷的,直接用 1 拼接就完事兒了。然後將固定的字符串 fc276cce08ba22dc 和這 35 位字符串拼接起來再一次 MD5,就得到了參數 s 的值,而參數 f 的值則是這個 35 位字符串。
第一個 s.webp fetch 操作就完成了,接着是第二個 s.webp,就在第一個 fetch 附近,如下圖的 ce 就是第二次的 s、f 參數的值:
這裏生成的方法大致是一樣的,首先 cd 是一個新的圖片的 base64 值,這個值是第一次 s.webp 請求成功返回的,先把這個新的 base64 MD5 加密一下,生成一個新的字符串,相當於替換了第一次請求固定的字符串 fc276cce08ba22dc,後續的流程和第一次都一樣了:
這兩次生成 s 和 f 的值的流程可以精簡成以下 js 實現:
MD5 = require("md5")
var baseImg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAAAXNSR0IArs4c6QAAIABJREFUeF7tnQm..."
function getParams(c8) {
var cb = MD5(baseImg)
, cc = cb.substring(0, 8)
, cd = cb.substring(8, 16)
, ce = cb.substring(16, 24)
, cf = cb.substring(24, 32)
, cg = cc + 1 + cd + 1 + ce + 1 + cf;
return {
"s": MD5(c8 + cg),
"f": cg
};
}
function getFirstParams() {
return getParams("fc276cce08ba22dc")
}
function getSecondParams(img) {
return getParams(MD5(img))
}
console.log(getFirstParams())
console.log(getSecondParams("data:image/png;base64,dqyixSOIJuJN0IRG288itylhqNFFXVqL"))
然後這個 cookie 值,你可以去 Hook 一下看看,但實際上觀察一下就可以發現 c0fc276cce08ba22dc 就是第一次 s.webp 請求的 s 參數,c1fc276cce08ba22dc 和 bxf 就是第一次 s.webp 請求的 f 參數,所以直接拿來用就行了。
獲取驗證碼
帶上前面生成的正確的 cookie,再次請求主頁,響應碼為 200,然後在返回的 html 裏可以看到有個超長的 js 地址,這個 js 直接把 .js 替換成 .jpg 就是驗證碼地址,替換成 .valid 就是驗證結果的地址,這個 js 返回的內容裏面就包含了要點擊的數字。
獲取點擊座標
最終提交的座標是長這樣的:
由於這個圖片是九宮格的樣式,一般的識別都是一排,所以這裏可以將九宮格裁剪後重新排列一下(當然自己會搞深度學習的話可以單獨給這種九宮格訓練一下,就不用重新裁剪排列了),重新排列前後對比如下:
這一步的利用 Python 的 PIL 庫很容易實現:
from PIL import Image
# 打開九宮格驗證碼
captcha = Image.open("captcha.jpg")
# 將圖片等分成三份,每份長寬為 150px 和 50px
part1 = captcha.crop((0, 0, 150, 50))
part2 = captcha.crop((0, 50, 150, 100))
part3 = captcha.crop((0, 100, 150, 150))
part1.save("part1.jpg")
part2.save("part2.jpg")
part3.save("part3.jpg")
# 創建新的圖片,長寬為 450px 和 50px
new_captcha = Image.new("RGB", (450, 50))
# 將三份圖片按順序拼接到新的圖片上
new_captcha.paste(part1, (0, 0))
new_captcha.paste(part2, (150, 0))
new_captcha.paste(part3, (300, 0))
# 保存新的圖片
new_captcha.save("captcha_new.jpg")
這樣處理後,怎樣得到對應的座標呢?以上圖為例,假設我們需要點擊 question = [1, 8, 3, 6],我們識別 captcha_new.jpg 結果為 recognition_result = "172958643",生成最後的座標流程如下:
import random
question = [1, 8, 3, 6] # 要點擊的數字
recognition_result = "172958643" # captcha_new.jpg 識別的結果
mapping_table = {
"0": f"{str(random.randint(15, 35))},{str(random.randint(15, 35))}|",
"1": f"{str(random.randint(65, 85))},{str(random.randint(15, 35))}|",
"2": f"{str(random.randint(115, 135))},{str(random.randint(15, 35))}|",
"3": f"{str(random.randint(15, 35))},{str(random.randint(65, 85))}|",
"4": f"{str(random.randint(65, 85))},{str(random.randint(65, 85))}|",
"5": f"{str(random.randint(115, 135))},{str(random.randint(65, 85))}|",
"6": f"{str(random.randint(15, 35))},{str(random.randint(115, 135))}|",
"7": f"{str(random.randint(65, 85))},{str(random.randint(115, 135))}|",
"8": f"{str(random.randint(115, 135))},{str(random.randint(115, 135))}|",
}
answer = ""
for q in question:
for r in recognition_result:
if q == int(r):
answer += mapping_table[str(recognition_result.index(r))]
print(answer)
每一個數字的圖片大小是 50x50,如果我要點擊上圖中的數字 1,那麼我的 x、y 座標範圍就應該為 [0~50, 0~50],如果我要點擊上圖中的數字 8,那麼我的 x、y 座標範圍就應該為 [100~150, 50~100]。
但是進過多次測試,點擊區域要靠正中心一點,成功率才高,所以座標範圍前後各增加、減少了 15。對應數字 1 的座標範圍就應該是 [15~35, 15~35],數字 8 的座標範圍就應該是 [115~135, 65~85]。
這裏為了簡便,直接定義了一個映射表 mapping_table,如果我點擊數字 8,那麼 captcha_new.jpg 識別結果 172958643 中,8 的位置是 5,對應 mapping_table["5"],也就是 random.randint(115, 135), str(random.randint(65, 85)。