网易易盾滑动验证码破解(上篇)
概述
最近简单研究了下网易易盾的滑动验证码,在这里记录下主要思想、步骤和方法等,供以后参考,我会分两篇文章来讲破解过程,此篇虽为上篇,但是其实主要涉及破解的后半部分,这主要与“逆向”一词有关,逆向都是从结果开始往前推敲,直到开头,所以为了更好的理解,我尽量保持与一开始的破解流程一致的方式来写这篇文章。
我是直接从网易易盾官网入手的,官网上有各种demo,滑动点击等类型,这里主要讲解滑动验证码,打开控制台,找到滑动验证码尝试滑动几次,可以看到这样几个请求:
1 | # get 获取验证码图片 |
暂时还未发现中间两个请求的具体作用,最重要的就是get获取验证码和check验证,这两个请求了,get请求会返回给我们图片验证码的链接和token
,check请求验证成功的话会返回给我们validate
参数值,因为利用官网的这个demo我们可以直接从浏览器中获取到验证码和token
,所以可以先将重点集中在check
上,这篇文章主要就是介绍破解check
请求,下篇里会讲如何获取验证码,计算缺口位置并模拟轨迹生成等。
入手
先来观察check
请求,url=https://c.dun.163.com/api/v2/check?
,参数如下:
1 | id: 07e2387ab53a4d6f930b8d9a9be71bdf |
经过观察可以发现,只有token
、data
和cb
参数是需要获取的,其他的都为定值,而token
是获取验证码的时候获取的,所以目前主要破解data
和cb
参数,id是定值,但是不同的网站之间是不同的。
后来补充:其实acToken
并不是定值,只是每次生成的值很像,所以会使人误认为定值,且acToken
的破解也很重要,这部分主要与watchman文件有关,后续的文章会有对这部分内容的介绍。
data参数
通过在Network中点击check
请求的调用栈,我们可以定位到这样一个js
文件https://cstaticdun.126.net/2.14.0/core.v2.14.0.min.js?v=2660198
。(这里有点坑”v”后面的数字每次刷新可能会变,会给我们断点调试带来麻烦,基本上你加了断点,下次刷新断点就都没了,后面会讲如何来解决这个问题)
我一开始破解的时候还是v2.14.0
,写这篇文章的时候已经是v2.14.1
了,所以如果后面我写混了不要奇怪
我是一开始定位到了7700多行的这样一个地方(这里的行号不是确定的,说不定网易会维护一下,加点东西干啥的行号就变了,但是大概的位置应该是确定的,而且因为这里进行一些总结记录,不会写的非常详细如何定位到这里等操作):
1 | p($, { |
这里是check请求发送的地方,基本可以看到check请求的全部参数,但是在这里基本都已经生成好了,我们需要查看调用栈,把这些参数生成的过程给抠出来。
接下里我是定位到了3800和3900多行的这个位置:
1 | onMouseMove: function(e) { |
这里u是前面讲的token,[Math.round(i.dragX < 0 ? 0 : i.dragX), Math.round(i.clientY - i.startY), a.now() - i.beginTime]
这个轨迹数组中的单个值,然后经过p方法加密得到f,然后放进加密数组this.traceData
里。
1 | onMouseUp: function(e) { |
这里this.traceData
是加密后的轨迹数组,this.$store.state.token
就是前面讲的token
,this.$jigsaw.style.left
是鼠标滑动的距离……
后来补充,this.$jigsaw.style.left
不能确切的说是鼠标滑动的距离,这个left的值与轨迹的最大值以及验证码图片缺口的距离,是三个独立的值,具体有着怎样的关系,不同的网站可能不同
其实对于新手来说,快速定位到这里并不容易,我自己也是调试了很久才能准确定位,不过只要你耐心找一定是可以找到的(如果你在自己练习,建议自己调试来找,不要直接看我写的行数来快速定位)。这里就是data参数生成的地方,通过我自己前面多次调试摸索,发现这里是在鼠标抬起时,计算了鼠标滑动的轨迹,然后先对轨迹进行了加密,接着又调用了一系列加密函数,生成了data参数。
为了这里调试方便,并且我们要获取到轨迹数据等,这里先讲一个利用Fiddler进行js
文件替换的操作。先把这个js
文件的内容全部复制下来,然后本地新建一个js
文件,然后加上一些全局变量和断点,如下:
接着定义origin临时变量保存原始轨迹,然后放到一开始定义的全局变量trace里面,把加密后的放到trace_e里面。(这样做目的是,当我们在浏览器中滑动过验证码之后,在console中输入这两个变量名,可以看到原始和加密后的轨迹数组值)
同样的,我们还要获取鼠标滑动的距离值(上面定义的全局变量jigsaw_style_left)
完成上面的js
改写后,要将它替换浏览器中的,这时候要用到Fiddler,下面是我在网上找的一张图,介绍了如何使用Fiddler来替换js
:
这里需要注意的一点是,因为前面讲的”v”后面数字会变,所以在规则里不能完全写该文件名称,这样写即可:”https://cstaticdun.126.net/2.14.1/core.v2.14.1.min.js?v="。
然后刷新网页,会在你写了debugger
的地方断住,类似这样:
然后你就可以在你想打断点的地方打上断点,然后操作就可以了。(注意这里利用Fiddler一举两得,一是解决断点调试困难的问题;二是可以获取轨迹等数据)
轨迹加密部分
前面准备工作完成后,下面就是扣js
的部分了,先从轨迹加密函数入手,这里也比较简单,上面讲了OnMouseMove
方法中,对轨迹进行了加密,所以我们从这里入手就好了,主要就是这一步:
1 | f = p(u, [Math.round(i.dragX < 0 ? 0 : i.dragX), Math.round(i.clientY - i.startY), a.now() - i.beginTime] + "") |
把断点打到这个位置,然后将网页运行到这个地方,点击跳转到p方法的位置,然后再把p方法中一系列调用的东西也给扣出来就好了,扣出来的代码大概是这样的:
1 | g = function() { |
这里要注意的是,一定在运行到当前环境再点击该函数跳转到函数定义的地方,否则跳转的可能并不是你真正要找的(比如恰巧当前环境有另一个名为p的方法),然后这里实际跳转过去的方法名并不叫”p”,而叫”n”,这里为了看起来更连贯,我将其命名为p。
接着我们就可以验证这个加密方法扣的对不对,在浏览器中滑动一次验证码,利用上面改写的js
,我们在console中可以获取原始轨迹trace
和加密后的轨迹trace_e
,对trace
调用我们抠出来的js
对比和trace_e
是否相同即可,代码大致如下:
1 | var trace = []; // 这里实际是你复制过来的原始轨迹 |
这里token也要一起复制过来,验证过程自行实现。
data加密部分
接下来要扣data参数加密的js
,这部分的js
较多,需要较大的耐心并且要细心,这里入手的地方是OnMouseUp
方法,这里调用的
js
方法有a.sample
、h
、p
等,a.sample
是一个无依赖的方法,一下就可以找到:
1 | function sample(e, t) { |
然后其实这里的p方法就是上面轨迹加密部分的p方法,所以主要就集中在扣h
方法上,同样地一定要在当前环境点击跳转该方法,然后会定位到一个名为B的方法:
1 | // 最开始的h方法实际调用的是B方法,因为后面还有h方法,所以这里将方法命名为B |
这里B方法中调用了很多方法,并且其调用的方法还有调用一系列方法,把它们全部扣出来(自行尝试),大致如下:
1 | function f(e) { |
上面还有一些变量是定值,一开始可能也不确定是定值,可以先标记,后面结果不对再看是否跟变量值有关。
然后有两点想强调下,算是坑,避免大家再踩:
这里面的层层调用中,不经意间又调用了一个p方法(我改写成了p_new,不过实际上其参数个数为5个,与我们上面扣的p方法的参数数量明显不同,有经验的或者细心的应该可以发现),但是对于新手来说,在扣这么多
js
的过程中已经有点头晕眼花,很难发现是另一个p,可能就以为是我们扣过的方法,这样在后面运行时会无限调用直到栈溢出(我就遇到了),所以即使你没发现,后面根据这个报错你再细心检查一遍应该可以发现问题,但是还是要提醒大家扣js
的时候要注意方法同名的问题,找对正确的,否则后面真的挺难检查出来的。他的原始
js
中都是使用的函数表达式,我为了适应自己的读写习惯,全部改成了函数声明的形式,但也就是这个操作,让我掉进了一个大坑(后面大家再做的时候,建议还是和原始的保持一致,原始的啥样扣出来就啥样,除非必要不做修改)。对于
__toByte
这个方法,使用函数声明得到的结果是不对的,而使用函数表达式就能验证通过,这里浪费了我好久好久的时间,一直找不到问题所在,要说最后是怎么发现的,我也讲不清,就是后来总觉得这个方法有问题,所以就尝试变动了下结果真的验证通过了,可是回过头来解释原因,好像也有点解释不通,函数声明相较于函数表达式有一个变量提升的效果,无论定义在哪里都能得到调用,而函数表达式只有在js
运行到这一步骤,赋值完成后,该函数才能调用,但是到了这里感觉区别也不大呀,到底是为啥呢?
这里后来我又去研究了一下,这里的__toByte
是一个立即执行的方法,而被我改写成了函数声明的形式是无法立即执行的,从而就对后面的运行结果造成了影响。具体可以参考下这篇文章。
cb
参数
最后就剩cb
参数的破解了,这个参数入手的地方是我们一开始说的7700多行的地方,在cb
前面打个断点,然后点击跳转s()方法,如下:
1 | function s() { |
这里的$.uuid其实是一个方法,定位到方法如下:
1 | uuid: function a(e, t) { |
然后A方法其实是data参数那里的B方法,所以cb参数就结束了。
验证
到这里本篇文章的破解部分(也就是整个滑动验证码的后半段破解)已经完成了,下面可以来测试是否能够顺利通过check
请求:
可以把前面轨迹加密、data参数加密和cb
参数加密三个部分整合到一个js
文件中,然后模拟OnMouseUp
和OnMouseMove
方法中的步骤生成参数,然后通过编写python代码(用node或者其他语言写都可以)发送check请求。
参数生成代码:
1 | var traces = [] // 这是从console中复制过来的原始轨迹 |
上面是根据原始的js
进行的简单改写,只要在浏览器中手动滑动一次验证码,然后将原始轨迹数组traces和token和jigsaw_style_left复制到这里,执行完可以得到data参数和cb
参数,然后再将token和data和cb
复制到下面的python代码中,进行check请求。
后续补充,这里的width是写死的320,实际上有些网站的width并不一定是320,这里生成加密参数的时候可能需要同步修改,或者改成参数传入的形式
1 | import requests |
如果得到的返回结果是这样的
1 | __JSONP_9e3su76_1({"data":{"result":false,"token":"4b08b0d51e5c44218df7a2e3d56d8be3","validate":""},"error":0,"msg":"ok"}); |
表示你扣的js
应该没问题,但是本次滑动验证失败了,有可能是轨迹和滑动距离之类的计算结果有问题;如果返回的结果并没有validate
这个关键字,那一般是扣的js
有问题;如果顺利通过验证,得到的结果应该是这样的:
1 | __JSONP_9e3su76_1({"data":{"result":true,"token":"9fc71bcf92404ae89521e506b32e8494","validate":"8gxMgwjCs36ABQyKiWXApPYj4qmeePZtNacdk+33/pWyyVk9xmp6Fe6etQ75HC/G/p4SqUksSPlt8+SstaDC3Eve43eTujSRtRjRkjwxQHoNGZdILEsbu7ScGEXhQOH3HGV6AdOA7QjyD6qFh68WpShmADgR1FFQsvYL+D3jJHA="},"error":0,"msg":"ok"}); |
总结
看完这篇文章,我所谓的破解后半段的意思想必你应该了解了,这里的token
、轨迹和滑动距离是我们从浏览器中通过改写js
手动获取的,拿到这些我们可以自行请求通过验证,下篇文章中我们会将如何获取验证码和token
,然后计算验证码缺口距离以及模拟轨迹生成等操作,这实际上属于破解的前半段。