聲明 本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關! 本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權, ...
聲明
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯繫作者立即刪除!
逆向目標
- 目標:安某客滑動驗證碼逆向分析
- 主頁:
aHR0cHM6Ly93d3cuYW5qdWtlLmNvbS9jYXB0Y2hhLXZlcmlmeS8/Y2FsbGJhY2s9c2hpZWxkJmZyb209YW50aXNwYW0=
抓包分析
首頁請求,有個初始化函數,其中有個 sessionId
後續會用到。
然後有個 getInfoTp
的請求,Form Data
里有個 dInfo
是加密參數,返回值里 info
也是加密的,包含了圖片信息,返回值 responseId
在後續的請求也會用到。
滑動之後,有個 checkInfoTp
請求,Form Data
里有個 data
是加密參數,包含了軌跡信息,返回值 message
可以看到是否校驗成功。
整體流程就是:請求首頁獲取 sessionId
,請求 getInfoTp
獲取圖片信息和 responseId
,請求 checkInfoTp
校驗是否成功,中間涉及到 dInfo
和 data
兩個加密參數,以及 getInfoTp
返回得到的 info
的解密。
dInfo 生成
先來看 getInfoTp
請求的 dInfo
參數,直接搜索可定位,刷新斷下,大致就可以看出是 AES 加密,傳入了 sessionId
和一個 _taN()
函數的返回值:
_taN()
函數是一些 URL,UA 之類的信息,可以寫死:
往裡跟就可以看到 AES 演算法了:
這裡簡簡單單扣一下,JavaScript 代碼如下:
/* ==================================
# @Time : 2021-12-14
# @Author : 微信公眾號:K哥爬蟲
# @FileName: ajk.js
# @Software: PyCharm
# ================================== */
var CryptoJS = require('crypto-js')
function AESEncrypt(_cRV, _2undefinedp) {
_2undefinedp = _2undefinedp.split("").reduce(function(_PUi, _JrX, _JP9) {
return _JP9 % 2 == 0 ? _PUi + "" : _PUi + _JrX;
}, "");
_2undefinedp = CryptoJS.enc.Utf8.parse(_2undefinedp);
_cRV = "string" == typeof _cRV ? _cRV : JSON.stringify(_cRV);
_cRV = CryptoJS.AES.encrypt(_cRV, _2undefinedp, {
iv: _2undefinedp,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encodeURIComponent(_cRV.toString())
}
function u() {
return {
"sdkv": "3.0.1",
"busurl": "https://www.脫敏處理.com/captcha-verify/?callback=shield&from=antispam",
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"clienttype": "1"
}
}
function getDInfo(sessionId){
return AESEncrypt(u(), sessionId)
}
// 測試樣例
var sessionId = "a8b339ec0c26459598786fee1cce8dc2"
console.log(getDInfo(sessionId))
這段邏輯也可以用 Python 來實現,關鍵代碼如下(脫敏處理,不能直接運行):
# ==================================
# --*-- coding: utf-8 --*--
# @Time : 2021-12-14
# @Author : 微信公眾號:K哥爬蟲
# @FileName: ajk.py
# @Software: PyCharm
# ==================================
import json
import base64
import requests
from lxml import etree
from loguru import logger
from urllib.parse import quote_plus
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
class AESAlgorithm:
@staticmethod
def encrypt(aes_key_iv, text):
""" 對明文進行加密 """
cipher = AES.new(key=bytes(aes_key_iv, encoding='utf-8'), mode=AES.MODE_CBC, iv=bytes(aes_key_iv, encoding='utf-8'))
result = base64.b64encode(cipher.encrypt(pad(text.encode('utf-8'), 16))).decode('utf-8')
result = quote_plus(result)
return result
@staticmethod
def decrypt(aes_key_iv, text):
""" 對密文進行解密 """
cipher = AES.new(key=bytes(aes_key_iv, encoding='utf-8'), mode=AES.MODE_CBC, iv=bytes(aes_key_iv, encoding='utf-8'))
result = unpad(cipher.decrypt(base64.b64decode(text)), 16).decode('utf-8')
return result
class AJKSlide:
def __init__(self, index_url, user_agent):
self.aes = AESAlgorithm()
self.index_url = index_url
self.user_agent = user_agent
self.headers = {"user-agent": self.user_agent}
def get_session_id(self):
""" 獲取 sessionId """
response = requests.get(url=self.index_url, headers=self.headers).text
session_id = etree.HTML(response).xpath("//input[@name='sessionId']/@value")[0]
logger.info(f"sessionId ==> {session_id}")
return session_id
@staticmethod
def get_aes_key_iv(session_id):
"""設置 AES key 和 iv"""
aes_key_iv = ''
for index, value in enumerate(session_id):
if index % 2 != 0:
aes_key_iv += value
logger.info(f"處理 sessionId 獲取 aes key iv ==> {aes_key_iv}")
return aes_key_iv
def get_d_info(self, aes_key_iv):
"""獲取 dInfo"""
sdk_info = {
"sdkv": "3.0.1",
"busurl": self.index_url,
"useragent": self.user_agent,
"clienttype": 1
}
d_info = self.aes.encrypt(aes_key_iv, json.dumps(sdk_info))
logger.info(f'dInfo ==> {d_info}')
return d_info
def run(self, session_id=None):
if not session_id:
session_id = self.get_session_id()
aes_key_iv = self.get_aes_key_iv(session_id)
self.get_d_info(aes_key_iv)
if __name__ == '__main__':
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"
index_url_ = "https://www.脫敏處理.com/captcha-verify/?callback=shield&from=antispam"
ajk_slide = AJKSlide(index_url_, UA)
ajk_slide.run()
getInfoTp 解密
getInfoTp
這個介面返回的 info
的值是加密的,前面我們已經知道用到了 AES 加密演算法,這裡可以直接猜測也是用的的 AES 來解密的,找到 AESDecrypt
這個方法,下個斷點,刷新發現斷下之後傳入了兩個參數,第一個正是 info
的內容,第二個則是 sessionId
。
解密結果可以看到滑塊的圖片地址等信息:
data 生成
接下來就是 checkInfoTp
提交驗證了,要搞清楚提交的 data
是什麼東西,同樣搜索打斷點,如下圖所示 _5DD
就是 data
值,傳過來的。
往上跟棧,可以看到 _Ug0
裡面有個 track
參數,這明顯就是軌跡了,同樣最後的結果經過了 AES 加密。
再往上跟,可以看到 _Ug0
由三個參數組成,x
是水平滑動的距離,track
是軌跡,p
是定值。
軌跡處理
軌跡生成前,得先識別缺口得到要滑動的距離,方式有很多,比如 OpenCV
、開源的 ddddocr
,或者直接打碼平臺都行,這裡唯一要註意的一點就是圖片是有縮放的,原始尺寸 480 × 270 px
渲染後的尺寸 280 × 158 px
,比例大概是 1:0.5833333333333333
,可以先將圖片進行縮放後再識別,也可以先識別距離後再將距離進行縮放。
軌跡的處理,該站點校驗並不太嚴格,所以可以自己寫一下,關於滑塊的軌跡處理,主要有縮放法、本地軌跡庫、根據一些函數來生成軌跡,如緩動函數、貝塞爾曲線等,K哥以後再單獨寫一篇文章來介紹,本例中可以使用縮放法,先採集一條正常的,手動滑出來的軌跡,然後根據識別出的實際距離和樣本軌跡中的距離相比,得到一個比值,然後將樣本中的 x 值和時間值都做一個對應的縮放,生成新的軌跡,主要代碼如下:
def generate_track(distance):
"""生成軌跡,樣本距離為 126"""
ratio = distance / 126
new_track = ""
base_track = "29,11,0|29,11,11|29,11,26|33,11,56|34,11,66|36,11,67|39,11,76|41,11,83|43,11,86|46,11,92|49,11,98|50,11,102|52,11,106|53,11,111|55,11,116|57,11,118|59,11,123|60,11,126|62,11,132|64,12,134|65,12,138|66,12,142|68,12,148|69,12,151|70,13,155|71,13,158|72,13,164|74,13,166|75,13,170|76,14,174|77,14,180|79,14,182|81,14,186|82,14,196|84,14,198|86,14,207|87,15,212|89,15,219|90,15,223|92,15,230|93,15,234|94,15,239|95,15,243|98,15,246|100,15,250|102,15,260|105,15,262|106,15,266|108,15,270|109,16,276|111,16,278|113,16,283|115,16,286|117,16,291|118,16,294|119,16,298|121,16,302|123,16,309|124,16,311|125,16,315|126,16,319|129,16,324|130,16,327|131,16,331|132,16,334|132,16,388|132,16,522|133,16,566|134,16,574|135,16,575|136,16,594|137,16,620|138,16,625|139,16,652|140,16,657|141,17,676|141,18,680|142,18,684|143,18,688|144,18,716|145,18,724|146,18,796|147,19,828|148,19,860|149,19,888|149,19,890|150,19,916|151,20,932|152,20,936|152,20,1021|153,20,1150|154,20,1152|155,20,1236|155,20,1388|155,20,1522|155,20,1717|"
base_track = base_track.split("|")[:-1]
for track in base_track:
t = track.split(",")
new_track += str(int(int(t[0]) * ratio)) + "," + str(t[1]) + "," + str(int(int(t[2]) * ratio)) + "|"
logger.info(f"new_track ==> {new_track}")
return new_track
結果驗證
整個過程比較簡單,驗證成功。