本文要分享的内容是去年为了抢鞋而分析 极验(GeeTest)反爬虫防护的笔记,由于篇幅较长(为了多混点CB)我会按照我的分析顺序,分成如下四个主题与大家分享:

  1. 极验反爬虫防护分析之交互流程分析
  2. 极验反爬虫防护分析之接口交互的解密方法
  3. 极验反爬虫防护分析之接口交互的解密方法补遗
  4. 极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

本文是第四篇, 也是最后一篇,网上大部分针对极验的绕过方法大都是模拟手工滑动滑块的方式,但是通过上面几篇文章的分析,我们是能知道Geetest已经对目前市面上大多自动化测试的工具进行了监测,包括 Selenium甚至electron等。所以基于这些工具的破解不是不行,只是人家官方没有严查,不长久的,稳妥之计还是要直接从封包入手。下面进入正文~


背景图片乱序的还原

如《极验反爬虫防护分析之交互流程分析》第五步的分析,得到的 bgfullbg图片都是乱序处理后的图片,要判断滑动的距离及轨迹需要将图片进行还原。如下图:

极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

1.jpg

还原后的代码为:

function SEQUENCE() {        var e = "6_11_7_10_4_12_3_1_0_5_2_9_8".split("_");    for (var t, n = [], r = 0; r < 52; r++) {            t = 2 * parseInt(e[parseInt(r % 26 / 2)]) + r % 2;            parseInt(r / 2) % 2 || (t += r % 2 ? -1 : 1);            t += r < 26 ? 26 : 0;            n["push"](t);    }    return n;}var result = SEQUENCE();console.log(result.join(", "));

至此,我们知道它是通过两次折叠构建出来52个元素的散列表。通过固定的公式将图片上下、左右互换并根据散列表的值进行乱序。通过分析代码中的字符串常亮6_11_7_10_4_12_3_1_0_5_2_9_8是在slide.7.6.0.js文件中,一开始的方法中定义的:$_DAEAF = decodeURI('N-%60%13)nN-%60%1C%1...,decodeURI解码后的数组第911位就是此字符串常量, 如下图:
极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

2.jpg

继续跟进绘图的代码:

极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

3.jpg

将混淆的代码还原之后,如下:

function $_GEN(t, e) {    var $_CJDIX = $_AB.$_Ei()[4][26];    for (; $_CJDIX !== $_AB.$_Ei()[8][24];) {        switch ($_CJDIX) {            case $_AB.$_Ei()[16][26]:                t = t[$_DEAo(65)], e = e[$_DDJm(65)];                var n = t["width"], r = t["height"], i = document[$_DDJm(27)]($_DEAo(91));                i["width"] = n, i["height"] = r;                var CanvasRenderingContext2D = i["getContext"]("2d");                $_CJDIX = $_AB.$_Ei()[8][25];                break;            case $_AB.$_Ei()[8][25]:                CanvasRenderingContext2D["drawImage"](t, 0, 0);                var CanvasRenderingContext2D = e["getContext"]("2d");                e["height"] = r, e["width"] = WIDTH;                for (var a = r / 2, u = 0; u < 52; u += 1) {                    var c = SEQUENCE % 26 * 12 + 1, _ = 25 < SEQUENCE ? a : 0,                        l = CanvasRenderingContext2D["getImageData"](c, _, 10, a);                    CanvasRenderingContext2D["putImageData"](l, u % 26 * 10, 25 < u ? a : 0);                }                $_CJDIX = $_AB.$_Ei()[0][24];                break;        }    }}

将以上JS编写为还原图片的Python代码如下:

import numpy as npfrom PIL import Image import matplotlib.pyplot as pltdef sequence():    t = 0    n = []    e = "6_11_7_10_4_12_3_1_0_5_2_9_8".split("_")    for r in range(0, 52):        t = 2 * int(e[int(r%26/2)]) + r % 2        if 0 == int(r/2)%2:            t += -1 if (r%2) else 1        t += 26 if (r<26) else 0        n.append(t)    return ndef gen(_seq, _img):    """    用于将图片还原    @param _seq: 图片的序列号,也就是 Sequence 方法生成的结果    @param _img: 图片    @return new img    """    r = 160    a = int(r / 2)    np_image = np.array(img)    new_np_img = np.zeros((160, 312, 3), dtype=np.uint8)    for u in range(0, 52):        c = _seq % 26 * 12 + 1        _ = int(a if (25 < _seq) else 0)        xpos = u % 26 * 10        ypos = a if (25 < u) else 0        # var l = getImageData(c, _, 10, a);        # putImageData(l, u % 26 * 10, 25 < u ? a : 0);        slice_img = np_image[_:(_+a), c:(c+10)]        n = len(slice_img[0])        new_np_img[ypos:(ypos+a), xpos:(xpos+n)] = slice_img    return new_np_imgif __name__ == "__main__":    seq = sequence()    img = Image.open('/Users/datochan/WorkSpace/VSCProjects/nike-bot/Test/src/images/fullbg.jpg')    newimg = gen(seq, img)    plt.imshow(newimg)    plt.show()

找一个待处理的图片:https://static.geetest.com/pictures/gt/6edec3cc1/6edec3cc1.jpg,测试结果如下图:

极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

4.jpg

至此,图片乱序还原的问题搞定。

滑动轨迹的加密方法

同样的方法跟踪滑块失败后的请求,分析回溯来到如下代码:

"$_CHBV": function (t, e, n) {    var $_CABJD = $_AB.$_Ds, $_CABIQ = ['$_CACCE'].concat($_CABJD), $_CACAM = $_CABIQ[1];    $_CABIQ.shift();    var $_CACBm = $_CABIQ[0];    var r = this, i = r[$_CABJD(78)];    var o = {        "lang": i[$_CABJD(172)] || $_CACAM(161),       // 语言固定为 zh-hk || zh-cn        "userresponse": $_CEI(t, i[$_CABJD(139)]),     // t=滑动的距离,用户响应的内容, $_CABJD(139) = "challenge" 的值        "passtime": n,   // 滑块消耗的时间=鼠标轨迹每个点耗时相加        "imgload": r[$_CABJD(744)],               "aa": e,   // 滑动轨迹的加密字符        "ep": r[$_CABJD(764)]()    };    i[$_CABJD(118)] && (o[$_CABJD(221)] = t);    o["rp"] = $_DCj(i[$_CACAM(159)] + i[$_CABJD(139)][$_CACAM(151)](0, 32) + o[$_CABJD(736)]);    var s = r[$_CACAM(791)]();  // rsa加密的aes密钥    var a = AES[$_CABJD(389)](gjson[$_CACAM(160)](o), r[$_CABJD(751)]()); // 将上面的json用aes加密    var u = Base64[$_CACAM(739)](a), c = {        "gt": i[$_CABJD(159)],        "challenge": i[$_CACAM(139)],        "lang": o[$_CABJD(172)],        "pt": r[$_CACAM(686)],        "w": u + s    };    ...}

至此,我们找到了移动滑块后提交的参数w的来历。根据以往经验s是aes加密用到密钥,用rsa加密后的密文。重点分析u的来历。向上回溯可知u的组成,将代码还原为:

/** * 用于计算 rp 的hash值 */function $_DCj(t) {        function u(t, e) {                return t << e | t > 32 - e;        }        function c(t, e) {                var n, r, i, o, s;                return i = 2147483648 & t, o = 2147483648 & e, s = (1073741823 & t) + (1073741823 & e), (n = 1073741824 & t) & (r = 1073741824 & e) ? 2147483648 ^ s ^ i ^ o : n | r ? 1073741824 & s ? 3221225472 ^ s ^ i ^ o : 1073741824 ^ s ^ i ^ o : s ^ i ^ o;        }        function e(t, e, n, r, i, o, s) {                return c(u(t = c(t, c(c(function a(t, e, n) {                        return t & e | ~t & n;                }(e, n, r), i), s)), o), e);        }        function n(t, e, n, r, i, o, s) {                return c(u(t = c(t, c(c(function a(t, e, n) {                        return t & n | e & ~n;                }(e, n, r), i), s)), o), e);        }        function r(t, e, n, r, i, o, s) {                return c(u(t = c(t, c(c(function a(t, e, n) {                        return t ^ e ^ n;                }(e, n, r), i), s)), o), e);        }        function i(t, e, n, r, i, o, s) {                return c(u(t = c(t, c(c(function a(t, e, n) {                        return e ^ (t | ~n);                }(e, n, r), i), s)), o), e);        }        function o(t) {                var n = "", r = "";                for (var e = 0; e <= 3; e++) {                        n += (r = "0" + (t > 8 * e & 255)["toString"](16))["substr"](r["length"] - 2, 2);                }                return n;        }        var s, a, _, l, f, h, d, p, g, m;        for (s = function v(t) {                var e, n = t["length"], r = n + 8, i = 16 * (1 + (r - r % 64) / 64), o = Array(i - 1), s = 0, a = 0;                while (a < n) s = a % 4 * 8, o[e = (a - a % 4) / 4] = o[e] | t["charCodeAt"](a) << s, a++;                return s = a % 4 * 8, o[e = (a - a % 4) / 4] = o[e] | 128 << s, o[i - 2] = n << 3, o[i - 1] = n > 29, o;        }(t = function w(t) {                t = t["replace"](/\r\n/g, "\n");                for (var e = "", n = 0; n < t["length"]; n++) {                        var r = t["charCodeAt"](n);                        r < 128 ? e += String["fromCharCode"](r) : (127 < r && r < 2048 ? e += String["fromCharCode"](r  6 | 192) : (e += String["fromCharCode"](r  12 | 224), e += String["fromCharCode"](r  6 & 63 | 128)), e += String["fromCharCode"](63 & r | 128));                }                return e;        }(t)), d = 1732584193, p = 4023233417, g = 2562383102, m = 271733878, a = 0; a < s["length"]; a += 16) p = i(p = i(p = i(p = i(p = r(p = r(p = r(p = r(p = n(p = n(p = n(p = n(p = e(p = e(p = e(p = e(l = p, g = e(f = g, m = e(h = m, d = e(_ = d, p, g, m, s[a + 0], 7, 3614090360), p, g, s[a + 1], 12, 3905402710), d, p, s[a + 2], 17, 606105819), m, d, s[a + 3], 22, 3250441966), g = e(g, m = e(m, d = e(d, p, g, m, s[a + 4], 7, 4118548399), p, g, s[a + 5], 12, 1200080426), d, p, s[a + 6], 17, 2821735955), m, d, s[a + 7], 22, 4249261313), g = e(g, m = e(m, d = e(d, p, g, m, s[a + 8], 7, 1770035416), p, g, s[a + 9], 12, 2336552879), d, p, s[a + 10], 17, 4294925233), m, d, s[a + 11], 22, 2304563134), g = e(g, m = e(m, d = e(d, p, g, m, s[a + 12], 7, 1804603682), p, g, s[a + 13], 12, 4254626195), d, p, s[a + 14], 17, 2792965006), m, d, s[a + 15], 22, 1236535329), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 1], 5, 4129170786), p, g, s[a + 6], 9, 3225465664), d, p, s[a + 11], 14, 643717713), m, d, s[a + 0], 20, 3921069994), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 5], 5, 3593408605), p, g, s[a + 10], 9, 38016083), d, p, s[a + 15], 14, 3634488961), m, d, s[a + 4], 20, 3889429448), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 9], 5, 568446438), p, g, s[a + 14], 9, 3275163606), d, p, s[a + 3], 14, 4107603335), m, d, s[a + 8], 20, 1163531501), g = n(g, m = n(m, d = n(d, p, g, m, s[a + 13], 5, 2850285829), p, g, s[a + 2], 9, 4243563512), d, p, s[a + 7], 14, 1735328473), m, d, s[a + 12], 20, 2368359562), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 5], 4, 4294588738), p, g, s[a + 8], 11, 2272392833), d, p, s[a + 11], 16, 1839030562), m, d, s[a + 14], 23, 4259657740), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 1], 4, 2763975236), p, g, s[a + 4], 11, 1272893353), d, p, s[a + 7], 16, 4139469664), m, d, s[a + 10], 23, 3200236656), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 13], 4, 681279174), p, g, s[a + 0], 11, 3936430074), d, p, s[a + 3], 16, 3572445317), m, d, s[a + 6], 23, 76029189), g = r(g, m = r(m, d = r(d, p, g, m, s[a + 9], 4, 3654602809), p, g, s[a + 12], 11, 3873151461), d, p, s[a + 15], 16, 530742520), m, d, s[a + 2], 23, 3299628645), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 0], 6, 4096336452), p, g, s[a + 7], 10, 1126891415), d, p, s[a + 14], 15, 2878612391), m, d, s[a + 5], 21, 4237533241), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 12], 6, 1700485571), p, g, s[a + 3], 10, 2399980690), d, p, s[a + 10], 15, 4293915773), m, d, s[a + 1], 21, 2240044497), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 8], 6, 1873313359), p, g, s[a + 15], 10, 4264355552), d, p, s[a + 6], 15, 2734768916), m, d, s[a + 13], 21, 1309151649), g = i(g, m = i(m, d = i(d, p, g, m, s[a + 4], 6, 4149444226), p, g, s[a + 11], 10, 3174756917), d, p, s[a + 2], 15, 718787259), m, d, s[a + 9], 21, 3951481745), d = c(d, _), p = c(p, l), g = c(g, f), m = c(m, h);        return (o(d) + o(p) + o(g) + o(m))["toLowerCase"]();}// console.log($_DCj("2328764cdf162e8e60cc0b04383fef81" + "4de7bb253d3999b2dca4d35049959acf7k"))var SlideObject = {        "$_CEAQ": function() {                // PerformanceTiming                var t = window["Performance"]["timing"];                return {                        "a": t["navigationStart"],                        "b": t["unloadEventStart"],                        "c": t["unloadEventEnd"],                        "d": t["redirectStart"],                        "e": t["redirectEnd"],                        "f": t["fetchStart"],                        "g": t["domainLookupStart"],                        "h": t["domainLookupEnd"],                        "i": t["connectStart"],                        "j": t["connectEnd"],                        "k": t["secureConnectionStart"],                        "l": t["requestStart"],                        "m": t["responseStart"],                        "n": t["responseEnd"],                        "o": t["domLoading"],                        "p": t["domInteractive"],                        "q": t["domContentLoadedEventStart"],                        "r": t["domContentLoadedEventEnd"],                        "s": t["domComplete"],                        "t": t["loadEventStart"],                        "u": t["loadEventEnd"]                };        },        "$_CHCg": function () {                return {                        "v": "7.6.0",  // slide.js的版本号                        "f": $_DCj(this["gt"] + this["challenge"]) || "",                        "te": false,  // touchEvent, PC端只有鼠标事件,没有触摸事件                        "me": true,   // mouseEvent                        "tm": this["$_CEAQ"]()                };        },        /**         * 用来处理 userresponse 参数         * t: 滑动的距离         * e: Challenge's value         */        "$_CEI": function (t, e) {            for (var n = e["slice"](32), r = [], i = 0; i < n["length"]; i++) {                var o = n["charCodeAt"](i);                r[i] = 57 < o ? o - 87 : o - 48;            }            n = 36 * r[0] + r[1];            var s, a = Math["round"](t) + n;            var u = [[], [], [], [], []], c = {}, _ = 0;            i = 0;            for (var l = (e = e["slice"](0, 32))[ "length"]; i < l; i++) {                c[s = e["charAt"](i)] || (c[s] = 1, u[_]["push"](s), _ = 5 == ++_ ? 0 : _);            }            var f, h = a, d = 4, p = "", g = [1, 2, 5, 10, 50];            while (0 < h) {                if (0 <= h - g[d]) {                    f = parseInt(Math["random"]() * u[d]["length"], 10);                    p += u[d][f];                    h -= g[d];                } else {                    u["splice"](d, 1);                    g["splice"](d, 1);                    d -= 1;                }            }            return p;        },        /**         *          * @param {*} e 轨迹数组         * @param {*} t 用于处理滑动轨迹数组每个元素的回调方法         */        "$_HBM": function (e, t) {            var n = [], i = e["length"];            if (e["map"]) {                        e["map"](t);                        return;                }            for (var r = 0; r < i; r += 1) {                    n[r] = t(e[r], r);                }        },        /**         * _pt_array: 滑动的轨迹数组         */        "$_BAFz": function (_pt_array) {                function n(t) {                    var s = "";                        var e = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr";                    var n = e["length"], r = "", i = Math["abs"](t), o = parseInt(i / n);                    n <= o && (o = n - 1), o && (r = e["charAt"](o));                    return t < 0 && (s += "!"), r && (s += "$"), s + r + e["charAt"](i %= n);                }            var t = function (t) {                    var e, n, r, i = [], o = 0, a = t["length"] - 1;                    for (var s = 0; s < a; s++) {                            e = Math["round"](t[s + 1][0] - t[s][0]);                            n = Math["round"](t[s + 1][1] - t[s][1]);                            r = Math["round"](t[s + 1][2] - t[s][2]);                            0 == e && 0 == n && 0 == r || (0 == e && 0 == n ? o += r : (i["push"]([e, n, r + o]), o = 0));                    }                    return 0 !== o && i["push"]([e, n, o]), i;                }(_pt_array);    // 滑动的轨迹数组                var r = [], i = [], o = [];                this["$_HBM"](t, function (t) {                var e = function (t) {                            var e = [[1, 0], [2, 0], [1, -1], [1, 1], [0, 1], [0, -1], [3, 0], [2, -1], [2, 1]];                            for (var n = 0; n < e["length"]; n++) {                                    if (t[0] == e[n][0] && t[1] == e[n][1])                                             return "stuvwxyz~"[n];                            }                            return 0;                        }(t);                e ? i["push"](e) : (r["push"](n(t[0])), i["push"](n(t[1]))), o["push"](n(t[2]));            });            return r["join"]("") + "!!" + i["join"]("") + "!!" + o["join"]("");        },        /**         * t: $_BAFz的返回值         * e: 接口返回值中C的值: [12, 58, 98, 36, 43, 95, 62, 15, 12]         * n: 接口返回的s值         */        "$_BGDl": function(t, e, n){                if (!e || !n) return t;                var r, i = 0, o = t, s = e[0], a = e[2], u = e[4];                while (r = n["substr"](i, 2)) {                        i += 2;                        var c = parseInt(r, 16);                        var _ = String["fromCharCode"](c);                        var l = (s * c * c + a * c + u) % t["length"];                        o = o["substr"](0, l) + _ + o["substr"](l);                }                return o;        },        /**         * t: 滑动的距离: 鼠标轨迹最后一个坐标点的X值         * e: 加密后的鼠标轨迹($_BGDl的返回值)         * n: 用户滑动的耗时=鼠标轨迹每个点耗时相加         */        "$_CHBV": function(t, e, n) {                var r = this, i = r[$_CABJD(78)]; // i是get.php返回的集合                var o = {                        "lang": "zh_hk" || "zh_cn",                        "userresponse": $_CEI(t, i["challenge"]),  // t=鼠标轨迹最后一个坐标点的X值                        "passtime": n,  // 滑块消耗的时间=鼠标轨迹每个点耗时相加                        "imgload": r[$_CABJD(744)],    // 图片加载需要的时间                        "aa": e,   // 滑块轨迹加密后的字符集                        "ep": this["$_CHCg"]()    // 基础环境信息收集                };                o["rp"] = $_DCj(i["gt"] + i["challenge"]["slice"](0, 32) + o["passtime"]);  // 会话信息的hash加密                var s = rsa_encrypt(), a = AES_encrypt(gjson(o), aes_key), u = Base64_encrypt(a);                var c = {                        "gt": i[$_CABJD(159)],                        "challenge": i[$_CACAM(139)],                        "lang": "zh_hk",                        "pt": r[$_CACAM(686)],                        "w": u + s                }                // TODO: 提交post请求        }}// 调用示例var pt_list = [[-37,-41,0], [0,0,0], [3,0,251], [6,0,266], [9,0,283], [13,0,300], [15,0,316], [17,0,333], [19,0,349], [20,0,366], [20,0,383], [20,0,400], [20,0,430]];var _ = SlideObject["$_BGDl"](SlideObject["$_BAFz"](pt_list), [12, 58, 98, 36, 43, 95, 62, 15, 12], "35304332")console.log(_);

其中 SlideObject["$_CEAQ"]方法依赖于浏览器的windows环境,如下图:

极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

5.jpg

继续跟进,发现"ep"值,是windows的Performance.timing中的值。

极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

6.jpg

因此,可以根据 Performance.timing 时间产生的先后顺序以及时间间隔,用当前的时间戳减去相应的值来模拟。由于相关代码比较简单,为了节省篇幅就不给出了。

滑动轨迹生成的思路

由于极验采用人工智能的方式对滑动的轨迹进行的验证,因此如果我们比较随意的生成鼠标滑动轨迹基本是肯定被封的,因此我们要详细分析一下鼠标轨迹的规律,
通之前介绍的调试手段,手工滑动滑块,获取到鼠标滑动轨迹的集合数组如下:

[[-37,-41,0], [0,0,0], [3,0,251], [6,0,266], [9,0,283], [13,0,300], [15,0,316], [17,0,333], [19,0,349], [20,0,366], [20,0,383], [20,0,400], [20,0,430]]

每个点的组成为:[x, y, timestamp],含义如下:

  • x 坐标: 从0开始,一直到滑动结束, 每个坐标间隔越大说明滑动越快,静止不动就不变。
  • y 坐标: 可以为正,也可以为负数,都是个位数。取值范围: [-2, 2), 多取值0,次之是-1,极少的-2和正1
  • timestamp: 鼠标在当前点停留的时间(毫秒)

经反复测试得知还有如下规律:

  • 滑动轨迹第一个坐标点(X,Y)是负数,其取值范围在(-40, -18)
  • 第二个坐标点是0,0,0,从第三甚至第四个坐标点开始
  • y坐标的取值范围比较简单,人手横向滑动的轨迹一般负责先减少,在快速增加,再慢慢减少的轨迹。

因为y的取值比较简单,只考虑x坐标与z坐标的关系,将手工调试取10个坐标,以时间为X坐标,滑动距离为Y坐标,打印出来绘图为:
极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

7.jpg

图像的轨迹有点儿像 tanharctan的混合体,如下图:
极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

8.jpg

我们将两个图像整合、移动并添加一些噪点,最终生成的图像为:

极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

9.jpg

这样的图像很像我们之前采集的鼠标轨迹图像了。至此,鼠标滑动轨迹的X坐标生成方法就告一段落。剩下的是滑动时间的分配。

对上面十次滑动的坐标集合,计算出每个坐标点消耗的时间,对时间进行汇总,如下表:

序号0-15(ms)15-20(ms)20-200(ms)200-400(ms)1