聲明 本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關! 本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權, ...
聲明
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯繫作者立即刪除!
逆向目標
- 目標:某驗深知 V2 業務風控逆向分析
- 主頁:
aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9kay12Mi5odG1s
深知簡介
某驗深知通過無感採集客戶端數據,對用戶的環境、標識、行為操作等進行智能化分析,結合業務場景有效識別有潛在風險的用戶。整個識別過程不幹擾用戶,不打斷業務既有流程。完整通訊流程如下:
抓包分析
訪問首頁,會引入一個 v2.sense.js
,後面接了個 id
,需要將其提取出來,後續有用到,當然一般情況下,同一個業務這個 id
應該是一樣的,直接複製下來寫死也行。
接著有個 gettype
的請求,這裡主要返回一些資源路徑,其中有個 gct.xxx.js
,這個 JS 名稱每隔一段時間就會變化,這個 JS 會生成一個鍵值對,例如 {'xnbw': '1158444372'}
,JS 變化,這個鍵值對也會變化,這個鍵值對參與了後面加密參數的生成,在某驗系列產品中都有這個東西,少量測試將其固定發現也可以通過驗證,盲猜大量請求或者某些校驗嚴格的網站可能有影響,建議還是動態去請求這個 JS 來獲取最新的鍵值對,這個後文具體再說。
然後是 judge
的請求,這個請求頁面一載入就完成了,不需要手動點擊請求,其中 Query String Parameters
里有個 app_id
就是我們前面提到的 id
,Request Payload
就是一串超長的字元串,這個也是我們需要逆向的參數。該請求如果驗證成功,會返回一個 session_id
。
然後就是業務介面了,本例中業務介面是 verify-dk-v2
,也就是一個登錄介面,帶上前面 judge
介面返回的 session_id
即可請求成功。
逆向分析
由於我們逆向的參數 Request Payload
沒有鍵名導致不能直接搜索關鍵字,所以只能跟棧或者下個 XHR 斷點,跟棧可以在 sense.2.3.0.js
第 6144 行找到一個 e + h[AUJ_(1173)]
,這個就是正確的 Request Payload
值。
上圖中其實核心代碼就四行,後文也是圍繞這四行代碼來分析的:
var h = o[AUJ_(1156)]()
, e = CoUE[ymDv(24)](NFeB)
, l = EbF_[ymDv(409)](e, h[ymDv(1194)])
, e = DWYi[ymDv(1137)](l)
獲取 h 值
先來看 h 的值,由一個方法生成一個對象,對象裡面分別是 aeskey
和 rsa
,每次也都是隨機變化的。
繼續跟到這個方法里,重點在於 e 和 t 的值,最後返回的就是 {aeskey: e, rsa: t}
。
先看這個 e 的值,也就是 RwyT()
方法,搞過某驗其他產品的就知道這裡是 16 位隨機值。
然後 t 的值,和某驗其他系列產品一樣,用到了 RSA 加密演算法,這裡圖中 BPqG()
就是 RSA 演算法,t 的值就是 RSA 加密後的結果,扣的時候註意找到演算法開頭的地方,將整個 BPqG()
方法扣下來即可。
獲取 e 值
接下來是 e 的值,e = CoUE[ymDv(24)](NFeB)
,很明顯是將 NFeB
的值進行了處理,NFeB
是個對象,裡面有一些 data
、id
等信息,如下圖所示:
所以我們得先找一下 NFeB
這個值是怎麼來的,直接搜索發現只有四個地方,在第 6109 行就是定義的地方,挨個看,首先有個 s 參數,將 id 傳入到一個函數進行處理,函數沒啥特別的,直接扣就行,通常經過處理後,s 的值為空,即 s=""
。
再來看有個 u 值,由一個方法生成了一大串包含很多感嘆號的字元串,本案例實際測試中,直接將這個值置空也行,可能其他校驗嚴格或者大批量請求的情況下,說不定也會校驗的,所以我們最好也跟進去找一下生成邏輯。
跟進這個方法,裡面是一些瀏覽器環境的值,比如屏幕高寬、canvas、ua、瀏覽器插件、時間、時區、語言等等,基本上都能寫死,後續會將這些值以 !!
相連接最終生成 u 的值。
然後繼續看,接下來是 c 值,是一個對象,值為 {"key":0,"value":[]}
,我這裡直接寫死了。
再往下就是 NFeB
了:
Unicode 轉換一下,簡單解一下混淆,就長下麵這樣:
NFeB = {
"id": a["id"],
"page_id": a["page_id"],
"lang": a["lang"] || AUJ_(31),
"data": {
"insights": u || null,
"track_key": c["value"] ? c["key"] : null,
"track": c["value"] || null,
"ep": o["KZrg"](i),
"eco": window["GEERANDOMTOKEN"] || "",
"ww3": ""
}
};
id
不用說,page_id
就是個時間戳,lang
中文就是 zh-cn
,insights
是前面得到的 u
值,track_key
、track
取 c
的鍵和值,ep
將 i
傳入了一個函數進行處理,i
是固定的字元串 client
,這個 KZrg
方法可以跟進去看看,裡面其實有很多都是定值,唯一需要註意的是 t["tm"]
這個值,和某驗其他系列一樣,是 window.performance.timing
的值,自己獲取一下時間戳隨機加減偽造一下就行了。
然後就是 eco
的值,取的 window.GEERANDOMTOKEN
,列印一下 window,除了有這個 token 以外,還可以看到 localStore
、session
裡面也有這個值。
由於某驗的 JS 都是混淆後的,不太好定位這個值生成的地方,所以拿出我們的 Hook 大法,先清除一下緩存,不然的話是 Hook 不到值的,Hook 代碼如下:
(function() {
var token = "";
Object.defineProperty(window, 'GEERANDOMTOKEN', {
set: function(val) {
console.log('GEERANDOMTOKEN->', val);
debugger;
token = val;
return val;
},
get: function()
{
return token;
}
});
})();
斷下後往前跟棧,window[o] = t
,o
就是 GEERANDOMTOKEN
,t
就是我們想要的值。
往上就可以找到 t
的生成方法,核心就是生成一個 32 位的隨機字元串,然後加上時間戳,再進行 MD5 加密得到最終值,生成位置以及實現的代碼如下:
var MD5 = require("md5")
function getToken(){
var t = MD5(function(e) {
for (var t = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"], n = "", r = 0; r < e; r++)
n += t[parseInt(61 * Math.random(), 10)];
return n;
}(32) + new Date().getTime());
return t;
}
當你把以上這些參數都搞完了,你可能認為都齊了,其實不然,後面接著還有一句 Yvwp(NFeB, r)
,將 r 的值增加到了 NFeB
里,這個 r 的值類似於 {olbo: "1588069361"}
,這個鍵值對都是每隔一段時間會變的,這個在某驗系列其他文章里也提過。
進一步分析,這個 r 是傳進來的,所以往上跟棧,有個 r[psPG(1183)]()
方法就生成了這個對象:
繼續跟到這個方法里去,首先定義了 e 這個對象,然後賦值 e = {ep: "test data", lang: "zh"}
,然後經過 window[tYlM(1126)]()
方法處理後,e 裡面就新增了 {olbo: "1588069361"}
,後續將 ep 和 lang 兩個值刪除後返回。
所以我們繼續跟進 window[tYlM(1126)]()
方法,會跳轉到 gct.xxxx.js 里,這個 JS 就是我們開頭講過的,他的名稱會每隔一段時間變化,內容也會變,所以導致生成的鍵值對也會變化,繼續跟,有個 t[e] = xxx
的語句,其中 e 和等號右邊的值,就是我們需要的鍵值對。
這個鍵值對在我們本地也可以動態獲取,只需要請求正確的 JS 文件,將要調用的方法全局導出就行了,以下給一個我的處理方法示例(註意裡面請求 url 已經脫敏處理,所以不可直接運行,自行抓包補上):
import re
import time
import json
import execjs
import requests
from loguru import logger
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
}
def get_gct():
url = "https://dkapi.脫敏處理.com/deepknow/v2/gettype"
params = {
"callback": "脫敏處理_" + str(int(time.time() * 1000))
}
response = requests.get(url, headers=headers, params=params).text
response = json.loads(re.findall(r"geetest_\d+\((.*?)\)", response)[0])
# gettype 介面返回的 gct.xxx.js 的地址
gct_path = "https://static.脫敏處理.com" + response["gct_path"]
logger.info("gct_path: %s" % gct_path)
gct_js = requests.get(gct_path, headers=headers).text
# 正則匹配需要調用的方法名稱
function_name = re.findall(r"\)\)\{return (.*?)\(", gct_js)[0]
# 查找需要插入全局導出代碼的位置
break_position = gct_js.find("return function(t){")
# window.gct 全局導出方法
gct_js_new = gct_js[:break_position] + "window.gct=" + function_name + ";" + gct_js[break_position:]
# 添加自定義方法調用 window.gct 獲取鍵值對
gct_js_new = "window = global;" + gct_js_new + """
function getGct(){
var e = {"lang": "zh", "ep": "test data"};
window.gct(e);
delete e["lang"];
delete e["ep"];
return e;
}"""
gct = execjs.compile(gct_js_new).call("getGct")
logger.info("gct: %s" % gct)
return gct
到這裡我們 NFeB
就生成完畢了,回到 e
的值,這裡其實就是把 NFeB
轉成字元串,直接 JSON.stringify()
即可。
獲取 l 值
l 的值比較簡單,就是將前面生成的 h["aeskey"]
作為 key,e
作為待加密字元串,經過 AES 加密後即可得到 l 的值。
本地復現如下(有些變數名稱不一樣無影響,我是直接復用的某驗其他產品的方法):
var CryptoJS = require("crypto-js")
function aesEncrypt(e, i) {
var key = CryptoJS.enc.Utf8.parse(i),
iv = CryptoJS.enc.Utf8.parse("0000000000000000"),
srcs = CryptoJS.enc.Utf8.parse(e),
encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
for (var r = encrypted, o = r.ciphertext.words, i = r.ciphertext.sigBytes, s = [], a = 0; a < i; a++) {
var c = o[a >>> 2] >>> 24 - a % 4 * 8 & 255;
s.push(c);
}
return s;
}
進一步處理 l
最後一步 e = DWYi[ymDv(1137)](l)
,將 l 的值經過了 tc_t
這個方法進行處理,就會得到最終 Request Payload
的一部分。
跟進這個 tc_t
方法,又是熟悉的 return e["res"] + e["end"]
,同樣和某驗其他產品一樣的。
跟到處理 e 的這個方法里,最後返回的是 {"res": a, "end": s}
,沒啥特別的,直接扣即可,這裡註意和某驗其他產品里的方法有些小區別,裡面有些常量的值是不一樣的,最開始我直接復用了其他產品的方法,發現結果是錯的。
自此整個流程分析完畢,最終 e + h[AUJ_(1173)]
的值與 Request Payload
的值一致。