关于python:验证码逆向专栏某验深知-V2-业务风控逆向分析

51次阅读

共计 5624 个字符,预计需要花费 15 分钟才能阅读完成。

申明

本文章中所有内容仅供学习交换应用,不用于其余任何目标,不提供残缺代码,抓包内容、敏感网址、数据接口等均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关!

本文章未经许可禁止转载,禁止任何批改后二次流传,擅自应用本文解说的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K 哥爬虫】分割作者立刻删除!

逆向指标

  • 指标:某验深知 V2 业务风控逆向剖析
  • 主页:aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vZGVtby9kay12Mi5odG1s

深知简介

某验深知通过无感采集客户端数据,对用户的环境、标识、行为操作等进行智能化剖析,联合业务场景无效辨认有潜在危险的用户。整个辨认过程不烦扰用户,不打断业务既有流程。残缺通信流程如下:

抓包剖析

拜访首页,会引入一个 v2.sense.js,前面接了个 id,须要将其提取进去,后续有用到,当然个别状况下,同一个业务这个 id 应该是一样的,间接复制下来写死也行。

接着有个 gettype 的申请,这里次要返回一些资源门路,其中有个 gct.xxx.js,这个 JS 名称每隔一段时间就会变动,这个 JS 会生成一个键值对,例如 {'xnbw': '1158444372'},JS 变动,这个键值对也会变动,这个键值对参加了前面加密参数的生成,在某验系列产品中都有这个货色,大量测试将其固定发现也能够通过验证,盲猜大量申请或者某些校验严格的网站可能有影响,倡议还是动静去申请这个 JS 来获取最新的键值对,这个后文具体再说。

而后是 judge 的申请,这个申请页面一加载就实现了,不须要手动点击申请,其中 Query String Parameters 里有个 app_id 就是咱们后面提到的 idRequest 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 的值,由一个办法生成一个对象,对象外面别离是 aeskeyrsa,每次也都是随机变动的。

持续跟到这个办法里,重点在于 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 是个对象,外面有一些 dataid 等信息,如下图所示:

所以咱们得先找一下 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-cninsights 是后面失去的 u 值,track_keytrackc 的键和值,epi 传入了一个函数进行解决,i 是固定的字符串 client,这个 KZrg 办法能够跟进去看看,外面其实有很多都是定值,惟一须要留神的是 t["tm"] 这个值,和某验其余系列一样,是 window.performance.timing 的值,本人获取一下工夫戳随机加减伪造一下就行了。

而后就是 eco 的值,取的 window.GEERANDOMTOKEN,打印一下 window,除了有这个 token 以外,还能够看到 localStoresession 外面也有这个值。

因为某验的 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] = to 就是 GEERANDOMTOKENt 就是咱们想要的值。

往上就能够找到 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 的值统一。

后果验证

正文完
 0