博客 / 詳情

返回

【驗證碼逆向專欄】百某網數字九宮格驗證碼逆向分析

聲明

本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據接口等均已做脱敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!

本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯繫作者立即刪除!

目標

目標:百 X 網數字九宮格驗證碼逆向分析

網址:aHR0cHM6Ly9iZWlqaW5nLmJhaXhpbmcuY29tL296L3M5dmVyaWZ5X2h0bWw=

01

抓包分析

本例中的驗證碼不是很難,但網站埋了點兒坑,容易出現識別正確、參數也正確,但仍然請求不成功的情況。訪問主頁響應碼為 307,接着請求了一個 bf.js 和兩個 s.webp 的圖片,然後又跳轉到首頁出現驗證碼。如果你沒有以上步驟,請求主頁直接就是 200 出現驗證碼,則需要清除 cookie 後再訪問,因為第一次 307 到請求 bf.js 再到兩次 s.webp 都是在設置 cookie。

第一次請求主頁,response headers 會設置一個名為 _trackId__city 的 cookie,如下圖所示:

02

然後帶着這兩個 cookie 請求了一個 bf.js,這個 js 用於後續兩個 s.webp 請求參數的加密,這裏注意,第一個坑,雖然可以直接調試 js 扣算法下來後面直接用就行了,但是這個 js 必須得請求一遍,不然後面請求主頁的時候一直是 307。

03

然後請求第一個 s.webp,get 請求,有三個參數:cfsf,明顯是加密得來的,同時請求的 cookie 也多了三個值:c0fc276cce08ba22dcc1fc276cce08ba22dcbxf,如下圖所示:

04

05

然後請求第二個 s.webp,和第一個類似,get 請求也有三個參數:cfsf,cookie 和第一個一樣,但第二次請求返回了一個名為 sbxf 的新 cookie,其值和 bxfc1fc276cce08ba22dc 其實是一樣的,如下圖所示:

06

然後帶上 __trackId__cityc0fc276cce08ba22dcc1fc276cce08ba22dcbxfsbxf 這六個 cookie 再次訪問主頁,就是驗證碼頁面了,返回的 html 裏有個新的 js,很長一串,如下圖所示:

07

08

然後觀察這個 js,裏面包含了驗證碼圖片的 URL,以及需要點擊的數字,如下圖所示:

09

10

點擊驗證後,會給 verify_url 發一個 get 請求,請求參數主要有一個 data,即點擊座標(這個座標也有講究,有可能你的值是對的,但有時候也不成功,這個後文再細説),cookie 和前面的請求一樣,如果驗證成功,會返回 ret 為 0,且有一個 code 供後續請求使用,如下圖所示:

11

12

獲取 cookie

這裏再注意一點,所有的請求,header 只需要 RefererUser-Agent 就行了,不要亂加,比如多了個 Host 也有可能導致後續請求不成功。

想要拿驗證碼,得先搞定 cookie,總體流程如下:

  1. 請求首頁 s9verify_html 獲取 __trackId__city,主要是 __trackId__city 要不要都行;
  2. 請求 bf.js,這一步不幹啥,但必須得請求,不然 cookie 不能用;
  3. 第一次請求 s.webp,cookie 裏多了 c0fc276cce08ba22dcc1fc276cce08ba22dcbxf,均為 js 生成;
  4. 第二次請求 s.webp,返回的 cookie 裏多了 sbxf,其值和 bxf 一樣,這一步可以理解為激活 cookie,使其有效。

前兩步倒沒有啥,第 3、4 步都有加密參數 sf,觀察這兩個 s.webp 都是 fetch 請求,所以我們直接一個 fetch 斷點,斷下後可以看到 cb 就是我們需要的兩個參數:

13

14

觀察 bf.js 是一個小小的類似 OB 混淆,可以 AST 解一下混淆,但這個邏輯不是很複雜,所以直接硬看也行,關鍵語句 cb = c3['s'](c7, c8),c7 是定值一個字符串 fc276cce08ba22dc,c8 也是定值表示顏色的字符串 rgba(255, 0, 0, 255)

15

主要是 c3['s']() 這個方法,跟進去,首先會取一下 c0fc276cce08ba22dcc1fc276cce08ba22dcbxf 三個值,如果有的話,直接返回,如果沒有的話,會生成新的,生成方法主要是 c6 這個函數,如下圖所示:

16

繼續跟到 c6 方法裏,首先對字符串 rgba(255, 0, 0, 255) 做了一個操作,生成了一張圖片的 base64 字符串:

17

這裏其實很明顯是 canvas 繪圖的一些操作,跟到 c7 看看確實是這樣的:

18

這裏對於我們扣算法來説,其實就不需要管了,因為同一台設備的同一個瀏覽器,按照相同的規則繪製的圖片,base64 值是一樣的,所以我們直接忽略 c7 這個方法,直接把生成的 base64 值拿來用就行了。

然後又將 base64 值進行了一個 c3["hash"]() 的操作,根據最終的值,或者跟到方法裏去看,很容易發現這個其實就是個 MD5 的操作:

19

20

接着往下看,八個字符串為一組,將 md5 值分為四組,然後四組之間用 0 或者 1 連接,拼接成新的 35 位字符串,拼接的是 0 還是 1,取決於中間的三目語句,判斷是否為 true,支持情況下都是 true,所以扣算法的話根本就沒必要再跟進去看是怎麼判斷的,直接用 1 拼接就完事兒了。然後將固定的字符串 fc276cce08ba22dc 和這 35 位字符串拼接起來再一次 MD5,就得到了參數 s 的值,而參數 f 的值則是這個 35 位字符串。

21

第一個 s.webp fetch 操作就完成了,接着是第二個 s.webp,就在第一個 fetch 附近,如下圖的 ce 就是第二次的 s、f 參數的值:

22

這裏生成的方法大致是一樣的,首先 cd 是一個新的圖片的 base64 值,這個值是第一次 s.webp 請求成功返回的,先把這個新的 base64 MD5 加密一下,生成一個新的字符串,相當於替換了第一次請求固定的字符串 fc276cce08ba22dc,後續的流程和第一次都一樣了:

23

這兩次生成 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 參數,c1fc276cce08ba22dcbxf 就是第一次 s.webp 請求的 f 參數,所以直接拿來用就行了。

獲取驗證碼

帶上前面生成的正確的 cookie,再次請求主頁,響應碼為 200,然後在返回的 html 裏可以看到有個超長的 js 地址,這個 js 直接把 .js 替換成 .jpg 就是驗證碼地址,替換成 .valid 就是驗證結果的地址,這個 js 返回的內容裏面就包含了要點擊的數字。

24

25

26

獲取點擊座標

最終提交的座標是長這樣的:

27

由於這個圖片是九宮格的樣式,一般的識別都是一排,所以這裏可以將九宮格裁剪後重新排列一下(當然自己會搞深度學習的話可以單獨給這種九宮格訓練一下,就不用重新裁剪排列了),重新排列前後對比如下:

28

這一步的利用 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)

結果驗證

29

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.