【驗證碼逆向專欄】某多多驗證碼逆向分析

K哥爬虫發表於2024-11-29

7AqXs9.png

宣告

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

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

前言

某多多的驗證碼,型別有很多,滑塊、點選、手勢等等,其中點選的題目繁多,每刷一次都能給你整個新的出來。本文主要對驗證碼的相關加密演算法(不同型別的都大差不差)進行逆向分析,識別模型的訓練後續也會推出相關文章,上述內容都僅供學習交流:

7AqMXZ.png

逆向目標

  • 目標:某多多點選驗證碼逆向分析

  • 網站:感興趣的小夥伴私聊

抓包分析

觸發驗證碼,抓包,/api/phantom/vc_pre_ck_b 介面返回的 salt 值,會參與到加密引數的生成:

7AuBjG.png

/api/phantom/obtain_captcha 介面獲取驗證碼圖片以及題目內容,都經過了加密處理:

7AuLtb.png

  • pictures:驗證碼背景圖片連結;
  • semantics:驗證碼需點選的題目內容。

該介面的請求引數中 anti_contentcaptcha_collect 都經過了加密,verify_auth_token 是觸發驗證碼之後,返回的識別標誌:

7AulyP.png

/api/phantom/user_verify 驗證介面:

7Au0ee.png

  • 驗證成功:{'code': 0, 'leftover': 9, 'result': True};
  • 驗證失敗:{'code': 3002, 'leftover': 9, 'result': False};
  • 驗證時間過長:{'code': 1001, 'leftover': null, 'result': False}。

請求引數與 /api/phantom/obtain_captcha 介面基本相同,多了一個 verify_code,也就是點選的座標:

7Au7Uw.png

anti_content 引數的解決思路,網上有很多詳解文章,跟棧就行了,本文就不對此多加分析了:

7Aw00H.png

逆向分析

驗證碼圖片

一般的驗證碼,其獲取圖片的介面,基本都是直接返回的下載連結,或者經過 Base64 編碼後的值。本案例中,驗證碼的背景圖片連結、標題都經過了加密處理,需要進行逆向分析。

這些圖片內容,在後端進行加密,那必然會在前端解密出真實的連結,然後渲染到頁面上,據此,基本就有兩種方案。

第一種,跟棧分析,/api/phantom/obtain_captcha 介面呼叫的堆疊基本都在 _app.js 檔案中,是非同步的,關於非同步跟棧的分析流程,之前的文章寫過很多,就不再贅述了。跟棧進去,在下圖 return 處下個條件斷點 i.pictures,返回驗證圖片相關資訊時即會斷住:

7AuX9b.png

單步除錯,分析,尋找解密點。向上跟棧到此處,可以看到,c 就是經過加密後的標題,經過 this.formatSemantics 方法處理後,還原出了明文標題:

7AuVnH.png

跟到 this.formatSemantics 中去,kc.Base64.decode(Oa.a.decode(e[0])) 方法解密出了明文值,最終的結果替換掉特殊字元 @ 即可:

7AucpZ.png

kc.Base64.decode 就是 base64 解碼(https://www.kgtools.cn/secret/base64):

7AuvGU.png

跟進到 Oa.a.decode 中,將相關演算法扣下來即可:

7Auazq.png

Python 復現:

# ======================
# -*-coding: Utf-8 -*-
# ======================
import re
import base64

from loguru import logger

c = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
     -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 3, -1, 20, -1, 17, 8, -1, 30,
     -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 12, 22, 10, -1, -1, 15, 14, 6, -1, 5, -1, -1, 7, 18, -1, 25, 9, -1,
     28, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 21, -1, 31, 13, 16, -1, 26, -1, 27, -1, 0, 19, -1, 11, 4, -1,
     -1, 23, -1, 29, -1, -1, -1, -1, -1, -1]

# 正規表示式, 用於匹配多位元組的 UTF-8 字元
b = re.compile(r'[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}')


# UTF-8 解碼
def y(e):
    length = len(e)
    if length == 4:
        t = ((7 & ord(e[0])) << 18 | (63 & ord(e[1])) << 12 | (63 & ord(e[2])) << 6 | 63 & ord(e[3])) - 65536
        return chr(55296 + (t >> 10)) + chr(56320 + (t & 1023))
    elif length == 3:
        return chr((15 & ord(e[0])) << 12 | (63 & ord(e[1])) << 6 | 63 & ord(e[2]))
    else:
        return chr((31 & ord(e[0])) << 6 | 63 & ord(e[1]))


def decode_captcha_img(e):
    t = len(e)
    if t % 8 != 0:
        return None
    n = []
    for r in range(0, t, 8):
        o = c[ord(e[r])]
        i = c[ord(e[r + 1])]
        a = c[ord(e[r + 2])]
        p = c[ord(e[r + 3])]
        h = c[ord(e[r + 4])]
        m = c[ord(e[r + 5])]
        v = c[ord(e[r + 6])]
        g = (31 & o) << 3 | (31 & i) >> 2
        b = (3 & i) << 6 | (31 & a) << 1 | (31 & p) >> 4
        y = (15 & p) << 4 | (31 & h) >> 1
        w = (1 & h) << 7 | (31 & m) << 2 | (31 & v) >> 3
        x = (7 & v) << 5 | 31 & c[ord(e[r + 7])]

        n.append(chr((31 & g) << 3 | b >> 5))
        n.append(chr((31 & b) << 3 | y >> 5))
        n.append(chr((31 & y) << 3 | w >> 5))
        n.append(chr((31 & w) << 3 | x >> 5))
        n.append(chr((31 & x) << 3 | g >> 5))

    _ = ''.join(n)
    _ = _.replace('#', '').replace('@?', '').replace('*&%', '').replace('<$|>', '')
    return _


# 解碼標題
def decode_captcha_title(pic_encode, decode_type=""):
    decoded_img = decode_captcha_img(pic_encode)
    if decoded_img is None:
        return None
    if decode_type == "title":
        decoded_title_str = base64.b64decode(decoded_img).decode('utf-8')
        # 使用正規表示式和 y 函式替換多位元組字元
        decoded_title_str = b.sub(lambda match: y(match.group(0)), decoded_title_str)
        return decoded_title_str
    else:
        return decoded_img


if __name__ == '__main__':
    # 標題測試樣例
    title_pic = "4ntGHUqMtltGiVfsfSqLiVwLtwqFSsiK4lqHXHOMOFfLnUGsORG94HKF6XMSUHwLiG2pjVyt"
    decode_title_result = decode_captcha_title(title_pic, "title")
    logger.info(decode_title_result.replace("@", ""))

    # 背景圖片測試樣例
    bg_pic = "pictures[0]"  # 太長了, 自行替換測試
    decode_bg_result = decode_captcha_title(bg_pic)
    logger.info(decode_bg_result)

背景圖片連結還原比標題少一步解碼:

7AuzLG.png

第二種,直接搜尋 decode,嘗試定位解密演算法的位置。跟棧進去後發現,_app.js 檔案內容未經過混淆,ctrl + f 搜尋一下,把感覺像的地方都打上斷點,重新整理驗證碼,斷住後,也能找到相關演算法的位置:

7AuCzt.png

將還原的結果去掉前面的 data:image/png;base64,(標識資訊),複製到 K哥工具站(https://www.kgtools.cn/convert/base64img)驗證一下,無誤:

7AujHe.png

captcha_collect 引數

總共有兩個介面需要 captcha_collect 引數,分別是獲取圖片的介面 /api/phantom/obtain_captcha 和驗證介面 /api/phantom/user_verify,本文將逐一分析。

兩個介面,和前文一樣,各用一種方案分析。/obtain_captcha 介面的 captcha_collect 引數,跟棧除錯,和前文一樣,下個條件斷點 i.includes('0as'),在 anti_content 引數生成之後,圖片結果響應返回之前,單步往下除錯:

7AuI6w.png

跟到下圖處會發現,此時的 captcha_collect 引數是由 Oa.a.getPrepareToken() 引數生成的:

7AufQ6.png

h(m(JSON.stringify(e)), k, C) 方法生成了 captcha_collect 引數的值,e 包含了一些環境相關的資訊,k、C 明顯也經過了加密處理:

7AuivO.png

先跟到 m 方法中去看看,這裡是將各環境引數組成的字串,使用 Gzip 壓縮演算法處理後得到的結果:

7AusIQ.png

在 JavaScript 中,一般使用兩種方式實現 Gzip 壓縮,分別是 pako 庫和 zlib 模組:

  • pako:JavaScript 庫,支援瀏覽器端和 Node.js 環境。pako 被設計為輕量級且跨平臺,因此可以在瀏覽器中直接使用,無需依賴額外的本地模組或工具。適用於:瀏覽器端應用、前端開發、客戶端 JavaScript 壓縮;
  • zlib:是 Node.js 自帶的原生模組,專門用於在 Node.js 環境中處理壓縮和解壓操作。zlib 基於 C++ 庫,效能上通常更好,適用於處理大規模的資料壓縮任務。適用於:伺服器端應用、Node.js 後端開發、處理大檔案壓縮。

直接使用 pako 庫即可:

npm install pako
const pako = require('pako');
// Gzip 壓縮明文
const compressedText = pako.gzip(text);
// 將壓縮後的位元組陣列轉換為字串
String.fromCharCode.apply(null, new Uint8Array(compressedText));

接下來,分析 h 函式,其就在 m 函式上面,跟過去下斷點,key 和 iv 對應前文提到的 k 和 C:

7Au6Lf.png

AES 加密演算法(https://www.kgtools.cn/secret/aes):

7AuFxc.png

k、C 定義在 getPrepareToken 函式上的 init 函式中,寫的很明顯了 aes_keyaes_iv

7AuZ43.png

是哪加密的呢?往上跟棧,發現 Za(c) 方法生成的 aes_keyaes_iv,c 就是前文提到的 /api/phantom/vc_pre_ck_b 介面響應返回的 salt 值:

7Aue9j.png

跟進去,將程式碼扣下來即可,也可以用 Python 復現加密演算法:

7AuoK5.png

第二個,captcha_collect 引數,直接在 _app.js 檔案中區域性搜尋定位,逐個下斷分析。定位到下圖處,c 就是點選的座標,Oa.a.getImageClickToken() 函式生成的就是驗證介面的 captcha_collect 引數的值:

7Aw9Pm.png

跟進去,這裡的 J(X["concat"]([B, U, $, V, H, q, G])) 就是將陣列 X 和 [B, U, $, V, H, q, G] 拼接之後加密,得到 captcha_collect 的值,包含了環境、軌跡等引數,校驗不嚴:

7Awk64.png

J 函式跟進去和第一個 captcha_collect 引數的加密方式一致,k、C 相同:

7AwTNh.png

至此,整個驗證碼的加密分析流程就結束了。

相關演算法原始碼,會分享到知識星球當中,需要的小夥伴自取,僅供學習交流。

結果驗證

7AwpIY.png

相關文章