文章目錄
- 1. 寫在前面
- 2. 接口分析
- 3. 加密分析
- 4. 算法還原
- 5. 設備指紋風控分析與繞過
【🏠作者主頁】:吳秋霖
【💼作者介紹】:擅長爬蟲與JS加密逆向分析!Python領域優質創作者、CSDN博客專家、阿里云博客專家、華為云享專家。一路走來長期堅守并致力于Python與爬蟲領域研究與開發工作!
【🌟作者推薦】:對爬蟲領域以及JS逆向分析感興趣的朋友可以關注《爬蟲JS逆向實戰》《深耕爬蟲領域》
未來作者會持續更新所用到、學到、看到的技術知識!包括但不限于:各類驗證碼突防、爬蟲APP與JS逆向分析、RPA自動化、分布式爬蟲、Python領域等相關文章
作者聲明:文章僅供學習交流與參考!嚴禁用于任何商業與非法用途!否則由此產生的一切后果均與作者無關!如有侵權,請聯系作者本人進行刪除!
1. 寫在前面
??今天端午節,祝大家端午安康!最近事情比較多,所以又有很長一段時間沒有寫文章了(差點忘了這個好習慣~~
)。剛好今天過節有時間,把前些天看的一些小案例分享一下。這期分析的是某航空網站的加密參數跟它的設備指紋風控,通過非登錄游客身份搜索航班機票查詢然后解決風控標記彈行為驗證(比較簡單適合新手研究
)
分析網站:
aHR0cHM6Ly93d3cuYWlyY2hpbmEuY29tLmNuL2dhdGV3YXkvYXBpL2ZsaWdodC9saXN0
2. 接口分析
隨便在搜索入口查詢一趟航班信息,可以看到發包請求參數params
一段密文,如下所示:
除了請求參數加密外,請求頭也有一個x-device-token
參數疑似動態生成的(但不是直接網站的JS代碼層面生成的
)。如下所示:
像這種Token
請求頭的參數一般情況下我們可以先不去管它,先去分析請求參數的加密。頭部一般非簽名的核心參數,你固定測試一次兩次是可以重放的。但是請求參數的加密對什么加密了、明文是什么以及后續能夠模擬偽造出請求來測試是必須
初看這個Token
的時候我感覺在哪里見過(比較熟悉
)。有經驗的可能會猜測它是由某個接口動態請求之后服務端下發的(在有效的時間范圍可以有效固定去使用
)做過某東逆向分析的可以發現它的這個參數開頭tak01...
跟它們官方的那個設備指紋高度相似(后續驗證發現就是用的某東的設備指紋風控)
3. 加密分析
開始定位找到發包加密的位置,這里可以直接通過XHR
斷點去跟棧就能夠溯源到整個發包跟調用加密的位置,如下所示:
可以看到o.P
就是加密方法,直接跳轉到對應的JS代碼,如下:
var c = function(e) {try {var t = encodeURIComponent(JSON.stringify(e));return o.sm2.doEncrypt(t, "04064c2a3bcafba2c1ca4f5fb8ecd876b23d70fc4479b78f3c8066c02a8c17749458bca86361bc563d2501b61e2ac93a676a1305893aafcc6be2ea48ecb048672e", a.yV)} catch (n) {return console.log(n),""}}
看上面加密代碼的入口,比較明顯的可以看到使用了疑似sm2
國密算法。像一般我們猜測到了加密算法大致看看是否標準的就可以直接使用自己擅長的語言導個包去進行還原。后面那一串的話看起來就是它的密鑰,sm2
一般長度256 bit
,開頭04
表示非壓縮。后面那64
字節(128個十六進制字符
),大致如下得出:
04 || <X (64 hex)> || <Y (64 hex)> = 1 + 64 + 64 = 129 hex digits
略微看一下它的那個JS實現,是標準一個sm2
。然后要還原這個加密算法的方式可以直接把doEncrypt
的JS代碼扣出來(webpack
)然后導出模塊再調用即可。還有就是確定完整個加密算法是標準的
還是變異的
或者魔改的
后用其他語言實現(代碼量會大大降低
)
4. 算法還原
既然是分享,這里扣webpack
跟使用純算還原的方式都說一下。首先跳轉到sm2.doEncrypt
代碼處,開始扣一些JS
。扣代碼的也是講究精扣
跟粗扣
的
這里精扣
的話就幾百行涉及加密的這幾段JS代碼就行。新手你全部粗扣
下來也無所謂(4W多行-不用在意這些細節-能用就行
)
然后再把webpack
內部的模塊加載函數導出來,模塊加載器的結構一般如下所示:
function enc(n) {var f = t[n];if (void 0 !== f) return f.exports;...e[n].call(r.exports, r, r.exports,enc)...return r.exports
}
t[n]
: 緩存已加載模塊
e[n]
: 模塊定義函數對象(模塊id -> 函數
)類似Dict
r.exports
: 模塊的導出結果
之后找到SM2
加密方法的所在模塊ID(70686
),如下所示:
那么這個時候我們就可以在JS中直接導出扣下來的webpack
模塊,重新封裝一下加密方法。代碼實現如下:
function encrypt(e) {var o = sm2(70686), // 加密模塊try {var t = encodeURIComponent(JSON.stringify(e));return 0.sm2.doEncrypt(t, "04064c2a3bcafba2c1ca4f5fb8ecd876b23d70fc4479b78f3c8066c02a8c17749458bca86361bc563d2501b61e2ac93a676a1305893aafcc6be2ea48ecb048672e", 1);} catch (n) {console.log(n);return "";}
}
除了上面說到的扣代碼,再就是直接使用導包的方式來還原(前提上面的分析我們已經知道了它是一個標準的加密算法
),這里我們直接可以使用NodeJS
導出加密模塊的方式實現,實現代碼如下:
const smCrypto = require('sm-crypto');
const { sm2 } = smCrypto;function encrypt(e) {try {const t = encodeURIComponent(JSON.stringify(e));// 密鑰const publicKey = "04064c2a3bcafba2c1ca4f5fb8ecd876b23d70fc4479b78f3c8066c02a8c17749458bca86361bc563d2501b61e2ac93a676a1305893aafcc6be2ea48ecb048672e";return sm2.doEncrypt(t, publicKey, 1);} catch (error) {console.error("加密失敗:", error);return "";}
}
這個密鑰好像是定期會更新的,然后接下來我們驗證一下已經通過逆向分析還原出來的加密算法,對接到單次的請求中是否可以正常拿到接口的響應數據,代碼實現如下:
import execjs
import requests
from loguru import logger
from getuseragent import UserAgentdef encrypt_request_data(data):with open("sm2.js", encoding='utf-8') as f:ctx = execjs.compile(f.read()) res = ctx.call("encrypt",data) return resdef send_request(encrypted_data):random_ua_list = ["chrome", "firefox", "safari"]ua = UserAgent(random.choice(random_ua_list))useragent = ua.Random()url = "https://www.xxx.com.cn/gateway/api/flight/list"headers = headers = {"Content-Type": "application/json","Accept": "application/json, text/plain, */*","Sec-Fetch-Site": "same-origin","Accept-Language": "zh-CN,zh-Hans;q=0.9","Accept-Encoding": "gzip, deflate, br","Sec-Fetch-Mode": "cors","Host": "www.xxx.com.cn","Origin": "https://www.xxx.com.cn","User-Agent": useragent,"Referer": "https://www.xxx.com.cn/flight/oneway/pek-ctu/2025-06-04","Content-Length": "846","Connection": "keep-alive","Sec-Fetch-Dest": "empty","X-Locale": "zh-CN","X-Device-Token": "" # 自行獲取}data = {'params': encrypted_data,'RequestParameterEncryptionIdentificationBit': True}data = json.dumps(data, separators=(',', ':'))response = requests.post(url, headers=headers, data=data)return response.json()if __name__ == "__main__":# 請求明文參數params = {"Trip": [{"Date": "2025-06-05", "Dep": "PEK", "Arrival": "CTU"}],"Passenger": {"adult": 1, "child": 0, "baby": 0},"notchType": None,"aimPrice": None,"RequestParameterSecurityIdentificationBit": True}encrypted_data = encrypt_request_data(params)logger.info(f'加密參數: {encrypted_data}')result = send_request(encrypted_data)logger.info(f'查詢數據: {result}')
運行一下上面封裝好的Python
請求代碼,可以看到是沒有問題的。能夠正常拿到數據,如下所示:
5. 設備指紋風控分析與繞過
上面測試的是非登錄狀態下的情況,其實登不登錄都無所謂。它本身是有一個風控參的,整個爬蟲的風控也都基于它來開展。就是前面我一開始分析提到的X-Device-Token
這個參數就是設備指紋的信息,用來防護惡意請求的。一個X-Device-Token
可以請求的次數在10
次以內,瀏覽器的環境就會被標記再配合風控判定推送行為驗證碼(點選
),如下所示:
所以如果需要多次持續的請求查詢一些數據的話,要么就是直接再逆向分析點選
的協議,拿到驗證的Token
提交也是可以的。再就是解決這個參數繞過點選
。繼續進一步分析這個參數發現是使用的某東設備指紋風控(包括這個點選也是某東云的驗證碼系統
)如下所示:
這個參數收集了【瀏覽器的指紋
、Canvas
、WebGL
、分辨率、Storage...
】以及一些初始化加載行為
針對這個X-Device-Token
參數的對抗方案其實也是有很多種的,第一個就是自己定制魔改,第二個就是使用開源好已經從Chromium
源碼層修改瀏覽器指紋信息的方案(外面的指紋瀏覽器也是可以的
)
這里我們就簡單的用魔改過的方案自己在本地搭一個服務,然后通過注入JS
的方式刷票務的接口Hook
到最新的這個參數,如下所示:
經過多次測試,一個設備指紋的新參數在請求風控出現行為驗證碼
的時候,調用測試搭建的刷新設備指紋的服務獲取新的X-Device-Token
都是可以立即繞過行為驗證正常再次請求數據的(因為我們使用的魔改方案、它會認為我們是一個新的設備及瀏覽器
),如下所示:
它這個不管是自動化瀏覽器的方案還是接口協議的方案都會遇到行為驗證碼的風控(它的行為驗證不像其他平臺是必須要過的、可以規避掉
)。然后像這種非登錄狀態或者游客模式可以訪問的爬蟲方案,廠商針對的風控一般只會從IP
、設備指紋
開展
最后多說一句(爬蟲目前想要持續抓取一些頭部平臺的數據、除了很普遍的很基本的接口驗簽逆向外。需要解決的就是風控,風控涉及的細分領域有很多。而其中最常遇見的就是設備指紋風控跟行為驗證風控,那么對抗是對抗的什么呢?就是這一系列的風控防護,所以定制或魔改指紋、改機這些都是需要涉及的。以前可能只需要考慮策略,但那已經是S3賽季了...
)