网易易盾滑动验证码破解(上篇)

网易易盾滑动验证码破解(上篇)

概述

最近简单研究了下网易易盾的滑动验证码,在这里记录下主要思想、步骤和方法等,供以后参考,我会分两篇文章来讲破解过程,此篇虽为上篇,但是其实主要涉及破解的后半部分,这主要与“逆向”一词有关,逆向都是从结果开始往前推敲,直到开头,所以为了更好的理解,我尽量保持与一开始的破解流程一致的方式来写这篇文章。

我是直接从网易易盾官网入手的,官网上有各种demo,滑动点击等类型,这里主要讲解滑动验证码,打开控制台,找到滑动验证码尝试滑动几次,可以看到这样几个请求:

1
2
3
4
5
6
7
8
9
10
11
# get 获取验证码图片
"https://c.dun.163.com/api/v2/get?id=07e2387ab53a4d6f930b8d9a9be71bdf&fp=K%5Cq%2FfnjqU5KJwQ28%2B0qfqpkTIAcv7b0a1Eej0w2wRhwLV5JSV1O1T6pOncRDChL8vqYVdei2%2Bytmu7qzX9Cy0y%5C9xwraR17YNuq1UDdifTQeoAU7otEGLw6GNxAi0guE4kr1rCK4LQwvdu0i8%2B5QGniLo8gojyO8kCHeY%5CfpMHjoYcpQ%3A1596095296574&https=true&type=2&version=2.14.0&dpr=1&dev=1&cb=Auexlrs9sfkqG%5CtYvrCLvgjhW8%2B71oZpoLjOj4Xp%2BhKNRA2FrYJFPS5PTdr0cQpq&ipv6=false&runEnv=10&group=&scene=&width=320&token=&referer=https%3A%2F%2Fdun.163.com%2Ftrial%2Fjigsaw&callback=__JSONP_1s17oyb_0"

# 获取ip地址
"https://only-d-xcu4ehhk0uvgoeui9hq68f8p2burt9pu-1596094399289.nstool.netease.com/ip.js"

# collect 收集信息
"https://c.dun.163.com/api/v2/collect?id=07e2387ab53a4d6f930b8d9a9be71bdf&token=&type=anticheat&target=&message=CaptchaError%3A%20600(request%20anticheat%20token%20error)%20-%20Cannot%20read%20property%20%27getToken%27%20of%20undefined%3BinitWatchman%3A%20undefined%3BWatchman%3A%20undefined%0A%20%20%20%20at%20c%20(https%3A%2F%2Fcstaticdun.126.net%2Fplugins.min.js%3Fv%3D26601572%3A4965%3A33)%0A%20%20%20%20at%20u%20(https%3A%2F%2Fcstaticdun.126.net%2Fplugins.min.js%3Fv%3D26601572%3A4979%3A21)%0A%20%20%20%20at%20https%3A%2F%2Fcstaticdun.126.net%2Fplugins.min.js%3Fv%3D26601572%3A4962%3A36&ip=114.217.233.155&dns=221.228.15.90&referer=https%3A%2F%2Fdun.163.com%2Ftrial%2Fjigsaw&callback=__JSONP_19wdqe1_2"

# check 验证
"https://c.dun.163.com/api/v2/check?id=07e2387ab53a4d6f930b8d9a9be71bdf&token=314d88b50517493ba08db7d17855d08a&acToken=9ca17ae2e6fecda16ae2e6eeb5cb528ab69db8ea65bcaeaf9ad05b9c94a3a3c434898987d2b25ef4b2a983bb2af0feacc3b92ae2f4ee95a132e29aa3b1cd72abae8cd1d44eb0b7bb82f55bb08fa3afd437fffeb3&data=%7B%22d%22%3A%22J%2B9%2B1BXHbeMZ0MMBsJ7V%2FpqgQ5YgWMZlqeohUja70cuVAawTWWDCWONcxx6jA9pp5cvZSXJ6K7xh%5C%5CGp7VKeWB8l2SFxX%2FO9leGRzTbNjKZ9VxOtQWazHBp0H6dbN0kNxZKpPLmo2Adi%2BtxspHrMzxRBtUdwfhEmlI0AIrmPR8B%5C%5ClfKEOoy5m85sCh8v%2FU6gwIk%2BbO7h%2Fux14R6ncvledZC9tWi4b6xdSs42hNA25j9oemhyU7eA8q4r70JrC2FYVKgeC%2B%2BGDxxv29hqpLGG1Si5L6%2B%5C%5C%2FV8K4oDhu0kp7lLFnr2swIc%5C%5CF4p2USyWrx7JEJA%5C%5CzYAZJPEkjHWB%2B5sdjBZSc52xqBuCvH%5C%5ChRJr0pSMNoEpyFwD2WUsK6hRBy5dI0x0pH6bsxoCnZbXL%5C%5CDGHQTiERokJA7yBcVh4GPaLwfdh8b1mMJi4HNyEOSF9%2F%2FRMfPm2YWuP%5C%5CbSyB4awPoAwC9rK5ZmYx%5C%5CL2BHyZZSUfuGpA6Yc6BIZOcWPkttd67Fd52VmuCBJCRSEMn0JJ%5C%5CJouorMsE%2B%5C%5Crmtenz6U%2FCicLQuk%2FAC1Yin%2FK5adtZKAbrbYbBmYshmNa5o88f0ChOoLH99rb65P08b%2F4EPArrTNzIPdCE0QOPmZzHIo17gzbAYRJ9%22%2C%22m%22%3A%22%22%2C%22p%22%3A%22opMMUcN%5C%5Cbb2jjMl2gIfnav%2BLou%2Bh2dFC%22%2C%22ext%22%3A%22%2B8op8C7fLNIwMIoNX9KANrvpG5eK6jsB%22%7D&width=320&type=2&version=2.14.0&cb=o1wR%2FcdQH4jG0sndfj9dkR5AlOpOBZ7%5C74VC0R%2BNzdF4ZwfNt%2Fwe2ElnqP%2F58mab&extraData=&runEnv=10&referer=https%3A%2F%2Fdun.163.com%2Ftrial%2Fjigsaw&callback=__JSONP_e3r81s2_1"

暂时还未发现中间两个请求的具体作用,最重要的就是get获取验证码和check验证,这两个请求了,get请求会返回给我们图片验证码的链接和token,check请求验证成功的话会返回给我们validate参数值,因为利用官网的这个demo我们可以直接从浏览器中获取到验证码和token,所以可以先将重点集中在check上,这篇文章主要就是介绍破解check请求,下篇里会讲如何获取验证码,计算缺口位置并模拟轨迹生成等。

入手

先来观察check请求,url=https://c.dun.163.com/api/v2/check?,参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
id: 07e2387ab53a4d6f930b8d9a9be71bdf
token: 314d88b50517493ba08db7d17855d08a
acToken: 9ca17ae2e6fecda16ae2e6eeb5cb528ab69db8ea65bcaeaf9ad05b9c94a3a3c434898987d2b25ef4b2a983bb2af0feacc3b92ae2f4ee95a132e29aa3b1cd72abae8cd1d44eb0b7bb82f55bb08fa3afd437fffeb3
data: {"d":"J+9+1BXHbeMZ0MMBsJ7V/pqgQ5YgWMZlqeohUja70cuVAawTWWDCWONcxx6jA9pp5cvZSXJ6K7xh\\Gp7VKeWB8l2SFxX/O9leGRzTbNjKZ9VxOtQWazHBp0H6dbN0kNxZKpPLmo2Adi+txspHrMzxRBtUdwfhEmlI0AIrmPR8B\\lfKEOoy5m85sCh8v/U6gwIk+bO7h/ux14R6ncvledZC9tWi4b6xdSs42hNA25j9oemhyU7eA8q4r70JrC2FYVKgeC++GDxxv29hqpLGG1Si5L6+\\/V8K4oDhu0kp7lLFnr2swIc\\F4p2USyWrx7JEJA\\zYAZJPEkjHWB+5sdjBZSc52xqBuCvH\\hRJr0pSMNoEpyFwD2WUsK6hRBy5dI0x0pH6bsxoCnZbXL\\DGHQTiERokJA7yBcVh4GPaLwfdh8b1mMJi4HNyEOSF9//RMfPm2YWuP\\bSyB4awPoAwC9rK5ZmYx\\L2BHyZZSUfuGpA6Yc6BIZOcWPkttd67Fd52VmuCBJCRSEMn0JJ\\JouorMsE+\\rmtenz6U/CicLQuk/AC1Yin/K5adtZKAbrbYbBmYshmNa5o88f0ChOoLH99rb65P08b/4EPArrTNzIPdCE0QOPmZzHIo17gzbAYRJ9","m":"","p":"opMMUcN\\bb2jjMl2gIfnav+Lou+h2dFC","ext":"+8op8C7fLNIwMIoNX9KANrvpG5eK6jsB"}
width: 320
type: 2
version: 2.14.0
cb: o1wR/cdQH4jG0sndfj9dkR5AlOpOBZ7\74VC0R+NzdF4ZwfNt/we2ElnqP/58mab
extraData:
runEnv: 10
referer: https://dun.163.com/trial/jigsaw
callback: __JSONP_e3r81s2_1

经过观察可以发现,只有tokendatacb参数是需要获取的,其他的都为定值,而token是获取验证码的时候获取的,所以目前主要破解datacb参数,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p($, {
id: w,
token: R,
acToken: I,
data: k,
width: O,
type: f,
version: j,
cb: s(),
extraData: a(S),
runEnv: E
}, X, {
onProcess: M(b, {
token: R
})
})

这里是check请求发送的地方,基本可以看到check请求的全部参数,但是在这里基本都已经生成好了,我们需要查看调用栈,把这些参数生成的过程给抠出来。

接下里我是定位到了3800和3900多行的这个位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
onMouseMove: function(e) {
var t = e.clientX
, n = e.clientY
, i = this.drag
, r = i.status
, s = i.beginTime
, l = i.startX;
if (i.status = s && t - l > 3 && "dragend" === r ? "dragstart" : r,
"dragend" !== i.status) {
!(e.type.indexOf("touch") !== -1 && o.supportPassive) && e.preventDefault(),
Object.assign(i, {
clientX: t,
clientY: n,
dragX: t - i.startX
});
var u = this.$store.state.token
, f = p(u, [Math.round(i.dragX < 0 ? 0 : i.dragX), Math.round(i.clientY - i.startY), a.now() - i.beginTime] + "");
this.traceData.push(f),
"dragstart" === i.status && this.onMouseMoveStart(e),
"dragging" === i.status && this.onMouseMoving(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
onMouseUp: function(e) {
var t = this.drag;
if ("dragend" === t.status)
return void Object.assign(t, {
beginTime: 0
});
Object.assign(t, w);
var n = a.sample(this.traceData, u)
, i = this.$store.state.token
, r = h(p(i, parseInt(this.$jigsaw.style.left, 10) / this.width * 100 + ""));
this.onVerifyCaptcha({
data: JSON.stringify({
d: h(n.join(":")),
m: "",
p: r,
ext: h(p(i, this.mouseDownCounts + "," + this.traceData.length))
})
})
},

这里this.traceData是加密后的轨迹数组,this.$store.state.token就是前面讲的tokenthis.$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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
g = function() {
return ["i", "/", "x", "1", "X", "g", "U", "0", "z", "7", "k", "8", "N", "+", "l", "C", "p", "O", "n", "P", "r", "v", "6", "\\", "q", "u", "2", "G", "j", "9", "H", "R", "c", "w", "T", "Y", "Z", "4", "b", "f", "S", "J", "B", "h", "a", "W", "s", "t", "A", "e", "o", "M", "I", "E", "Q", "5", "m", "D", "d", "V", "F", "L", "K", "y"]
}

b = function() {
return "3"
}

function m (e, t, n) {
var i, r, o, a = g(), s = b(), l = [];
if (1 == n)
i = e[t],
r = 0,
o = 0,
l.push(a[i >>> 2 & 63]),
l.push(a[(i << 4 & 48) + (r >>> 4 & 15)]),
l.push(s),
l.push(s);
else if (2 == n)
i = e[t],
r = e[t + 1],
o = 0,
l.push(a[i >>> 2 & 63]),
l.push(a[(i << 4 & 48) + (r >>> 4 & 15)]),
l.push(a[(r << 2 & 60) + (o >>> 6 & 3)]),
l.push(s);
else {
if (3 != n)
throw new Error("1010");
i = e[t],
r = e[t + 1],
o = e[t + 2],
l.push(a[i >>> 2 & 63]),
l.push(a[(i << 4 & 48) + (r >>> 4 & 15)]),
l.push(a[(r << 2 & 60) + (o >>> 6 & 3)]),
l.push(a[63 & o])
}
return l.join("")
}

function _(e) {
if (null == e || void 0 == e)
return null;
if (0 == e.length)
return "";
var t = 3;
try {
for (var n = [], i = 0; i < e.length; ) {
if (!(i + t <= e.length)) {
n.push(m(e, i, e.length - i));
break
}
n.push(m(e, i, t)),
i += t
}
return n.join("")
} catch (r) {
throw new Error("1010")
}
}

function p(e, t) {
function n(e, t) {
return e.charCodeAt(Math.floor(t % e.length))
}
function i(e, t) {
return t.split("").map(function(t, i) {
return t.charCodeAt(0) ^ n(e, i)
})
}
return t = i(e, t),
_(t)
}

这里要注意的是,一定在运行到当前环境再点击该函数跳转到函数定义的地方,否则跳转的可能并不是你真正要找的(比如恰巧当前环境有另一个名为p的方法),然后这里实际跳转过去的方法名并不叫”p”,而叫”n”,这里为了看起来更连贯,我将其命名为p。

接着我们就可以验证这个加密方法扣的对不对,在浏览器中滑动一次验证码,利用上面改写的js,我们在console中可以获取原始轨迹trace和加密后的轨迹trace_e,对trace调用我们抠出来的js对比和trace_e是否相同即可,代码大致如下:

1
2
3
4
5
6
7
8
9
var trace = [];  // 这里实际是你复制过来的原始轨迹
var traces_e = []; //加密后的轨迹数组
var token = "69d861aa4d4f4305a6f10eff124b6edf";
for (index in traces){
trace_e = p(token, traces[index]+""); // 转换为字符串
console.log(trace_e);
traces_e.push(trace_e);
}
console.log(traces_e);

这里token也要一起复制过来,验证过程自行实现。

data加密部分

接下来要扣data参数加密的js,这部分的js较多,需要较大的耐心并且要细心,这里入手的地方是OnMouseUp方法,这里调用的

js方法有a.samplehp等,a.sample是一个无依赖的方法,一下就可以找到:

1
2
3
4
5
6
7
8
9
function sample(e, t) {
var n = e.length;
if (n <= t)
return e;
for (var i = [], r = 0, o = 0; o < n; o++)
o >= r * (n - 1) / (t - 1) && (i.push(e[o]),
r += 1);
return i
}

然后其实这里的p方法就是上面轨迹加密部分的p方法,所以主要就集中在扣h方法上,同样地一定要在当前环境点击跳转该方法,然后会定位到一个名为B的方法:

1
2
3
4
5
6
7
8
9
10
11
12
// 最开始的h方法实际调用的是B方法,因为后面还有h方法,所以这里将方法命名为B
function B(e) {
var t = "14731382d816714fC59E47De5dA0C871D3F";
if (null == t || void 0 == t)
throw new Error("1008");
null != e && void 0 != e || (e = "");
var n = e + E(e)
, i = c(n)
, r = c(t)
, o = V(i, r);
return _(o)
}

这里B方法中调用了很多方法,并且其调用的方法还有调用一系列方法,把它们全部扣出来(自行尝试),大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
function f(e) {
if (null == e || 0 == e.length)
return [];
for (var t = new String(e), n = [], i = t.length / 2, r = 0, o = 0; o < i; o++) {
var a = parseInt(t.charAt(r++), 16) << 4
, s = parseInt(t.charAt(r++), 16);
n[o] = __toByte(a + s)
}
return n
}

function c(e) {
if (null == e || void 0 == e)
return e;
for (var t = encodeURIComponent(e), n = [], i = t.length, r = 0; r < i; r++)
if ("%" == t.charAt(r)) {
if (!(r + 2 < i))
throw new Error("1009");
n.push(f(t.charAt(++r) + "" + t.charAt(++r))[0])
} else
n.push(t.charCodeAt(r));
return n
}

function l(e) {
var t = [];
// s可能是定值
var s = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];
return t.push(s[e >>> 4 & 15]),
t.push(s[15 & e]),
t.join("")
}

function u(e) {
var t = e.length;
if (null == e || t < 0)
return new String("");
for (var n = [], i = 0; i < t; i++)
n.push(l(e[i]));
return n.join("")
}

function d(e) {
var t = j(e);
return u(t)
}

function S(e) {
var t = 4294967295;
// T可能是定值
var T = [0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270, 936918e3, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117];
if (null != e)
for (var n = 0; n < e.length; n++) {
var i = e[n];
t = t >>> 8 ^ T[255 & (t ^ i)]
}
return d(4294967295 ^ t, 8)
}

function E(e) {
return S(null == e ? [] : c(e))
}

/*##################### V 方法 ####################*/

// 这里有一些全局变量
var R = [120, 85, -95, -84, 122, 38, -16, -53, -11, 16, 55, 3, 125, -29, 32, -128, -94, 77, 15, 106, -88, -100, -34, 88, 78, 105, -104, -90, -70, 90, -119, -28, -19, -47, -111, 117, -105, -62, -35, 2, -14, -32, 114, 23, -21, 25, -7, -92, 96, -103, 126, 112, -113, -65, -109, -44, 47, 48, 86, 75, 62, -26, 72, -56, -27, 66, -42, 63, 14, 92, 59, -101, 19, -33, 12, -18, -126, -50, -67, 42, 7, -60, -81, -93, -86, 40, -69, -37, 98, -63, -59, 108, 46, -45, 93, 102, 65, -79, 73, -23, -46, 37, -114, -15, 44, -54, 99, -10, 60, -96, 76, 26, 61, -107, 18, -116, -55, -40, 57, -76, -82, 45, 0, -112, -77, 29, 43, -30, 109, -91, -83, 107, 101, 81, -52, -71, 84, 36, -41, 68, 39, -75, -122, -6, 11, -80, -17, -74, -73, 35, 49, -49, -127, 80, 103, 79, -25, 52, -43, 56, 41, -61, -24, 17, -118, 115, -38, 8, -78, 33, -85, -106, 58, -98, -108, 94, 116, -125, -51, -9, 71, 82, 87, -115, 9, 69, -123, 123, -117, 113, -22, -124, -87, 64, 13, 21, -89, -2, -99, -97, 1, -4, 34, 20, 83, 119, 30, -12, -110, -66, 118, -48, 6, -36, 104, -58, -102, 97, 5, -20, 31, -72, 70, -39, 67, -68, -57, 110, 89, 51, 10, -120, 28, 111, 127, 22, -3, 54, 53, -1, 100, 74, 50, 91, 27, -31, -5, -64, 124, -121, 24, -13, 95, 121, -8, 4];
var k = 4;
var C = 4;
var O = 4;
var I = 4;

__toByte = function (e) {
function t(t) {
return e.apply(this, arguments)
}
return t.toString = function() {
return e.toString()
}
,
t
}(function(e) {
if (e < -128)
return __toByte(128 - (-128 - e));
if (e >= -128 && e <= 127)
return e;
if (e > 127)
return __toByte(-129 + e - 127);
throw new Error("1001")
});

function N() {
for (var e = [], t = 0; t < I; t++) {
var n = 256 * Math.random();
n = Math.floor(n),
e[t] = __toByte(n)
}
return e
}

function y(e) {
for (var t = [], n = 0; n < e; n++)
t[n] = 0;
return t
}

function h(e, t, n) {
var i = [];
if (null == e || 0 == e.length)
return i;
if (e.length < n)
throw new Error("1003");
for (var r = 0; r < n; r++)
i[r] = e[t + r];
return i
}

function $(e) {
var t = [];
if (null == e || void 0 == e || 0 == e.length)
return y(C);
if (e.length >= C)
return h(e, 0, C);
for (var n = 0; n < C; n++)
t[n] = e[n % e.length];
return t
}

function o(e, t) {
return e = __toByte(e),
t = __toByte(t),
__toByte(e ^ t)
}

function a(e, t) {
if (null == e || null == t || e.length != t.length)
return e;
for (var n = [], i = e.length, r = 0, a = i; r < a; r++)
n[r] = o(e[r], t[r]);
return n
}

function j(e) {
var t = [];
return t[0] = e >>> 24 & 255,
t[1] = e >>> 16 & 255,
t[2] = e >>> 8 & 255,
t[3] = 255 & e,
t
}

function X(e) {
if (null == e || void 0 == e || 0 == e.length)
return y(k);
var t = e.length
, n = 0;
n = t % k <= k - O ? k - t % k - O : 2 * k - t % k - O;
var i = [];
p_new(e, 0, i, 0, t);
for (var r = 0; r < n; r++)
i[t + r] = 0;
var o = j(t);
return p_new(o, 0, i, t + n, O),
i
}

function x(e) {
if (null == e || e.length % k != 0)
throw new Error("1005");
for (var t = [], n = 0, i = e.length / k, r = 0; r < i; r++) {
t[r] = [];
for (var o = 0; o < k; o++)
t[r][o] = e[n++]
}
return t
}

function M(e, t) {
if (null == e)
return null;
for (var n = __toByte(t), i = [], r = e.length, a = 0; a < r; a++)
i.push(o(e[a], n));
return i
}

function i(e, t) {
return __toByte(e + t)
}

function D(e, t) {
if (null == e)
return null;
for (var n = __toByte(t), r = [], o = e.length, a = 0; a < o; a++)
r.push(i(e[a], n));
return r
}

function L(e) {
var t = M(e, 56)
, n = D(t, -40)
, i = M(n, 103);
return i
}

function r(e, t) {
if (null == e)
return null;
if (null == t)
return e;
for (var n = [], r = t.length, o = 0, a = e.length; o < a; o++)
n[o] = i(e[o], t[o % r]);
return n
}

function A(e) {
var t = e >>> 4 & 15
, n = 15 & e
, i = 16 * t + n;
return R[i]
}

function P(e) {
if (null == e)
return null;
for (var t = [], n = 0, i = e.length; n < i; n++)
t[n] = A(e[n]);
return t
}

function p_new(e, t, n, i, r) {
if (null == e || 0 == e.length)
return n;
if (null == n)
throw new Error("1004");
if (e.length < r)
throw new Error("1003");
for (var o = 0; o < r; o++)
n[i + o] = e[t + o];
return n
}

function V(e, t) {
null == e && (e = []);
var n = N();
t = $(t),
t = a(t, $(n)),
t = $(t);
var i = t
, o = X(e)
, s = x(o)
, l = [];
p_new(n, 0, l, 0, I);
for (var u = s.length, f = 0; f < u; f++) {
var c = L(s[f])
, j = a(c, t)
, d = r(j, i);
j = a(d, i);
var h = P(j);
h = P(h),
p_new(h, 0, l, f * k + I, k),
i = h
}
return l
}

/*################### h方法 ##################*/

// 最开始的h方法实际调用的是B方法,因为后面还有h方法,这里将方法命名为B
function B(e) {
var t = "14731382d816714fC59E47De5dA0C871D3F";
if (null == t || void 0 == t)
throw new Error("1008");
null != e && void 0 != e || (e = "");
var n = e + E(e)
, i = c(n)
, r = c(t)
, o = V(i, r);
return _(o)
}

上面还有一些变量是定值,一开始可能也不确定是定值,可以先标记,后面结果不对再看是否跟变量值有关。

然后有两点想强调下,算是坑,避免大家再踩:

  1. 这里面的层层调用中,不经意间又调用了一个p方法(我改写成了p_new,不过实际上其参数个数为5个,与我们上面扣的p方法的参数数量明显不同,有经验的或者细心的应该可以发现),但是对于新手来说,在扣这么多js的过程中已经有点头晕眼花,很难发现是另一个p,可能就以为是我们扣过的方法,这样在后面运行时会无限调用直到栈溢出(我就遇到了),所以即使你没发现,后面根据这个报错你再细心检查一遍应该可以发现问题,但是还是要提醒大家扣js的时候要注意方法同名的问题,找对正确的,否则后面真的挺难检查出来的。

  2. 他的原始js中都是使用的函数表达式,我为了适应自己的读写习惯,全部改成了函数声明的形式,但也就是这个操作,让我掉进了一个大坑(后面大家再做的时候,建议还是和原始的保持一致,原始的啥样扣出来就啥样,除非必要不做修改)。

    对于__toByte这个方法,使用函数声明得到的结果是不对的,而使用函数表达式就能验证通过,这里浪费了我好久好久的时间,一直找不到问题所在,要说最后是怎么发现的,我也讲不清,就是后来总觉得这个方法有问题,所以就尝试变动了下结果真的验证通过了,可是回过头来解释原因,好像也有点解释不通,函数声明相较于函数表达式有一个变量提升的效果,无论定义在哪里都能得到调用,而函数表达式只有在js运行到这一步骤,赋值完成后,该函数才能调用,但是到了这里感觉区别也不大呀,到底是为啥呢?

​ 这里后来我又去研究了一下,这里的__toByte是一个立即执行的方法,而被我改写成了函数声明的形式是无法立即执行的,从而就对后面的运行结果造成了影响。具体可以参考下这篇文章

cb参数

最后就剩cb参数的破解了,这个参数入手的地方是我们一开始说的7700多行的地方,在cb前面打个断点,然后点击跳转s()方法,如下:

1
2
3
4
function s() {
var e = $.uuid(32);
return A(e)
}

这里的$.uuid其实是一个方法,定位到方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uuid: function a(e, t) {
var n = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("")
, a = []
, i = void 0;
if (t = t || n.length,
e)
for (i = 0; i < e; i++)
a[i] = n[0 | Math.random() * t];
else {
var r = void 0;
for (a[8] = a[13] = a[18] = a[23] = "-",
a[14] = "4",
i = 0; i < 36; i++)
a[i] || (r = 0 | 16 * Math.random(),
a[i] = n[19 === i ? 3 & r | 8 : r])
}
return a.join("")
}

然后A方法其实是data参数那里的B方法,所以cb参数就结束了。

验证

到这里本篇文章的破解部分(也就是整个滑动验证码的后半段破解)已经完成了,下面可以来测试是否能够顺利通过check请求:

可以把前面轨迹加密、data参数加密和cb参数加密三个部分整合到一个js文件中,然后模拟OnMouseUpOnMouseMove方法中的步骤生成参数,然后通过编写python代码(用node或者其他语言写都可以)发送check请求。

参数生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var traces = []  // 这是从console中复制过来的原始轨迹
var token = ""; // 这里是复制的token
var trace_data_length = traces.length; //长度根据实际情况而定
var jigsaw_style_left = "136px"; // 这里是复制的鼠标滑动距离

var trace_data = []; //加密后的轨迹数组
for (index in traces){
data = p(token, traces[index]+""); // 转换为字符串
trace_data.push(data);
}
console.log(trace_data)

// 以下是可能为定值的变量
var width = 320;
var mouse_down_counts = 1;

var n = sample(trace_data, 50);
// 这里为了避免和方法同名冲突,改了变量名
var r_var = B(p(token, parseInt(jigsaw_style_left, 10) / width * 100 + ""));

var data = JSON.stringify({
d: B(n.join(":")),
m: "",
p: r_var,
ext: B(p(token, mouse_down_counts + "," + trace_data_length))
})

// 这里为了避免同名方法的影响,改变了方法名
cb = s_func();

console.log(data);
console.log(cb);

上面是根据原始的js进行的简单改写,只要在浏览器中手动滑动一次验证码,然后将原始轨迹数组traces和token和jigsaw_style_left复制到这里,执行完可以得到data参数和cb参数,然后再将token和data和cb复制到下面的python代码中,进行check请求。

后续补充,这里的width是写死的320,实际上有些网站的width并不一定是320,这里生成加密参数的时候可能需要同步修改,或者改成参数传入的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests
from urllib.parse import urlencode


class YiDun:

header = {
"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/"
"537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36"),
"Referer": "https://dun.163.com/trial/jigsaw",
"Host": "c.dun.163.com",
"Upgrade-Insecure-Requests": "1"
}

def check_captcha(self):
paras = {
"id": "07e2387ab53a4d6f930b8d9a9be71bdf", # CaptchaId
"token": "9fc71bcf92404ae89521e506b32e8494",
"acToken": ("9ca17ae2e6fecda16ae2e6eeb5cb528ab69db8ea65bcaeaf9ad05b9c94a3a3c434898987d2b25ef4b2a983bb2af0feacc3b92ae2f4ee95a132e29aa3b1cd72abae8cd1d44eb0b7bb82f55bb08fa3afd437fffeb3"), # 可能为定值
"data": "",
"width": "320",
"type": "2",
"version": "2.14.0",
"cb": "+LvN4mOHhAmFXjN9HYsawHrk8cnj54qtVXQW/1g856GLBEn0+O\k2joeAMINFe5o",
"extraData": "", # 可能为空
"runEnv": "10",
"referer": "https://dun.163.com/trial/jigsaw",
"callback": "__JSONP_9e3su76_1"
}
data = {"d":"wWsa9LuiB+h1gpTrVrEwVY5Gb4fuIQM10y+JdlQqXZO/8DLKDByKFI0n12AvMpISQ4UKSB8+MQnV252WOZSnzrqvt+pY8gcdUrmK5Cky2GtSfOzip\\OF1ojelyiqRWY9qZ4qi\\++T4t4KD7ZXd9bhqXXax0rb/t1MYsP52cDuYSf9EZrXKMJ0HTEmZXh9HkWGjtp62zpVZKw+XArESsxAmghDxxQTaFwNiCixFD5C+IrHAqkX9WDNlklXc//uuySkByKBkUM19EcswSdjVtqS4bIz88WT5SSkyrnKoOtG7+vHU4xcCVaugx52HYkVEd/L11REND97WP0PO2XgCkRkNh8MdkT0fUoAe\\6k1/+6WfrPnjt2fyLm8uVTW\\8QeL/00xyEN1Ja+OL\\zGre\\H9wisX+owWDHdThxdVlUn8DnchLjRu7UKfBU/myHihySJ5q8jRIf5ntKIJWV1gkKKUEiQxRmnaUViSp6jfScasX4\\1TDAdN/xygLpvrU5hWDhcWY4Dkk95/scPEYs/h5s05rX0XhtxQIcfUvBxhlw762kT0S8WAe6jlfY+uYOlPOXSn\\KJzcLHgWvotoE/OWzDE1x5drUi91RrPlZ6wkzCfVOMup52muWzPVTYXhpwJuN6","m":"","p":"n+FpUbCqcUubKrQ2+qLna9E0L7lQEFZC","ext":"ZQrtBOqLHQfHeMdyEk7A5VibLAQXd\\pr"}
paras.update({"data": data})
url = f"https://c.dun.163.com/api/v2/check?{urlencode(paras)}"
req = requests.get(url, headers=self.header)
print(req.text)


if __name__ == "__main__":
test = YiDun()
req = requests.get("https://dun.163.com/trial/jigsaw", headers=test.header)
test.check_captcha()

如果得到的返回结果是这样的

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,然后计算验证码缺口距离以及模拟轨迹生成等操作,这实际上属于破解的前半段。