2

顶象验证码破解与研究

 3 years ago
source link: https://www.daqianduan.com/17478.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

声明

原创文章,请勿转载!

1、本文内容仅限于安全研究,不公开具体源码。维护网络安全,人人有责。

2、如果想更系统地了解第三代验证码、更全面体会安全产品的设计,可以结合我的另几篇篇文章

(1)《第三代验证码研究》 https://www.cnblogs.com/boycelee/p/11363611.html(推荐)

(2)《顶象验证码破解与研究》

(3)《极验验证码破解与研究》 https://www.cnblogs.com/boycelee/p/14021048.html(推荐)

(4)《极验无感验证破解》 https://www.cnblogs.com/boycelee/p/13951819.html

(5)《同盾小程序指纹破解》 https://www.cnblogs.com/boycelee/p/13899956.html

3、本文主要通过破解协议的方式绕过顶象安全验证,思路与网上自动化的方式有很大的不同

一、个人心路历程

(1)顶象的验证码真的非常棒。在我从2018年12月开始到2019年5月持续与顶象验证码对抗半年。

(2)我猜在对抗期间顶象很多的防护策略都是被我逼出来的吧(哈哈哈,个人yy)

(3)js混淆与加密算法一天更换两次、滑块验证码干扰槽等应该都是在于我对抗中升级的。

(4)一年后回头看顶象的验证码产品又多了语序点选、刮刮卡验证、空间语义验证、乱序拼图验证、旋转验证、面积验证等验证码类型,在创新方面做得非常好,应该是国内数一数二的。

(5)与顶象的对抗,使我对安全产品有了更深刻、全面的认识。

(6)以下是我与顶象的对抗过程。

2Ez2M3.png

二、完整流程

  • 访问顶象官网,注册账号后登录控制台,访问“无感验证”模块,申请开通后系统会分配一个唯一的AppId、AppSecret。

  • 当用户滑动验证码通过后,验证码服务会生成一个 token ,用户的业务请求带上这个验证码 token ,业务系统再调用后台 SDK 验证 token 的有效性。

  • 顶象请求链路如下:

    YfUfu2.png

三、实例研究

1、研究目标

当用户滑动验证码通过后,验证码服务就会生成token,用户携带token上传至顶象服务进行验证。我们的目标就是生成没有被标记为异常的token。

NjYFn2.png

2、案例历史研究

2018年12月研究的是国航的手机注册场景。不过由于国航该场景的验证码已经不再是滑块,所以后续可能需要使用官网案例来具体分析。

7vQJ3u.png

3、官网案例分析

miUjye.png

从请求中我们可以看出,一共是三个请求(c1、a、v1)。我们需要弄清楚这三个请求分别的含义。(从设计角度个人觉得顶象相对于极验要简洁一些。个人觉得顶象的设计非常棒,在做公司的安全产品时,我也会参考一些顶象的设计)

四、请求分析

1、c1请求:请求顶象服务器,获取c参数。

目的:此处应该是计算设备指纹,通过设备指纹能够标识唯一设备信息

(1)请求参数示例 :

428#X8m8K4SIcMDkTOm/r2P3j5vTyaRhmW3m9aksXQ8m2r13DgOarX1TmbmTMX13/vXXC9RmXS8m2XHfDg/uV8RP/4uaMuk8/WnUNX+6Xnah2rDfmZ3YXXwR0gpt2lJu07Nozs5jXY433RaCfCO6Y8VX/6aVU2mT6cuhX14q+2v8jcQ/awvNF8X1YuuJmzf3XXbXmCvq8M8uiwXSm3vhmyyuXXOiXmo8Ya21XXGjS8ft1qT2+Al3+Ab0wXWXmyf8aCO4WrrXj6flFh9p4PnSFPokuP5vXmuFn9fM8Am+JXvlT2TiW8QL88SXjtZykz8LUtZ9kAQpkd4pn2oXmpP9upuLVr2XvVTUjyTPTXWPCy+oTofzwi2oRX41sYk1sXWARiw1w32AOrfAe1fPTXVXivd2CFHaGgNezipHxx6cciF1xORN2iUklOpQzldUBvAxwBHXSoHBAGyUXXJ99ei8lxEoj2X8sJTvz/novxbofltai6k3wmXXjtyTItyoi8QsjUMIGI0siT3XYWKG9FkI54DQdTnXm00umH0KBTIXmACOT1rFmC8XmA2rTPuVmm/Xmg1R9usmM5/6XX+vqnlSylnDXX+jGnElyi3fXXphvMw444YeNUpw0TyXuFi3HFitgAiquUM3HXtquUM3HXwt1rXSuNMs1MJfuNrniBCkEnrfYmCsYuL5Y5Cf+nCkUFCkYdCSUFrSJXonuMoXZkrpk8nf3Ue/P9F0pmjG6Q9/DgBJzVZqcg6po/NleM6PXXVE8F9oSzdt++YgMiFBYxWbOUaHPXXS+2C5jY5/ZcmEaAvR+cxxjRQ+k8CoD1xHFcxSj6ovU270Dwyik9oBPTVXYNS86LTmha3t68oQPrXs3/ZLJMPTI2QLAtomXX07drAmu4omycphXrnXYbUG9GpOSg6yts0JX2XfTXVqvE26Y/COD9OqWA5IPaQTDVxc41r6mXXn8Xo6JyIPJPyuXYQim8OYJX3tu6ci/TX3muOg/Lm8//nuY9nljN/sX3vTXX4qX1nIa//1Yc9x

(2)请求参数信息:

只展示部分数据

"supportAddBehavior": "ab","adblock": "adb","availResolution": "ar","canvasFP": "can","cpuClass": "cc","colorDepth": "cd","cookieEnabled": "ce","canPlayType": "cpt","collectTime": "ct","doNotTrack": "dnt","deviceMemory": "dm","languages": "lugs","mimeTypes": "mts","mediaDevices": "mds","platform": "np","supportOpenDatabase": "od","devicePixelRatio": "pr","resolution": "res","plugins": "rp","supportSessionStorage": "ss","timezoneOffset": "to","touch": "ts","userAgent": "ua","webgl": "web","webgl2": "gi"

通过加密以上操作系统与浏览器以及网络数据等信息生成c1请求参数param。

(3)加密算法

Y7JB7n.png

index.js?_t=xxxx中,进行每日更新。那么该如何确保每日都能获取到准确的加密算法呢?通过解析原语法树,生成我们想要的语法树。(这个后续讲)

(4)返回参数

{"data":"f8839e00435f2e05f9ed60b3d3c5498554cb367655ec6e7318adefda150437040a74963c","msg":"lid invalid","status":-4} // 会根据指纹情况对指纹进行风险等级判断

(5)指纹本地存储

顶象会将指纹进行多地存储,例如cookie、Session Storage、Local Storage等

2、a请求:获取图片与token2

2Y3uem.png

(1)请求参数示例:

de=0&wp=1&aid=dx-1547996895410-3601284-1&jsv=1.3.11.98&c=5c3c65a6uSNGXiEhdEwxwwICxBq5Qdjdky4kxjo1&ak=5f6727ec854786a86cd4c3c171d13499&s=50&h=150&w=300&_r=0.9866721061865382
  • wp:表示图片类型。非常重要,因为通过该参数选择图片类型,省去了我们取转换webp格式图片(0表示jpg,1表示webp。目前只有chrome支持,safari不支持webp格式)。
  • aid:时间戳+随机数+1。目前猜测其目的是number once。
  • jsv:表示版本号。
  • c:c1请求返回参数,可以理解为第一阶段token1。
  • ak:appKey,顶象会为每一个接入其无感验证码的server提供一个appKey,以便标识他们是哪一个服务。
  • s、h、w:描述图片的长宽等信息。h与w非常重要,因为获取的图片信息是200*400,所以我们在计算距离时,应该按比率缩小,否则滑块无法通过。
  • _r:number once。

(2)返回参数:

{"sid":"86ae78ed0fba04f8384f3c9376271b0d","y":30,"success":true,"p1":"/dx/ib3oV3MeuO/zib3/b5cce61f91c6447bbc2f10e7836a7827.webp","p2":"/dx/ib3oV3MeuO/zib3/e34db2ab70064b2c998d14159f0b8ff8.webp","p3":"/dx/ib3oV3MeuO/zib3/50969dc0b2f14d118fcf166dc0871021.webp","msg":null,"t":null,"result":1,"type":0,"logo":null}
  • sid:token2。
  • y:y坐标
  • p1:不完整图片
  • p2:拼图块
  • p3:完整图片

注意:如果该请求错误,可能会返回另一种图片验证码(点击类型)

3、v1请求:获取token3

(1)请求示例:

y: 30 x: 33 aid: dx-1547997910506-78792589-1 sid: 75dc20f81b2bd0ff871b9f12e54ef2c8 jsv: 1.3.11.98 c: 5c448ee09pUgUAwgiaRMmhhDea79K4O1B7oQhRh1 ak: 5f6727ec854786a86cd4c3c171d13499 ac: 492#X8Xn8AQv/Y6pdvgYXXfOuMffR/W3XjVgMOYfQntkaQbRZ/smuRlFpvD6L+qHM21JPdOjXTgzLGKauxS0c29jXCAeYaTRl589z9Ug+vZ1YDXIBNwrm8X1k6vEf3rKmrXmS2WXmFPMgH3nXX8XXXXmY8XEJ37H/cWc68WnDdW+DXSjXtXZj9c6v33n6aa6W9bhYu/c8XIXui83m5U1gFUJzxqJzxoGUqNSYXpFY2Xio3O5Ja/dw/NP2X5X3oFjcWf2r9znqaJQ2LQr7homIr5X3oFPjnXMr9znqaJQ2LQr7homIr2XsBHwJhSSRygCnh3ufTc1v/8V8YZsiz/D4raoj9XLRtOYv9a53D2kZcWV8YcHUAa/mrXmS25X3oFPjvS2r9znqaJQ2LQr7homIr5XjoFPjvrMr9DIGaUiwYmYYrXslqs+7bRz2kEuonW=
  • y:坐标,a请求会返回,所以不需要自己计算。

  • x:坐标,需要计算。

  • aid:时间戳+随机数+1。目前猜测其目的是number once。

  • jsv:表示版本号。

  • c:c1请求的返回值,理解为token1

  • ak:appKey,顶象会为每一个接入其无感验证码的server提供一个appKey,以便标识他们是哪一个服务。

  • ac:加密信息。包括时间戳、操作系统对应编码、Referer、特殊加密算法(随机取加密算法中一段并加密)、JSV、token(sid)、是否使用headless、浏览器版本、屏幕信息、服务网址、版本、检测是否使用webdriver等方式抓取、token、鼠标轨迹等进行数据加密。

(2)返回示例:

{"success":true,"token":"37AD877DC3953CA1116E487DFBF6DB435DFE7C4CD204B8DC861D5857BE2FDDCF4565648C95BAFD15030AA555713B5DA1FD5D57C82427F4C7D222E5990F47AB83B3F13ED08965032CFF26FA37B82012FF","msg":null,"tp":null,"sv":null,"retry":0}

返回token则表示成功。但注意此时token未必有效,如果c1请求使用appKey与v1请求使用的不同,是无法通过check校验的。

4、check

在获取token之后会通过check接口,进行token时效性与正确性校验。使用方通过调用顶象的open api进行token校验。通过则说明,验证码完成。

五、部分服务化源码

greenseer.js部分服务化代码

function getParamUa(sid, position, referer, browserVersion) {
    var _ua = "";
    var ua = "";
    var tm = new Date().getTime();
    delete require.cache[require.resolve('../builder/dingxiang_greenseer_finish.js')];
    var encryptionFunc = require('../builder/dingxiang_greenseer_finish.js');
    var replace_encrypt = encryptionFunc.replace_encrypt;

    delete require.cache[require.resolve('../builder/dingxiang_version_finish.js')];
    var versionFunc = require('../builder/dingxiang_version_finish.js');
    var replace_version = versionFunc.replace_encrypt;
    let mouseMove = buildMouseMove(position);
    let mouseMoveEvent = buildMouseMoveEvent(mouseMove);
    let mouseDownEvent = buildMouseDownEvent(mouseMoveEvent);
    app = function (u, c) {
        var s = (0, toStr)([u].concat((0, replace_bs2)(c.length)));
        _ua += [s, c].join(""),
            ua = [replace_version.exports.version, "#", (0, replace_btoa)(_ua)].join("");
        console.log(ua);
        ua.replace("xxxxxxxxx", "");
    }
    process = function (t) {
        var c = [].slice.call(arguments);
        return t = c.length === 1 && (0, isArray)(t) ? t : c, t = (0, flatten)(t), (0, toStr)(t)
    }
    getTM = function () {
        var a = process((0, replace_bs8)(tm));
        app(1, (0, replace_encrypt.encryptTM)(a))
    }
    //浏览器版本
    getBR = function (browserVersion) {
        //浏览器版本 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
        //Chrome/73.0.3683.86 ,chromeVersion = "73"
        //var version = browserVersion;
        var version = browserVersion;
        console.log("version: " + version)
        var a = 3, b = [1, version], c = b[1];
        a = process(a, b[0], (0, replace_bs2)(c.length), (0, replace_bss)(c));
        app(2, (0, replace_encrypt.encryptBR)(a))
    }
    //屏幕信息
    getSC = function () {
        //7,128,4,56,7,128,4,33,0,241,4,56,7,128,1,73,7,128,4,56
        //var n = this.process((0, getScreenInfo)());
        //var n = "8! ´:8";
        //var scValue = [[5,160],[3,132],[5,160],[3,109],[0,0],[0,0],[5,160],[1,49],[5,160],[3,132]];
        var scValue = buildSCArray();
        //var scValue = [[7,128],[4,56],[7,128],[4,33],[0,241],[4,56],[7,128],[2,5],[7,128],[0,0]];
        var n = this.process(scValue);
        app(3, (0, replace_encrypt.encryptSC)(n))
    }
    //网址
    getLO = function (referer) {
        var o = "",
            a = referer,
            i = this.process((0, replace_bs2)(a.length), (0, replace_bss)(a), (0, replace_bs2)(0), (0, replace_bss)(o));
        app(4, (0, replace_encrypt.encryptLO)(i))
    }
    //函数
    getCF = function () {
        let cfParam = buildCFParam();
        let range = Math.floor(Math.random() * 3 + 7);
        let lastIndex = Math.floor(Math.random() * (cfParam.length - 9) + 9);
        let beginIndex = lastIndex - range;
        cfParam = cfParam.slice(beginIndex, lastIndex);
        var s = this.process((0, replace_bs2)(beginIndex), (0, replace_bs2)(range), (0, replace_bss)(cfParam));
        //var s = " += t";
        app(5, (0, replace_encrypt.encryptCF)(s))
    }
    //是否开启控制台
    getDI = function () {
        var a = 1;
        a = process(a);
        app(6, (0, replace_encrypt.encryptDI)(a))
    }
    //检测是否使用webdriver等方式抓取
    getEM = function () {
        var s = process((0, replace_bs4)(0));
        app(7, (0, replace_encrypt.encryptEM)(s))
    }
    //版本
    getJSV = function () {
        var o = process((0, replace_bs4)(1));
        app(8, (0, replace_encrypt.encryptJSV)(o))
    }
    //token
    getTK = function (sid) {
        var a = sid;
        a && (a = this.process((0, replace_bs2)(a.length), (0, replace_bss)(a)),
            app(9, (0, replace_encrypt.encryptTK)(a)))
    }
    getMM = function (mouseMove) {
        var v = mouseMove.eventName
            , h = mouseMove.tm
            , d = mouseMove.x
            , p = mouseMove.y
            , l = this.process((0, replace_bs4)(h)
            , (0, replace_bs2)(d)
            , (0, replace_bs2)(p)
            , (0, replace_bs2)(v.length)
            , (0, replace_bss)(v));
        console.log("mouse_mm info :" + "MM_v:" + v + " tm:" + h + " MM_getPageX:" + d + " MM_getPageY:" + p);
        this.app(11, (0, replace_encrypt.encryptMM)(l))
    }
    getMD = function (mouseDown) {
        var f = mouseDown.eventName
            , s = mouseDown.button
            , v = mouseDown.tm
            , h = mouseDown.x
            , d = mouseDown.y
            , p = this.process((0, replace_bs4)(v)
            , (0, replace_bs2)(h)
            , (0, replace_bs2)(d)
            , s
            , (0, replace_bs2)(f.length)
            , (0, replace_bss)(f));
        console.log("MD_getTarget:" + f + " MD_getButton:" + s + " MD_time:" + v + " MD_getPageX:" + h + " MD_getPageY:" + d + " MD_process:" + p);
        this.app(12, (0, replace_encrypt.encryptMD)(p))
    }
    var _sa = [];
    recordSA = function (mouseMove) {
        var i = mouseMove.tm
            , u = mouseMove.x
            , c = mouseMove.y
            , f = this.process((0, replace_bs4)(i),
            (0, replace_bs2)(u),
            (0, replace_bs2)(c));
        console.log("recordSA: " + " tm: " + i + " getPageX: " + u + " getPageY: " + c);
        _sa.push((0, replace_encrypt.encryptSA)(f))
    }
    sendSA = function (r) {
        this.app(17, r);
    }
    sendTemp = function (t) {
        var n = process((0, replace_bs2)(t.length), (0, replace_bss)(t));
        app(10, (0, replace_encrypt.encryptTEMP)(n))
    }

    var tm = getTM();
    var br = getBR(browserVersion);
    var LO = getLO(referer);
    var CF = getCF();
    var DI = getDI();
    var EM = getEM();
    var JSV = getJSV();
    var TK = getTK(sid);
    var SC = getSC();
    var MM = getMM(mouseMoveEvent[mouseMoveEvent.length - 1]);
    var MD = getMD(mouseDownEvent[0]);
    var DI = getDI();
    for (let mveIndex = mouseMoveEvent.length - 2; mveIndex >= 0; mveIndex--) {
        var MM = getMM(mouseMoveEvent[mveIndex]);
    }
    for (let mvIndex = mouseMove.length - 1; mvIndex >= 0; mvIndex--) {
        var SA1 = recordSA(mouseMove[mvIndex]);
    }
    for (let saIndex = 0; saIndex < _sa.length; saIndex++) {
        var SA2 = sendSA(_sa[saIndex]);
    }
    var temp = sendTemp(position);
    return ua;
}

Java服务化代码

@Override
    public CrackResult crackDingXiang(DingXiangParam dingXiangParam, Integer captchaType, String caller) throws IOException {
        Preconditions.checkNotNull(dingXiangParam, "crackDingXiang dingXiangParam is null");
        Preconditions.checkNotNull(StringUtils.isNotBlank(caller), "caller is null");

        String userAgent = HeaderBuilder.buildChrome();
        String browserVersion = getBrowserVersion(userAgent);
        dingXiangParam.setUserAgent(userAgent);
        //Lid请求
        String lidResult = getDingXiangLidFunc(dingXiangParam);
        dingXiangParam = changeDingXiangParam(dingXiangParam, lidResult);
        //C请求
        String cFunctionResult = getDingxiangCFunc(dingXiangParam);
        CFunctionResponse cFunctionResponse = buildCFunctionResponse(cFunctionResult);
        AFunctionParam aFunctionParam = buildPictureRequestParam(cFunctionResponse, dingXiangParam);
        //A请求
        String aFunctionResult = getDingXiangAFunc(aFunctionParam);
        AFunctionResponse aFunctionResponse = buildAFunctionResponse(aFunctionResult);
        //分析图片
        PictureAnalysisParam pictureAnalysisParam = buildPictureAnalysisParam(dingXiangParam, aFunctionResponse, browserVersion);
        String pictureAnalysisResult = analysePicture(pictureAnalysisParam);
        PictureAnalysisResponse pictureAnalysisResponse = buildPictureAnalysisResponse(pictureAnalysisResult, pictureAnalysisParam);
        //V请求
        VFunctionParam vFunctionParam = buildVFunctionParam(pictureAnalysisResponse, aFunctionParam, aFunctionResponse, dingXiangParam);
        List<NameValuePair> vFunctionPostParam = buildDingXiangVFuncPostParam(vFunctionParam);
        String vFunctionResult = getDingXiangVFunc(vFunctionPostParam, dingXiangParam);
        VFunctionResponse vFunctionResponse = buildDingXiangVFuncResponse(vFunctionResult);
        CrackResult crackResult = buildCrackResult(vFunctionResponse, cFunctionResponse, dingXiangParam, captchaType);

        return crackResult;
    }

六、服务化关键

1、图片还原

vUVb2a.png

2、随机加密算法解析(重点设计部分)

加密算法会随着js混淆的改变而改变,如何服务化?

UFzAbi.png

(1)语法树思路

6FvEfu.png

(2)举例子

(1)未处理的index.js。

BjiAvm.png

(2)创建模板并生成其语法树。

AvAFNv.png

(3)将加密算法转化成语法树-> 与模板语法树结合 -> 生成新的语法树 -> 生成新的js文件

FV3aue.png

(4)启用定时任务,每天固定时间去生成新的加密算法js文件。

七、成果

RFFrAn.png

八、顶象无感验证流程

(1)顶象无感验证流程图

UbARFr.png

(2)顶象风控流程图

6NbIVb.png

九、总结

(1)顶象相对于极验产品设计更加清晰。步骤非常明确 “设备数据->行为数据->验证”;

(2)通过每日更新混淆js文件、加密算法提升破解服务化难度;

(3)通过验证码下发策略,进行验证码种类变换,提升验证码识别难度;

(4)验证码产品创新能力强;

(5)整体而言,顶象对于验证码的理解应该是业界领先的。

十、最后

1、目前国内在安全产品方面的文章较少,我们很难全面了解与学习安全产品,希望我的文章能够帮助到更多人。

2、如果想系统地了解第三代验证码,可以结合我的另几篇篇文章

(1)《第三代验证码研究》 https://www.cnblogs.com/boycelee/p/11363611.html(推荐)

(2)《极验验证码破解与研究》 https://www.cnblogs.com/boycelee/p/14021048.html(推荐)

(3)《极验无感验证破解》 https://www.cnblogs.com/boycelee/p/13951819.html

(4)《同盾小程序指纹破解》 https://www.cnblogs.com/boycelee/category/1819211.html

3、本文不提供完整解决方案和完整数据,仅用于理论研究,维护网络安全,人人有责。

#感谢您访问本站#
#本文转载自互联网,若侵权,请联系删除,谢谢!657271#qq.com#

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK