# 极验滑块:
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 值的生成有如下几步:
- 首先 new 一个 X 对象,其中 X 指向 E ()。
- 在 new 的时候,首先获取两个定参数,然后根据两个定参数设置一个 public。
- 接着调用原型链上的 encrypt 方法,同时传入了一个 16 位的随机数。
- 最后生成 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 块。
- ExpressionStatement
- ExpressionStatement
- ExpressionStatement
- ExpressionStatement
- FunctionDeclaration
- 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 值不同,因为它传进去的不是轨迹,而是一系列参数。
# 参数分析
逻辑与上面类似。