# 极验滑块:

https://www.geetest.com/demo/slide-bind.html

# 登录参数 w 值

F12,点击登录,弹出滑块界面,出现滑块图和背景图,拖动滑块,会发送请求:

参数如下:gt\challenge\w\callback (其余的貌似不为加密)

明显,在该请求中生成了这些参数。

跟值,找到生成参数的位置。

u = r[$_CAGEe(750)]()
                  , l = V[$_CAGEe(342)](gt[$_CAGEe(209)](o), r[$_CAGEe(742)]())
                  , h = m[$_CAGEe(733)](l)
                  , f = {
                    "\u0067\u0074": i[$_CAGEe(147)],
                    "\u0063\u0068\u0061\u006c\u006c\u0065\u006e\u0067\u0065": i[$_CAGDp(154)],
                    "\u006c\u0061\u006e\u0067": o[$_CAGDp(119)],
                    "\u0024\u005f\u0042\u0042\u0046": r[$_CAGEe(623)],
                    "\u0063\u006c\u0069\u0065\u006e\u0074\u005f\u0074\u0079\u0070\u0065": r[$_CAGEe(648)],
                    "\u0077": h + u
                };

"\u0077":为所需的 w 值。

下面就需要去寻找其生成逻辑,可以直观的看到,其是由 h,u 拼接而成的。

接下来就需要分析 u、l、h 的逻辑即可。

首先明确一个观点,这里一定有一个值为定值,因为滑块拖动的轨迹是一定的

# u 值
u = r[$_CAGEe(750)]()

u 运行了一个函数,未传参数进去、直接自执行。经过测试,发现其不为定值。

内部逻辑:

function(t) {
                var $_CBEDU = lTloj.$_CX
                  , $_CBECg = ['$_CBEGr'].concat($_CBEDU)
                  , $_CBEEc = $_CBECg[1];
                $_CBECg.shift();
                var $_CBEFV = $_CBECg[0];
     // 前面属于初始化的步骤
                var e = new X()[$_CBEEc(342)](this[$_CBEEc(742)](t));
                while (!e || 256 !== e[$_CBEEc(182)])
                    e = new X()[$_CBEDU(342)](this[$_CBEDU(742)](!0));
                return e;
            }
$_CBEEc(342) = 'encrypt';
this[$_CBEEc(742)] = (Ot = rt(),
            function(t) {
                var $_CBDIs = lTloj.$_CX
                  , $_CBDHp = ['$_CBEBa'].concat($_CBDIs)
                  , $_CBDJA = $_CBDHp[1];
                $_CBDHp.shift();
                var $_CBEAd = $_CBDHp[0];
                return !0 === t && (Ot = rt()),
                Ot;
            }
            )
var rt = function() {
    		// 初始化操作
            var $_BFBDL = lTloj.$_CX
              , $_BFBCi = ['$_BFBGO'].concat($_BFBDL)
              , $_BFBEt = $_BFBCi[1];
            $_BFBCi.shift();
            var $_BFBFk = $_BFBCi[0];
    
            function t() {
                var $_DBFAh = lTloj.$_DP()[0][4];
                for (; $_DBFAh !== lTloj.$_DP()[2][3]; ) {
                    switch ($_DBFAh) {
                    case lTloj.$_DP()[0][4]:
                        return (65536 * (1 + Math[$_BFBDL(75)]()) | 0)[$_BFBDL(396)](16)[$_BFBDL(476)](1);
                        break;
                    }
                }
            }
            return function() {
                
                var $_BFBIl = lTloj.$_CX
                  , $_BFBHs = ['$_BFCBY'].concat($_BFBIl)
                  , $_BFBJq = $_BFBHs[1];
                $_BFBHs.shift();
                var $_BFCAQ = $_BFBHs[0];
                
                
                return t() + t() + t() + t();
            }
            ;
        }();
t = (65536 * (1 + Math['random']()) | 0)['toString'](16)['substring'](1)

首先运行了 rt 函数,接着看 rt 函数内部,内部包含两次初始化操作,貌似没啥用。我们可以看到其中真实的返回值为 4 个 t () 函数运行之和,那么接着我们只需要分析 t 函数即可,

本质上 t 其实是先通过 random 了一个随机数,然后位运算,在经过 toString、substring 将其转化为一个 4 位字符、接着 4 次拼接,就生成了 16 位随机字符。

接下来,就就可以还原成如下形式:

var decode_u = function(){return (65536 * (1 + Math['random']()) | 0)['toString'](16)['substring'](1)};
var u_decode = decode_u() + decode_u() + decode_u() + decode_u();

接下来再看 e 的整体部分

//$_CBEEc(342) = 'encrypt';
var e = new X()[$_CBEEc(342)](this[$_CBEEc(742)](t));
 while (!e || 256 !== e[$_CBEEc(182)])
                    e = new X()[$_CBEDU(342)](this[$_CBEDU(742)](!0));
 return e;
// 还原 e
var e = new X()['encrypt'](u_decode);

while 那部分 e 貌似经过调试也不经过那里,因此可以不用管,将 e 简单的还原,明显,我们只需要知道这个 X () 是啥不就行了吗,从字面意思上来看,就是对随机的 u_decode 的一个加密操作,跟进去:

function E() {
                var $_DBDBL = lTloj.$_DP()[2][4];
                for (; $_DBDBL !== lTloj.$_DP()[2][3]; ) {
                    switch ($_DBDBL) {
                    case lTloj.$_DP()[0][4]:
                        this[$_HHJn(310)] = null,
                        this[$_HHJn(319)] = 0,
                        this[$_HHIO(392)] = null,
                        this[$_HHIO(365)] = null,
                        this[$_HHJn(352)] = null,
                        this[$_HHJn(358)] = null,
                        this[$_HHIO(395)] = null,
                        this[$_HHIO(329)] = null;
                        //$_HHIO(349) = 'setpublic'
                        this[$_HHIO(349)]($_HHJn(370), $_HHIO(354));
                        $_DBDBL = lTloj.$_DP()[2][3];
                        break;
                    }
                }
            }
// E['prototype']['setpublic']
E[$_HHJn(261)][$_HHIO(349)] = function ut(t, e) {
                var $_JIId = lTloj.$_CX
                  , $_JIHf = ['$_JJBk'].concat($_JIId)
                  , $_JIJy = $_JIHf[1];
                $_JIHf.shift();
                var $_JJAX = $_JIHf[0];
                null != t && null != e && 0 < t[$_JIJy(182)] && 0 < e[$_JIId(182)] ? (this[$_JIId(310)] = function n(t, e) {
                    var $_JJDX = lTloj.$_CX
                      , $_JJCE = ['$_JJGq'].concat($_JJDX)
                      , $_JJEo = $_JJCE[1];
                    $_JJCE.shift();
                    var $_JJFy = $_JJCE[0];
                    return new y(t,e);
                }(t, 16),
                this[$_JIJy(319)] = parseInt(e, 16)) : console && console[$_JIJy(6)] && console[$_JIId(6)]($_JIId(348));
            }
// E['prototype']['encrypt']
E[$_HHIO(261)][$_HHJn(342)] = function lt(t) {
                var $_JJI_ = lTloj.$_CX
                  , $_JJHt = ['$_BAABI'].concat($_JJI_)
                  , $_JJJB = $_JJHt[1];
                $_JJHt.shift();
                var $_BAAAC = $_JJHt[0];
                var e = function a(t, e) {
                    var $_BAADI = lTloj.$_CX
                      , $_BAACi = ['$_BAAGL'].concat($_BAADI)
                      , $_BAAEP = $_BAACi[1];
                    $_BAACi.shift();
                    var $_BAAFA = $_BAACi[0];
                    if (e < t[$_BAAEP(182)] + 11)
                        return console && console[$_BAADI(6)] && console[$_BAADI(6)]($_BAADI(312)),
                        null;
                    var n = []
                      , r = t[$_BAADI(182)] - 1;
                    while (0 <= r && 0 < e) {
                        var i = t[$_BAADI(137)](r--);
                        i < 128 ? n[--e] = i : 127 < i && i < 2048 ? (n[--e] = 63 & i | 128,
                        n[--e] = i >> 6 | 192) : (n[--e] = 63 & i | 128,
                        n[--e] = i >> 6 & 63 | 128,
                        n[--e] = i >> 12 | 224);
                    }
                    n[--e] = 0;
                    var o = new l()
                      , s = [];
                    while (2 < e) {
                        s[0] = 0;
                        while (0 == s[0])
                            o[$_BAADI(276)](s);
                        n[--e] = s[0];
                    }
                    return n[--e] = 2,
                    n[--e] = 0,
                    new y(n);
                }(t, this[$_JJJB(310)][$_JJI_(353)]() + 7 >> 3);
                if (null == e)
                    return null;
                var n = this[$_JJJB(388)](e);
                if (null == n)
                    return null;
                var r = n[$_JJJB(396)](16);
                return 0 == (1 & r[$_JJJB(182)]) ? r : $_JJJB(44) + r;
            }

总的来说,对于 u 值的生成有如下几步:

  1. 首先 new 一个 X 对象,其中 X 指向 E ()。
  2. 在 new 的时候,首先获取两个定参数,然后根据两个定参数设置一个 public。
  3. 接着调用原型链上的 encrypt 方法,同时传入了一个 16 位的随机数。
  4. 最后生成 256 位结果。
# l 值
l = V["encrypt"](gt["stringify"](o), r["$_CBFG"]()),
// 手动还原
r["$_CBFG"]() 
// 这个会生成一个值,每次刷新都不一样,伪动态,同时跟进去分析发现,这个值其实返回的就是一个一开始初始化的值,逻辑就是 u 值的 Ot, 可改写为 u_decode。
//gt ["stringify"](o) 这一部分会返回一个对象,

通过对比可以发现 o,传入了一系列参数,这也是导致两次运行结果不一致的主要原因,其中最后生成的 rp 也是一个不一致的参数。

那么我们主要分析点就是 o 是如何生成的。

r = this,
i = r["$_CIY"],
o = {
          "lang": i["lang"] || "zh-cn",
          "userresponse": H(t, i["challenge"]),
          "passtime": n,
          "imgload": r["$_BJHF"],
          "aa": e,
          "ep": r["$_CBD_"]()
        };

在这部分中,我们可以看到,使用了 n、e、t 这三个传进来的参数,貌似看起来和鼠标轨迹有关。

"$_CGHV": function (t, e) {
        var n = this,
            r = n["$_BAHt"],
            i = n["$_BBGR"],
            o = n["$_CIY"],
            s = n["$"];
        try {
          if (i["$_HA_"]() !== $t) return;
          if (n["$_CHHW"] && "pointerup" != t["type"]) return;
          v(function () {
            o["link"] && s(".link")["$_CBV"]({
              "target": "_blank",
              "href": o["link"]
            });
          }, 0), t["$_BGDK"](), i["$_GJD"]("lock");
		//u 生成的位置
          var a = n["$_CFDP"],
              _ = e ? n["lastPoint"]["x"] : t["$_BGBX"]() / a - n["$_CHJg"],
              c = e ? n["lastPoint"]["y"] : n["$_CIAc"] - t["$_BGCq"]() / a;
          n["$_CECK"] = $_HP() - n["$_CHIF"], n["$_CIBw"]["$_BBBF"]([Math["round"](_), Math["round"](c), n["$_CECK"]]);
          var u = parseInt(_),
              l = n["$_CIBw"]["$_BBCA"](n["$_CIBw"]["$_GEy"](), n["$_CIY"]["c"], n["$_CIY"]["s"]);
          r["$_CBCM"](u, l, n["$_CECK"]), n["$_CFGp"]["$_EFJ"]();
        } catch (t) {
          r["$_DAK"](t);
        }

在这里 n、e、t 分别对应 u、l、n ["$_CECK"],通过调试可以发现这三个估计分别对应距离、轨迹、时间。

# 小 u:

这里的小 u 向上看,由于传进来的 e 为 undefined,所以这里的流程只走向了后面一部分,同时,通过对滑块添加 pointermove 事件发现,这里的 t ["$_BGBX"] 为滑块停止的位置,在经过反复调试后,貌似 a 是个可以写死的东西,最后一项为初始位置,基本上该部分就是计算了一下偏移路径。

# 小 l:

根据分析可知,该部分由一个函数传入了三个参数构成,后面两个参数为服务器返回,第一个参数为一个三维数组,第一维为一个 x 轴偏移,第二个为 y 轴偏移,第三个为时间戳偏移。

# n["$_CECK"]:

这项就是个时间戳之差,用结束时间戳减去开始的时间戳。

# V["encrypt"]

这部分可以直接导出。

# h 值
h = m["$_GFm"](l);
"$_GFm": function (t) {
        var e = this["$_GDR"](t);
        return e["res"] + e["end"];
},

这部分也可直接导出。

# AST 解混淆

观察代码,我们可以发现,存在大量混淆,增加了阅读难度,所以首先使用 AST 解混淆。

将代码放入在线网站解析,发现分为 6 块。

  1. ExpressionStatement
  2. ExpressionStatement
  3. ExpressionStatement
  4. ExpressionStatement
  5. FunctionDeclaration
  6. ExpressionStatement

其中前四部分可以看作解密函数,第五部分可以看做一个类似声明对象的过程。与之前解的 OB 混淆原理不太一样,这里面采用了大量的重赋值操作。

var $_DBIV = lTloj.$_CX
, $_DBHX = ['$_DCBv'].concat($_DBIV)
, $_DBJB = $_DBHX[1];
$_DBHX.shift();
var $_DCAU = $_DBHX[0];

我们可以看到,其显示通过一个变量修饰器,将一个解密函数赋值过来,然后通过数组拼合的方式生成了一个新数组,再通过数组取值的操作,将其中的值取出来,接着对数组首元素进行去除,在取值。

简单来讲,就是把解密函数赋值过去,数组一开始携带的参数貌似没什么用。

# STEP1 ASCII 码还原:

ASCII 码还原,增加可读性,没啥好讲的。

# STEP2 解密函数部分还原:

由于在后面中,存在变量声明调用解密函数的过程,这里为了方便先直接进行一部分替换。

通过分析我们可以知道,第三个和第四个解密函数,才为真正的解密函数,因为在后续的代码中,很大一部分都是通过该部分才进行了操作,那么我们只需要遍历 MemberExpression 节点,同时限制其父节点为声明表达式即可将遍历条件限定在前几个,然后通过遍历作用域,将有变量赋值的部分替换。

/* 
a.b = function (){return val};
var c = a.b;
转化为
a.b = function (){return val};
var c = function (){return val};
*/
const visitor1 =  {
     MemberExpression(path){
        const {object,property} = path.node;
        const {scope} = path;
        var func_name = object.name,pro_name = property.name;
        // 只遍历声明式的部分,防止反复调用
        if(!types.isAssignmentExpression(path.parent)) return;
        if(func_name !== func_all) return;
        path.scope.traverse(path.scope.block, {
            MemberExpression(_path){
                if(_path.node.object.name === func_name && _path.node.property.name === property.name){
                    // 对于变量赋值
                    if(types.isVariableDeclarator(_path.parent)){
                        _path.replaceInline(path.parent.right);
                    };
                }
            }
        });
    }
};

这一步主要是为后一步生成数组做铺垫。

# STEP3 解密函数内存写入:

紧接着我们将前 5 部分全部压入内存中,这样的好处就是后面在有调用的部分处,可以直接通过 eval 结合解密函数运行出值,直接用于替换。

// 解密函数对象昵称
let descrypt_strfun_js = '';
for(let i=0;i<=4;i++){
    // 加入代码
    descrypt_strfun_js += generator(ast_code.program.body[i], {compact:true}).code
    delete ast_code.program.body[i]
}
// 运行数组
eval(descrypt_strfun_js);
# STEP4 赋值表达式替换:

由于后续的针对数组变化的插件需要能将数组写入内存中,而 concat 部分又包含了数组的调用,所以我们要将该部分还原成其原型。

/* 
var b = function (){return arguments};
var a = ['as'].concat (b)
还原后
var a = ['as'].concat (function (){return arguments})
*/
const var_visitor  = {
    // 查找赋值表达式
    VariableDeclarator(path) {
        const {id, init} = path.node;
        var binding = path.scope.getBinding(id.name);
        references = binding.referencePaths;
        if(!types.isFunctionExpression(init) || init.body.body.length !== 1) return;
        for(reference of references){
            if(reference.parentPath.toString().indexOf('concat') > -1){
                reference.replaceInline(init);
            }
        };
    }
};
# STEP 5 数组还原 + 临时变量还原:

该部分就是对先前提到的数组进行的还原,临时变量其实指代的也是解密函数。

// 变量替换
const arr_change_visitor = {
    // 数组替换
    VariableDeclarator(path) {
        const {id, init} = path.node;
        var func_name = id.name;
        var binding = path.scope.getBinding(func_name);
        var referpaths = binding.referencePaths;
        // 如果变量没引用,则返回
        if (referpaths.length === 0) return;
        // 保证变量的 init 部分值唯一,数字、字符类型直接替换,函数定义也可直接替换,前提是引用部分为单引用,若为函数调用,可考虑直接计算值。
        // 单独针对这种情况,不考虑单纯的数组。
        if (types.isCallExpression(init) && types.isArrayExpression(init.callee.object)) {
            // 将数组中的内容直接写入内存
            if(init.callee.property.name !== 'concat') return;
            var old_arr = init.callee.object.elements.concat(init.arguments[0]);
            global[func_name] = old_arr;
            // 遍历作用域,获取修改值的地址
            binding.scope.traverse(binding.scope.block, {
                MemberExpression(_path) {
                    // 后面可能还要确保只修改一次的情况
                    // console.log(_path.toString());
                    if(_path.node.object.name !== func_name) return;
                    if (_path.node.property.value >= 0) {
                        let index_value = _path.node.property.value;
                        _path.replaceInline(global[func_name][index_value]);
                    };
                    // 若包含 shift 操作
                    if (_path.node.property && _path.node.property.name === 'shift') {
                        global[func_name].shift()
                    }
                }
            });
            path.remove();
        }
    }
};
// 这部分主要是通过将还原后的部分写入内存,在遍历,接着对包含 shift () 的代码删除,由于该部分的解密体为函数,所以遍历的是 CallExpression 节点。
const var_write = {
    VariableDeclarator(path){
        const {id,init} = path.node;
        var binding = path.scope.getBinding(id.name);
        references = binding.referencePaths;
        if(!types.isFunctionExpression(init) || init.body.body.length !== 1) return;
        eval(path.toString());
        path.scope.traverse(path.scope.block,{
            CallExpression(_path) {
                if(_path.node.callee.name === id.name){
                 _path.replaceInline(types.valueToNode(eval(_path.toString())));
            };
        }
        });
        // 遍历替换结束,删除原节点
        path.remove();
        },
    MemberExpression(path){
        const {object,property} = path.node;
        if(property.name !== 'shift') return;
        path.parentPath.remove();
    }
};
# STEP 6 解密函数批量还原

到了这一步,将解密函数全部运行替换即可。

var visitor_sec = {
    // 遍历 call 节点,将内存中的函数直接计算
    CallExpression(path){
        const {callee,arguments} = path.node;
        // 如果于内存中存在
        if(callee.object && global[callee.object.name] && global[callee.object.name][callee.property.name]){
            if(arguments.length>0){
            path.replaceInline(types.valueToNode(eval(path.toString())));
            }
            else if(arguments.length === 0){
                // 由于该处的数组是个无限递归,推测该部分应该只是一个用于比较的逻辑,即判断数组下标即可。
                if(types.isMemberExpression(path.parent) &&types.isMemberExpression(path.parentPath.parent)){
                    // var args1 = path.parent.property.value,args2 = path.parentPath.parent.prototype.value;
                    //console.log(path.parentPath.parentPath.toString());
                    //path.replaceInline(types.valueToNode(eval(path.parentPath.parentPath.toString())))
                }
            }
        }
    }
}
# STEP 7 控制流平坦化:

通过上述步骤,已经将代码还原到了一个可以阅读的程度了,但是在整体的代码中还包含着一个及其特殊的部分。

function $_BFf(t) {
      var $_DAIEJ = lTloj.$_DP()[0][4];
      for (; $_DAIEJ !== lTloj.$_DP()[2][3];) {
        switch ($_DAIEJ) {
          case lTloj.$_DP()[2][4]:
            return {
              ".popup_ghost": {},
              ".popup_box": {
                ".popup_header": {
                  "span.popup_tip": {},
                  "span.popup_close": {}
                },
                ".popup_wrap": t
              }
            };
            break;
        }
      }
    }

由于 lTloj.$_DP () 这个解密函数解出来是一个无限套娃的玩意,同时,我们可以看到,其在代码流程中好像除了做判断,就没啥用了,也就是说,对于上述函数的流程中,我们只需要关注 case 中的那一部分即可,因为 for 循环貌似给整体加上了个无限循环,但是有 return 的话,就直接跳出去了。

其实还原到这一步了,代码的可读性已经很大程度提高了,其检测了什么环境已经一目了然,接下来就是去补环境了。

# canvas

canvas 为画布,在过验证码的过程中,我们发现服务器没返回图片,而是返回了两张 webp 格式的散乱图,也就是说,在代码中一定存在操作,使得返回的散乱图拼合成一个完整的图片,那么我们首先要做的就是找到其生成参数的位置。

在该例中图片的格式为 312*160

# STEP1 观察图片生成原理:

在事件断点中打上 canvas 断点,调试,发现该位置貌似是生成参数的位置,观察 l 发现,其 width = 10、height = 80,我们将其 webp 格式的图片打开可以发现,其是将图片划分为了两层,也就是说,这里的操作实际上是对一个 52 块的图片进行一个拼合,而且这里也只循环了 52 次,接下来只需要搞清楚里面在干什么就能对整体进行还原了。

for (var a = r / 2, _ = 0; _ < 52; _ += 1) {
       					// 这里应该是确定图片位置
                        var c = Ut[_] % 26 * 12 + 1
                          , u = 25 < Ut[_] ? a : 0
                          , l = o[$_CJET(69)](c, u, 10, a);
       					// 这里是拼图的过程
                        s[$_CJET(66)](l, _ % 26 * 10, 25 < _ ? a : 0);
                    }

目前思路是,先拼上面的 26 张,在拼下面的 26 张,这里的 l 是将分割的图片一个个展示在画布上,c 是 x 轴坐标,u 是 y 轴坐标,a 为 y 轴高度。

也就是说我们只需要将图片分割后,一快快的按顺序拼起来即可。

这里每一小块的大小为 12 * 80 然后在展示的时候只展示了 10 * 80 的大小,最后拼成一张 260 * 160 的图片,其中展示的顺序与传入的那个数组一样,数组中的值为其对应的位置,左上角为 0,然后 26 代表从左下角开始。

//a 为传入的大数组
new_img = Image.new('RGB', (260, 160))
for idx in range(len(a)):
    #获取当前位置的真实图的区域
    x = _Ge[idx] % 26 * 12 + 1
    y = h_sep if a[idx] > 25 else 0
    #图像裁剪
    img_cut = _img.crop((x, y, x + w_sep, y + h_sep))
   	new_x = idx % 26 * 10
    new_y = h_sep if idx > 25 else 0
    #将真实图回填
    new_img.paste(img_cut, (new_x, new_y))
# STEP2 缺口识别:

原理上是通过检测二值化的边界,与滑块图进行比对 (代码来源网上)。

def show(name):
    '''展示圈出来的位置'''
    cv2.imshow('Show', name)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
def _tran_canny(image):
    """消除噪声"""
    image = cv2.GaussianBlur(image, (3, 3), 0)
    return cv2.Canny(image, 50, 150)
def detect_displacement(img_slider_path, image_background_path):
    """detect displacement
    	params: img_slider_path 滑块图片
    	parmas: image_background_path 带缺口的滑块图片
    """
    # # 参数 0 是灰度模式
    image = cv2.imread(img_slider_path, 0)
    template = cv2.imread(image_background_path, 0)
    # 寻找最佳匹配
    res = cv2.matchTemplate(_tran_canny(image), _tran_canny(template), cv2.TM_CCOEFF_NORMED)
    # 最小值,最大值,并得到最小值,最大值的索引
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
    top_left = max_loc[0]  # 横坐标
    # 展示圈出来的区域
    x, y = max_loc  # 获取 x,y 位置坐标
    w, h = image.shape[::-1]  # 宽高
    cv2.rectangle(template, (x, y), (x + w, y + h), (255, 0, 0), 2)
    show(template)
    return top_left
top_left = detect_displacement("b.png", "a.jpg")
# STEP3 轨迹模拟:

通过随机数,模拟轨迹。

def get_random_arr(num):
    '''
    :param num: 图片的路径差
    :return: 三维数组,x轴,y轴,时间戳
    '''
    x_arr = [-50]
    y_arr = [-10]
    time_arr = [0]
    i = 0
    temp_x = 0
    temp_y = 0
    temp_time = 0
    while (num-i > 0):
        temp_x += random.randint(1,5)
        temp_y += 0
        temp_time += random.randint(20,50)
        x_arr.append(temp_x)
        y_arr.append(temp_y)
        time_arr.append(temp_time)
        i += temp_x
    result = [[x_arr[i],y_arr[i],time_arr[i]] for i in range(len(x_arr))]
    return result

# 初始化页面 w 值

# 加密逻辑

咋分析页面的时候,我们发现,整体的流程如下:

首先访问页面,初始化,生成一个随机数,随机数生成 gt、challage 两个参数,然后通过 fullpage.js 生成 w 值,接着传入 w、gt、challage 获取一串 json (包含初始化以及返回的验证码图片),接着拖动滑块在在在 slider.js 中获取轨迹参数 w,最终通过校验。

页面初始化的逻辑只有 l 值不同,因为它传进去的不是轨迹,而是一系列参数。

# 参数分析

逻辑与上面类似。

# 补充

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

Mr2 WeChat Pay

WeChat Pay