本文章中所有內容僅供學習交流使用,不用于其他任何目的。否則由此產生的一切后果均與作者無關!
在爬蟲開發過程中,滑塊驗證碼常常成為我們獲取數據的一大阻礙。而滑塊驗證碼的加密方式多種多樣,其中軌跡加密和坐標加密是比較常見的兩種方式。本文將詳細介紹這兩種加密方式的原理以及如何進行逆向分析。
驗證碼逆向過程分析
第一步,找生成圖片的接口(接口可能有加密參數)獲取圖片url或者圖片base64編碼,可能還有id,token等值。
第二步,用識別工具識別圖片,獲取缺口坐標(用函數模擬軌跡)
第三步,獲取缺口坐標(軌跡)(可能會進行加密)和第一次接口獲取id、token等(可能會進行加密),可能直接攜帶加密坐標(軌跡)和token、id直接請求頁面。也可能作為請求體或者cookie、請求頭進行另一個接口請求(驗證接口可能不止一個,甚至多級),請求成功返回一個成功的token(可能有時間效期,token可能只能用一次,也可能用多次)。攜帶token請求你要請求的頁面,成功請求。
最好用seesion = requests.session()進行請求
接口不一定一次性返回 圖片+id+token
?識別后并不一定直接拿“缺口坐標”(軌跡)就能用,可能會進行加密
驗證接口可能不止一個,甚至多級
還有一個典型的「狀態依賴」問題:
驗證碼接口依賴會話狀態
/captcha/image
只做一件事:根據當前會話生成一張圖并返回其token。
緊接著調 /captcha/check
也返回一個token
兩個token一模一樣
但如果只請求/captcha/image獲取
token,沒有經過/captcha/check
直接去攜帶token請求頁面,是通過不了的。
以下兩個例子:
/captcha/image返回的token和/captcha/check返回的token一模一樣
,
但是如果跳過/captcha/check,直接用/captcha/image返回的token去請求頁面。是沒法通過的。
一句話:token 必須被 /captcha/check
把狀態從 issued
變成 verified
才能繼續用,否則服務端會判定“未經驗證的驗證碼”。
/captcha/image
接口
/captcha/check接口
返回響應內容
要請求的接口內容
這三個token一樣,但是跳過/captcha/check接口
直接用/captcha/image
接口的token是請求不通過的。
1.坐標加密
目標網址:aHR0cHM6Ly96YnRiLmdkLmdvdi5jbi8jL2p5Z2c=
大于五頁后都會有滑塊驗證碼
抓包分析,圖片接口
圖片接口請求頭字段加密字段
找加密位置非常簡單,xhr跟棧就行了
回調第一個就是加密的位置
事實上是搜不到的,對字段進行打亂重組,但是跟棧也輕輕松松
這個值是要求的
求出來這種格式像什么
CryptoJS 中 WordArray 對象的內部格式,哈希加密,.toString()
測試,這個就是原型的SHA256加密,直接用原生庫即可
內部進行字符串拼接
最后一個參數需要傳驗證碼接口的請求體
輸出為請求體的拼接
直接寫死即可(這網站請求詳情,則不能寫死,因為頁數變化,時間變化)
簡簡單單請求出參數。獲取兩張圖片
在驗證碼接口一共四個參數有用到
兩張圖片
secretKey為密鑰
token為驗證接口攜帶
用ddddcor求出x距離
ocr = ddddocr.DdddOcr()
img_1 = base64.b64decode(response.json()['data']['repData']['jigsawImageBase64'])
img_2 = base64.b64decode(response.json()['data']['repData']['originalImageBase64'])
token = response.json()['data']['repData']['token']
secretKey = response.json()['data']['repData']['secretKey']
print(ocr.slide_match(img_1,img_2)['target'][0])
第二個接口check
直接搜,比較簡單
這個X對于是距離。
進去NO函數。非常容易的AES加密
def aes_encrypted(w, L):data = json.dumps(w, separators=(',', ':')).encode('utf-8')key = L.encode('utf-8')[:16].ljust(16, b'\0')cipher = AES.new(key, AES.MODE_ECB)ct = cipher.encrypt(pad(data, AES.block_size))return base64.b64encode(ct).decode('ascii')
對坐標加密
有個非常巨大的坑,表面L已經寫好了,其實傳參L不是這個值
惡心。實際L值是第一個接口返回secretKey
最后成功獲取token
在詳情頁攜帶token,就能順利請求內容了。
2.軌跡加密
目標網址:aHR0cHM6Ly9janljLmhiYmlkZGluZy5jb20uY24vaHViZWl5dGgvanl4eC90cmFkZV9pbmZvci5odG1s
驗證碼接口直接請求即可,非常友好(后面你就知道有一個巨大的坑)
圖片處理請求距離
img_1 = base64.b64decode(response.json()['captcha']['templateImage'].split('base64,')[1])
img_2 = base64.b64decode(response.json()['captcha']['backgroundImage'].split('base64,')[1])
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
print(x)
非常友好,瞬間出來
看看第二個驗證接口
一眼看出,id是第一個接口請求的,data加密很明顯是base64加密
響應cookies為第二個接口求這個值
base64解密看一下
解析一下
'bgImageWidth': 260, 'bgImageHeight': 159, 'sliderImageWidth': 49, 'sliderImageHeight': 159,這四個參數為兩張驗證碼大小
"startSlidingTime":"2025-08-14T09:55:48.568Z","endSlidingTime":"2025-08-14T09:55:50.110Z",
startSlidingTime開始時間,endSlidingTime點擊到驗證碼驗證時間
trackList就是軌跡
一大串軌跡,跟棧調試一下
x代表移動距離,y代表上下多動,由x的變化看出,是先慢后快,在慢,t則是時間
軌跡生成的函數
def gen_track( gap_x, gap_y=0, seed=None):# ran_x = random.randint(19, 40)"""模擬軌跡生成生成「慢→快→慢」三段式軌跡gap_x : 缺口 x 像素gap_y : y 軸最大抖動像素seed : 隨機種子,方便調試"""if seed:random.seed(seed)# 總步數 & 總耗時steps = random.randint(40, 60) # 步數少一點更平滑total_t = random.randint(2800, 3500) # 總耗時 2.8~3.5 strack = []x0, y0 = 0, 0t0 = 2383 # 起始時間戳gap_x = gap_xfor i in range(steps + 1):# 1. 三段式 S 曲線映射t = i / steps# 三次貝塞爾緩動:慢→快→慢ratio = 3 * t ** 2 - 2 * t ** 3if i != 0:# 2. 計算本次坐標x = int(round(x0 + gap_x * ratio)) + 1y = y0 + random.randint(-1, 1)if i == 0:x = 0y = 0# 3. 時間分布也按 S 曲線:開始稀疏、中間密集、末尾稀疏dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)t0 += dt# 4. 事件類型if i == 0:ev_type = "down"elif i == steps:ev_type = "up"else:ev_type = "move"track.append({"x": x, "y": y, "type": ev_type, "t": t0})# 提前到達終點就停if x >= gap_x:track[-1]['x'] = gap_xtrack[-1]['type'] = "up"breakreturn track
y上下抖動,X先慢后快在慢,t時間不能太快,傳入x距離即可,t實際為毫秒,隨機累加
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
在把時間整理一下
tart_iso = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
end_iso = (datetime.datetime.utcnow() +datetime.timedelta(milliseconds=track[-1]['t'])).isoformat(timespec='milliseconds') + 'Z'
全部求出來,再用base64編碼
ef gen_track(gap_x, gap_y=0, seed=None):# ran_x = random.randint(19, 40)"""模擬軌跡生成生成「慢→快→慢」三段式軌跡gap_x : 缺口 x 像素gap_y : y 軸最大抖動像素seed : 隨機種子,方便調試"""if seed:random.seed(seed)# 總步數 & 總耗時steps = random.randint(40, 60) # 步數少一點更平滑total_t = random.randint(2800, 3500) # 總耗時 2.8~3.5 strack = []x0, y0 = 0, 0t0 = 2383 # 起始時間戳gap_x = gap_xfor i in range(steps + 1):# 1. 三段式 S 曲線映射t = i / steps# 三次貝塞爾緩動:慢→快→慢ratio = 3 * t ** 2 - 2 * t ** 3if i != 0:# 2. 計算本次坐標x = int(round(x0 + gap_x * ratio)) + 1y = y0 + random.randint(-1, 1)if i == 0:x = 0y = 0# 3. 時間分布也按 S 曲線:開始稀疏、中間密集、末尾稀疏dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)t0 += dt# 4. 事件類型if i == 0:ev_type = "down"elif i == steps:ev_type = "up"else:ev_type = "move"track.append({"x": x, "y": y, "type": ev_type, "t": t0})# 提前到達終點就停if x >= gap_x:track[-1]['x'] = gap_xtrack[-1]['type'] = "up"breakreturn track
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
tart_iso = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
end_iso = (datetime.datetime.utcnow() +datetime.timedelta(milliseconds=track[-1]['t'])).isoformat(timespec='milliseconds') + 'Z'
payload = {'bgImageWidth': 260,'bgImageHeight': 159,'sliderImageWidth': 49,'sliderImageHeight': 159,'startSlidingTime': tart_iso,'endSlidingTime': end_iso,'trackList': track
}
data = base64.b64encode(json.dumps(payload, separators=(',', ':')).encode()).decode()
url = "https://cjyc.hbbidding.com.cn/captcha/check2"
payload = {"id": id ,"data":data
}
response = requests.post(url, headers=headers, cookies=cookies, data=payload)
print(response.text)
最終結果請求失敗,請求多次還是失敗
我在想,這么完美的請求方式,為什么錯了,最后調試好久,對比距離得出
最后x的距離是網頁驗證碼的距離,不是實際下載圖片的距離
實際下載圖片這么大
最終的大小按頁面大小算
所以還得把圖片修改一下
用opencv,改一下圖片大小,再識別距離
def _resize(b64: str, w: int, h: int) -> str:"""把 base64 圖片縮放成指定寬高后再 base64 編碼"""img = cv2.imdecode(np.frombuffer(base64.b64decode(b64), np.uint8), cv2.IMREAD_COLOR)img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)return base64.b64encode(cv2.imencode('.jpg', img)[1]).decode()
以下為核心代碼。圖片大小,軌跡生成
response = requests.get(url, headers=headers, cookies=cookies, params=params)
id = response.json()['id']def _resize(b64: str, w: int, h: int) -> str:"""把 base64 圖片縮放成指定寬高后再 base64 編碼"""img = cv2.imdecode(np.frombuffer(base64.b64decode(b64), np.uint8), cv2.IMREAD_COLOR)img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)return base64.b64encode(cv2.imencode('.jpg', img)[1]).decode()
slider_b64 = _resize(response.json()['captcha']['templateImage'].split(',', 1)[-1], 49, 159)
bg_b64 = _resize(response.json()['captcha']['backgroundImage'].split(',', 1)[-1], 260, 159)
target_bytes = base64.b64decode(slider_b64)
bg_bytes = base64.b64decode(bg_b64)
img_1 = base64.b64decode(slider_b64)
img_2 = base64.b64decode(bg_b64)
def gen_track(gap_x, gap_y=0, seed=None):# ran_x = random.randint(19, 40)"""模擬軌跡生成生成「慢→快→慢」三段式軌跡gap_x : 缺口 x 像素gap_y : y 軸最大抖動像素seed : 隨機種子,方便調試"""if seed:random.seed(seed)# 總步數 & 總耗時steps = random.randint(40, 60) # 步數少一點更平滑total_t = random.randint(2800, 3500) # 總耗時 2.8~3.5 strack = []x0, y0 = 0, 0t0 = 2383 # 起始時間戳gap_x = gap_xfor i in range(steps + 1):# 1. 三段式 S 曲線映射t = i / steps# 三次貝塞爾緩動:慢→快→慢ratio = 3 * t ** 2 - 2 * t ** 3if i != 0:# 2. 計算本次坐標x = int(round(x0 + gap_x * ratio)) + 1y = y0 + random.randint(-1, 1)if i == 0:x = 0y = 0# 3. 時間分布也按 S 曲線:開始稀疏、中間密集、末尾稀疏dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)t0 += dt# 4. 事件類型if i == 0:ev_type = "down"elif i == steps:ev_type = "up"else:ev_type = "move"track.append({"x": x, "y": y, "type": ev_type, "t": t0})# 提前到達終點就停if x >= gap_x:track[-1]['x'] = gap_xtrack[-1]['type'] = "up"breakreturn track
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
請求失敗
多試幾次就請求成功了。請求概率挺高的
請求成功