声明
原创文章,请勿转载!
本文内容仅限于安全研究,不公开具体源码。维护网络安全,人人有责。
环节概述
首先我们还是需要使用请求转发工具,把目标代码替换为我们处理过的代码。
- 观察验证过程中触发了哪些请求
- 破解
w
参数 - 根据参数构造请求
验证过程中触发的请求
- 请求总览请求里有两个参数:gt 和 challenge,这两个参数非常重要,后续的请求也都会携带他们,极验通过这两个参数来标识你在做什么操作,以及后续还需什么流程。其中 gt 是定值,这个是和极验申请的 ID。challenge 是一个行为的 ID,上一步的操作会返回你新操作的 challenge,然后下一个操作带上这个新 challenge 即可。同时每个请求返回的都是 JSONP,我们自己解析返回值的时候注意处理下格式即可(比如用正则匹配出来结果)请求中另外一个最重要的参数是 w。它是被加密过的一段数据,是校验通过与否的核心,我们这次的目的就是破解这个 w 参数。
- gettype.php:获取核心 JS 文件链接
- get.php:无感验证的一部分,收集浏览器信息并上报。其中 c 和 s 比较关键,后续的请求会用到它
- ajax.php:执行无感验证。如果验证失败会返回验证类型,比如滑块,点选等。
- slide.x.x.x.js:正式进入滑动验证的部分,这里是在加载相关 JS 文件
- get.php:获取滑动验证的一些基本数据,需要关注的有 bg, c, challenge, fullbg, gt, slice, s
- ajax.php:执行滑动验证,成功后返回 validate
破解
w
参数
因为本次介绍的内容是滑动验证,所以 fullpage.js 发起的无感验证的相关请求,里面加密的
w
参数随便填,其他的参数看着填即可。这样最后就会触发滑动验证。
因为我们破解的关键在于
w
参数,所以先在本地代码搜索下它,然后在那里写一个 debugger 进去,这样我们可以在它执行时随意调试了。
- 观察
w
是怎么来的#mermaid-svg-vPxs4qz1ocXS2K3p {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p .error-icon{fill:#552222;}#mermaid-svg-vPxs4qz1ocXS2K3p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vPxs4qz1ocXS2K3p .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-vPxs4qz1ocXS2K3p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vPxs4qz1ocXS2K3p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vPxs4qz1ocXS2K3p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vPxs4qz1ocXS2K3p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vPxs4qz1ocXS2K3p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vPxs4qz1ocXS2K3p .marker.cross{stroke:#333333;}#mermaid-svg-vPxs4qz1ocXS2K3p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vPxs4qz1ocXS2K3p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p .cluster-label text{fill:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p .cluster-label span{color:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p .label text,#mermaid-svg-vPxs4qz1ocXS2K3p span{fill:#333;color:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p .node rect,#mermaid-svg-vPxs4qz1ocXS2K3p .node circle,#mermaid-svg-vPxs4qz1ocXS2K3p .node ellipse,#mermaid-svg-vPxs4qz1ocXS2K3p .node polygon,#mermaid-svg-vPxs4qz1ocXS2K3p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vPxs4qz1ocXS2K3p .node .label{text-align:center;}#mermaid-svg-vPxs4qz1ocXS2K3p .node.clickable{cursor:pointer;}#mermaid-svg-vPxs4qz1ocXS2K3p .arrowheadPath{fill:#333333;}#mermaid-svg-vPxs4qz1ocXS2K3p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vPxs4qz1ocXS2K3p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vPxs4qz1ocXS2K3p .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-vPxs4qz1ocXS2K3p .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-vPxs4qz1ocXS2K3p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vPxs4qz1ocXS2K3p .cluster text{fill:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p .cluster span{color:#333;}#mermaid-svg-vPxs4qz1ocXS2K3p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vPxs4qz1ocXS2K3p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}l 来自 w h u r.$_CCDv() m.$_GGc(l) V.encrypt(pt.stringify(o), r.$_CCEB()) 可以看出w
参数有两个部分组成,其中每个部分有自己独特的构造逻辑。我们要做的就是把不会变的逻辑单独拿出来,然后会变动的部分想办法做破解。 - 破解
u
参数我们进入r.$_CCDv()
这个函数,他是这样的:里面首先有一个U
对象,并且调用了它内部的一个函数。我们不太需要关心这个对象具体的逻辑,把他复制出来直接调用即可。然后它还会调用上方的一个$_CCEB
函数。这个函数的逻辑其实很简单,里面的rt
函数点击去是这样的。可以看出它的功能只是生成一个随机值。这些代码我们都复制出来,u
参数已经破解好了。functiongetRandomTextHelper(){consthelper=()=>((65536*(1+ Math.random()))|0).toString(16).substring(1)returnhelper()+helper()+helper()+helper()}// 随机值的缓存var oldRandomText =getRandomTextHelper()// 这个就是刚才生成随机值的函数functiongetRandomText(needRefresh){if(needRefresh){ oldRandomText =getRandomTextHelper()}return oldRandomText}// 获取 u 参数functiongetU(){// U 对象自己复制出来即可,太长了var e =newU().encrypt(getRandomText())while(!e ||256!== e.length) e =newU().encrypt(getRandomText(true))return e}// 自行把这个对象复制过来varU=...
- 破解
h
参数这个h
参数的生成相对复杂一些。从刚才的代码我们可以看出,h
来自于l
,l
又来自于o
。其中h
和l
的逻辑比较简单,只是简单的复制工作。1.h
参数:var h = m.$_GGc(l)
这个$_GGc
函数其实也是固定内容,复制下来即可。2.l
参数:var l = V.encrypt(pt.stringify(o), r.$_CCEB())``````V
这个对象和上面的U
差不多,复制出来即可。pt.stringify
这个函数也是固定的函数,复制出来即可。r.$_CCEB()
就是刚才的生成随机数的函数。3.o
参数这个参数是最复杂的一个部分。我们先看下这个参数是在哪里创建的。从这里我们已经可以确定一些字段了。- lang 是语言,我们可以直接设为zh-cn
- userresponse:H
是一个固定的函数,可以复制出来,t
具体是什么暂时不确定,i.challenge
看起来是接口返回的challenge
,我们一会儿可以确认一下。- passtime:验证的通过时间,来自函数入参n
- imgload:验证码图片的加载时间,搞个 30 - 100ms 的随机值即可- aa:暂时还不确定是什么,来自函数入参e
- ep:点进去是这样的里面的内容可以简单填写为这样:现在我们需要继续研究t
、e
、n
三个参数的生成逻辑,但是目前不好通过分析代码来获得更多内容了,所以我们根据刚才的 debugger 断点,去页面上具体研究。4.t
、e
、n
参数进入断点之后,我们可以看到变量的具体内容。先确定先找个函数的入参t e n
分别是数字,奇怪的字符串,数字。因为这三个值是函数的入参,所以我们根据调用栈,前往它的上层函数看看。同时这个函数具体的代码是这样的:对于passtime
对应的n
参数:我们根据代码可以确定,它就是整个滑动验证中,鼠标从摁下到抬起经过的时间。对于userresponse
对应的t
参数,也就是这里的u
参数,我们添加额外的 debugger,结合上述操作步骤,我们发现它其实是滑块的滑动距离,同时challenge
字段确实就是接口返回到的challenge
字段。对于aa
对应的e
参数,也就是这里的l
参数,我们通过观察代码,结合 debugger 数据可以发现,里面c
和s
是来自于接口的c
和s
字段,_BBES
是固定的函数,同样也是复制下来即可。不过里面的$_GFJ()
部分有些特殊,通过断点我们发现里面涉及到一个大数组。这个大数组其实是对鼠标拖动滑块的轨迹的记录。我们观察一下发现:- 大数组有多个小数组构成,小数组的定义是:[x, y, time]
- 第一行的 x y 是负数,并非从 0 开始,第二项所有的值均为 0- 每次记录的耗时大约在 10ms根据这个结论,我们大致实现一个函数来构造轨迹functiongenerateSlideTrace(distance:number){// 前两行按照刚才的观察来构造const trace =[[random(-50,-10),random(-50,-10),0],[0,0,0],]// 轨迹记录数量const count =30+ Math.floor(distance /2)// 耗时let t =random(50,100)// 记录上一个轨迹let lastX =0let lastY =0let lastYCount =0for(let i =0; i < count; i++){// 已滑动的距离const x = Math.round(i == count ?1:(1- Math.pow(2,(-10* i)/ count))* distance)// 耗时 t +=random(10,20)if(x === lastX){// 不合理continue} lastYCount +=1if(lastYCount >random(5,10)){// y 的变动不太大,连续多个 y 之后再考虑更新 lastYCount =0 lastY =random(-2,2)} lastX = x trace.push([x, lastY, t])}return trace}
- 合并
u
和h
至此,我们已经完成了所有的待破解元素。接下来把两个参数合并就是最后的w
参数了。整体的代码是这样的(变量名做了点优化):// 计算 w 参数,props 里是 c、s、trace(滑动轨迹数组)、challengeexportfunctiongetSlideW(props){returngetSlideLeft(props)+getRight()}functiongetSlideLeft(props){// encodeLeft 是扣出来的代码,getL 同理returnencodeLeft(getL(props))}functiongetL({ c, s, trace, challenge }){const o ={ lang:'zh-cn', userresponse:H(trace.at(-1)[0], challenge), passtime: trace.at(-1)[2], imgload: Math.floor(Math.random()*50+30),// getAA 是抠出来的代码,encodeTrace 同理 aa:getAA(encodeTrace(trace), c, s), ep:{ v:'7.8.8', $_BIQ:false, me:true, tm:-1, td:-1,},}// 都是抠出来的代码returnV.encrypt(stringify(o),getRandomText())}functiongetRight(){var e =newU().encrypt(getRandomText())while(!e ||256!== e.length) e =newU().encrypt(getRandomText(true))return e}functiongetRandomTextHelper(){consthelper=()=>((65536*(1+ Math.random()))|0).toString(16).substring(1)returnhelper()+helper()+helper()+helper()}var oldRandomText =getRandomTextHelper()functiongetRandomText(needRefresh){if(needRefresh){ oldRandomText =getRandomTextHelper()}return oldRandomText}// 下面的是从 slide 文件里抠出来的逻辑functionstringify(t, e, n){...}functionH(t, e){...}functiongetAA(t, e, n){...}functionencodeTrace(arr){...}functionencodeLeft(l){...}varU=...varV=...
构造请求
构造请求的过程其实和真是进行验证时的过程相同,按照上面对请求过程的分析,使用合适的参数发出对应的请求即可。
大致的顺序为:
ajax.php
->
get.php
-> 得到基本数据,计算参数 ->
ajax.php
-> 验证成功!
总结
这一节内容里,我们分析了网络请求和代码逻辑,理清了请求的顺序、作用,并得到了核心参数
w
的计算逻辑,最后成功根据参数通过了验证。
至此极验三代滑动验证码的分析已结束,之后我会更新其他验证方案的分析过程,感兴趣的朋友欢迎和我交流。
本期文章到这里就结束了,如果对您有帮助,记得收藏关注,有什么想法也可以联系我哦。
后续内容持续更新中。。。
版权归原作者 __aoko 所有, 如有侵权,请联系我们删除。