宣告
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整程式碼,抓包內容、敏感網址、資料介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯絡作者立即刪除!
前言
某多多的驗證碼,型別有很多,滑塊、點選、手勢等等,其中點選的題目繁多,每刷一次都能給你整個新的出來。本文主要對驗證碼的相關加密演算法(不同型別的都大差不差)進行逆向分析,識別模型的訓練後續也會推出相關文章,上述內容都僅供學習交流:
逆向目標
-
目標:某多多點選驗證碼逆向分析
-
網站:感興趣的小夥伴私聊
抓包分析
觸發驗證碼,抓包,/api/phantom/vc_pre_ck_b
介面返回的 salt 值,會參與到加密引數的生成:
/api/phantom/obtain_captcha
介面獲取驗證碼圖片以及題目內容,都經過了加密處理:
- pictures:驗證碼背景圖片連結;
- semantics:驗證碼需點選的題目內容。
該介面的請求引數中 anti_content
、captcha_collect
都經過了加密,verify_auth_token
是觸發驗證碼之後,返回的識別標誌:
/api/phantom/user_verify
驗證介面:
- 驗證成功:{'code': 0, 'leftover': 9, 'result': True};
- 驗證失敗:{'code': 3002, 'leftover': 9, 'result': False};
- 驗證時間過長:{'code': 1001, 'leftover': null, 'result': False}。
請求引數與 /api/phantom/obtain_captcha
介面基本相同,多了一個 verify_code
,也就是點選的座標:
anti_content
引數的解決思路,網上有很多詳解文章,跟棧就行了,本文就不對此多加分析了:
逆向分析
驗證碼圖片
一般的驗證碼,其獲取圖片的介面,基本都是直接返回的下載連結,或者經過 Base64 編碼後的值。本案例中,驗證碼的背景圖片連結、標題都經過了加密處理,需要進行逆向分析。
這些圖片內容,在後端進行加密,那必然會在前端解密出真實的連結,然後渲染到頁面上,據此,基本就有兩種方案。
第一種,跟棧分析,/api/phantom/obtain_captcha
介面呼叫的堆疊基本都在 _app.js
檔案中,是非同步的,關於非同步跟棧的分析流程,之前的文章寫過很多,就不再贅述了。跟棧進去,在下圖 return 處下個條件斷點 i.pictures
,返回驗證圖片相關資訊時即會斷住:
單步除錯,分析,尋找解密點。向上跟棧到此處,可以看到,c 就是經過加密後的標題,經過 this.formatSemantics
方法處理後,還原出了明文標題:
跟到 this.formatSemantics
中去,kc.Base64.decode(Oa.a.decode(e[0]))
方法解密出了明文值,最終的結果替換掉特殊字元 @ 即可:
kc.Base64.decode
就是 base64 解碼(https://www.kgtools.cn/secret/base64):
跟進到 Oa.a.decode
中,將相關演算法扣下來即可:
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)
背景圖片連結還原比標題少一步解碼:
第二種,直接搜尋 decode,嘗試定位解密演算法的位置。跟棧進去後發現,_app.js
檔案內容未經過混淆,ctrl + f 搜尋一下,把感覺像的地方都打上斷點,重新整理驗證碼,斷住後,也能找到相關演算法的位置:
將還原的結果去掉前面的 data:image/png;base64,
(標識資訊),複製到 K哥工具站(https://www.kgtools.cn/convert/base64img)驗證一下,無誤:
captcha_collect 引數
總共有兩個介面需要 captcha_collect
引數,分別是獲取圖片的介面 /api/phantom/obtain_captcha
和驗證介面 /api/phantom/user_verify
,本文將逐一分析。
兩個介面,和前文一樣,各用一種方案分析。/obtain_captcha
介面的 captcha_collect
引數,跟棧除錯,和前文一樣,下個條件斷點 i.includes('0as')
,在 anti_content 引數生成之後,圖片結果響應返回之前,單步往下除錯:
跟到下圖處會發現,此時的 captcha_collect
引數是由 Oa.a.getPrepareToken()
引數生成的:
h(m(JSON.stringify(e)), k, C)
方法生成了 captcha_collect
引數的值,e 包含了一些環境相關的資訊,k、C 明顯也經過了加密處理:
先跟到 m 方法中去看看,這裡是將各環境引數組成的字串,使用 Gzip 壓縮演算法處理後得到的結果:
在 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:
AES 加密演算法(https://www.kgtools.cn/secret/aes):
k、C 定義在 getPrepareToken 函式上的 init 函式中,寫的很明顯了 aes_key
、aes_iv
:
是哪加密的呢?往上跟棧,發現 Za(c) 方法生成的 aes_key
和 aes_iv
,c 就是前文提到的 /api/phantom/vc_pre_ck_b
介面響應返回的 salt 值:
跟進去,將程式碼扣下來即可,也可以用 Python 復現加密演算法:
第二個,captcha_collect
引數,直接在 _app.js
檔案中區域性搜尋定位,逐個下斷分析。定位到下圖處,c 就是點選的座標,Oa.a.getImageClickToken()
函式生成的就是驗證介面的 captcha_collect
引數的值:
跟進去,這裡的 J(X["concat"]([B, U, $, V, H, q, G]))
就是將陣列 X 和 [B, U, $, V, H, q, G] 拼接之後加密,得到 captcha_collect
的值,包含了環境、軌跡等引數,校驗不嚴:
J 函式跟進去和第一個 captcha_collect
引數的加密方式一致,k、C 相同:
至此,整個驗證碼的加密分析流程就結束了。
相關演算法原始碼,會分享到知識星球當中,需要的小夥伴自取,僅供學習交流。