摘要
分析验证码素材图片混淆原理,并采用selenium模拟人拖动滑块过程,进而破解验证码。
人工验证的过程
1、打开威锋网注册页面
2、移动鼠标至小滑块,一张完整的图片会出现(如下图1)
3、点击鼠标左键,图片中间会出现一个缺块(如下图2)
4、移动小滑块正上方图案至缺块处
5、验证通过
selenium模拟验证的过程
- 加载威锋网注册页面
- 下载图片1和缺块图片2
- 根据两张图片的差异计算平移的距离x
- 模拟鼠标点击事件,点击小滑块向右移动x
- 验证通过
- 详细分析
1、打开chrome浏览器控制台,会发现图1所示的验证码图片并不是极验后台返回的原图。而是由多个div拼接而成(如下图3)
通过图片显示div的style属性可知,极验后台把图片进行切割加错位处理。把素材图片切割成10 * 58大小的52张小图,再进行错位处理。在网页上显示的时候,再通过css的background-position属性对图片进行还原。以上的图1和图2都是经过了这种处理。在这种情况下,使用selenium模拟验证是需要对下载的验证码图片进行还原。如上图3的第一个div.gt_cut_fullbg_slice标签,它的大小为10px * 58px,其中style属性为background-image: url("http://static.geetest.com/pictures/gt/969ffa43c/969ffa43c.webp"); background-position: -157px -58px;会把该属性对应url的图片进行一个平移操作,以左上角为参考,向左平移157px,向上平移58px,图片超出部分不会显示。所以上图1所示图片是由26 * 2个10px * 58px大小的div组成(如下图4)。每一个小方块的大小58 * 10
2、下载图片并还原,上一步骤分析了图片具体的混淆逻辑,具体还原图片的代码实现如下,主要逻辑是把原图裁剪为52张小图,然后拼接成一张完整的图。
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
|
/** *还原图片 * @param type */ private static void restoreimage(string type) throws ioexception { //把图片裁剪为2 * 26份 for ( int i = 0 ; i < 52 ; i++){ cutpic(basepath + type + ".jpg" ,basepath + "result/" + type + i + ".jpg" , -movearray[i][ 0 ], -movearray[i][ 1 ], 10 , 58 ); } //拼接图片 string[] b = new string[ 26 ]; for ( int i = 0 ; i < 26 ; i++){ b[i] = string.format(basepath + "result/" + type + "%d.jpg" , i); } mergeimage(b, 1 , basepath + "result/" + type + "result1.jpg" ); //拼接图片 string[] c = new string[ 26 ]; for ( int i = 0 ; i < 26 ; i++){ c[i] = string.format(basepath + "result/" + type + "%d.jpg" , i + 26 ); } mergeimage(c, 1 , basepath + "result/" + type + "result2.jpg" ); mergeimage( new string[]{basepath + "result/" + type + "result1.jpg" , basepath + "result/" + type + "result2.jpg" }, 2 , basepath + "result/" + type + "result3.jpg" ); //删除产生的中间图片 for ( int i = 0 ; i < 52 ; i++){ new file(basepath + "result/" + type + i + ".jpg" ).deleteonexit(); } new file(basepath + "result/" + type + "result1.jpg" ).deleteonexit(); new file(basepath + "result/" + type + "result2.jpg" ).deleteonexit(); } |
还原过程需要注意的是,后台返回错位的图片是312 * 116大小的。而网页上图片div的大小是260 * 116。
3、计算平移距离,遍历图片的每一个像素点,当两张图的r、g、b之差的和大于255,说明该点的差异过大,很有可能就是需要平移到该位置的那个点,代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
bufferedimage fullbi = imageio.read( new file(basepath + "result/" + full_image_name + "result3.jpg" )); bufferedimage bgbi = imageio.read( new file(basepath + "result/" + bg_image_name + "result3.jpg" )); for ( int i = 0 ; i < bgbi.getwidth(); i++){ for ( int j = 0 ; j < bgbi.getheight(); j++) { int [] fullrgb = new int [ 3 ]; fullrgb[ 0 ] = (fullbi.getrgb(i, j) & 0xff0000 ) >> 16 ; fullrgb[ 1 ] = (fullbi.getrgb(i, j) & 0xff00 ) >> 8 ; fullrgb[ 2 ] = (fullbi.getrgb(i, j) & 0xff ); int [] bgrgb = new int [ 3 ]; bgrgb[ 0 ] = (bgbi.getrgb(i, j) & 0xff0000 ) >> 16 ; bgrgb[ 1 ] = (bgbi.getrgb(i, j) & 0xff00 ) >> 8 ; bgrgb[ 2 ] = (bgbi.getrgb(i, j) & 0xff ); if (difference(fullrgb, bgrgb) > 255 ){ return i; } } } |
4、模拟鼠标移动事件,这一步骤是最关键的步骤,极验验证码后台正是通过移动滑块的轨迹来判断是否为机器所为。整个移动轨迹的过程越随机越好,我这里提供一种成功率较高的移动算法,代码如下。
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
|
public static void move(webdriver driver, webelement element, int distance) throws interruptedexception { int xdis = distance + 11 ; system.out.println( "应平移距离:" + xdis); int movex = new random().nextint( 8 ) - 5 ; int movey = 1 ; actions actions = new actions(driver); new actions(driver).clickandhold(element).perform(); thread.sleep( 200 ); printlocation(element); actions.movetoelement(element, movex, movey).perform(); system.out.println(movex + "--" + movey); printlocation(element); for ( int i = 0 ; i < 22 ; i++){ int s = 10 ; if (i % 2 == 0 ){ s = - 10 ; } actions.movetoelement(element, s, 1 ).perform(); printlocation(element); thread.sleep( new random().nextint( 100 ) + 150 ); } system.out.println(xdis + "--" + 1 ); actions.movebyoffset(xdis, 1 ).perform(); printlocation(element); thread.sleep( 200 ); actions.release(element).perform(); } |
完整代码如下
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
|
package com.github.wycm; import org.apache.commons.io.fileutils; import org.jsoup.jsoup; import org.jsoup.nodes.document; import org.jsoup.nodes.element; import org.jsoup.select.elements; import org.openqa.selenium.by; import org.openqa.selenium.point; import org.openqa.selenium.webdriver; import org.openqa.selenium.webelement; import org.openqa.selenium.chrome.chromedriver; import org.openqa.selenium.interactions.actions; import org.openqa.selenium.support.ui.expectedcondition; import org.openqa.selenium.support.ui.webdriverwait; import javax.imageio.imageio; import javax.imageio.imagereadparam; import javax.imageio.imagereader; import javax.imageio.stream.imageinputstream; import java.awt.*; import java.awt.image.bufferedimage; import java.io.file; import java.io.fileinputstream; import java.io.ioexception; import java.net.url; import java.util.iterator; import java.util.random; import java.util.regex.matcher; import java.util.regex.pattern; public class geettestcrawler { private static string basepath = "src/main/resources/" ; private static string full_image_name = "full-image" ; private static string bg_image_name = "bg-image" ; private static int [][] movearray = new int [ 52 ][ 2 ]; private static boolean movearrayinit = false ; private static string index_url = "https://passport.feng.com/?r=user/register" ; private static webdriver driver; static { system.setproperty( "webdriver.chrome.driver" , "d:/dev/selenium/chromedriver_v2.30/chromedriver_win32/chromedriver.exe" ); if (!system.getproperty( "os.name" ).tolowercase().contains( "windows" )){ system.setproperty( "webdriver.chrome.driver" , "/users/wangyang/workspace/selenium/chromedriver_v2.30/chromedriver" ); } driver = new chromedriver(); } public static void main(string[] args) throws interruptedexception { for ( int i = 0 ; i < 10 ; i++){ try { invoke(); } catch (ioexception e) { e.printstacktrace(); } catch (interruptedexception e) { e.printstacktrace(); } } driver.quit(); } private static void invoke() throws ioexception, interruptedexception { //设置input参数 driver.get(index_url); //通过[class=gt_slider_knob gt_show] by movebtn = by.cssselector( ".gt_slider_knob.gt_show" ); waitforload(driver, movebtn); webelement moveelemet = driver.findelement(movebtn); int i = 0 ; while (i++ < 15 ){ int distance = getmovedistance(driver); move(driver, moveelemet, distance - 6 ); by gttypeby = by.cssselector( ".gt_info_type" ); by gtinfoby = by.cssselector( ".gt_info_content" ); waitforload(driver, gttypeby); waitforload(driver, gtinfoby); string gttype = driver.findelement(gttypeby).gettext(); string gtinfo = driver.findelement(gtinfoby).gettext(); system.out.println(gttype + "---" + gtinfo); /** * 再来一次: * 验证失败: */ if (!gttype.equals( "再来一次:" ) && !gttype.equals( "验证失败:" )){ thread.sleep( 4000 ); system.out.println(driver); break ; } thread.sleep( 4000 ); } } /** * 移动 * @param driver * @param element * @param distance * @throws interruptedexception */ public static void move(webdriver driver, webelement element, int distance) throws interruptedexception { int xdis = distance + 11 ; system.out.println( "应平移距离:" + xdis); int movex = new random().nextint( 8 ) - 5 ; int movey = 1 ; actions actions = new actions(driver); new actions(driver).clickandhold(element).perform(); thread.sleep( 200 ); printlocation(element); actions.movetoelement(element, movex, movey).perform(); system.out.println(movex + "--" + movey); printlocation(element); for ( int i = 0 ; i < 22 ; i++){ int s = 10 ; if (i % 2 == 0 ){ s = - 10 ; } actions.movetoelement(element, s, 1 ).perform(); // printlocation(element); thread.sleep( new random().nextint( 100 ) + 150 ); } system.out.println(xdis + "--" + 1 ); actions.movebyoffset(xdis, 1 ).perform(); printlocation(element); thread.sleep( 200 ); actions.release(element).perform(); } private static void printlocation(webelement element){ point point = element.getlocation(); system.out.println(point.tostring()); } /** * 等待元素加载,10s超时 * @param driver * @param by */ public static void waitforload( final webdriver driver, final by by){ new webdriverwait(driver, 10 ).until( new expectedcondition< boolean >() { public boolean apply(webdriver d) { webelement element = driver.findelement(by); if (element != null ){ return true ; } return false ; } }); } /** * 计算需要平移的距离 * @param driver * @return * @throws ioexception */ public static int getmovedistance(webdriver driver) throws ioexception { string pagesource = driver.getpagesource(); string fullimageurl = getfullimageurl(pagesource); fileutils.copyurltofile( new url(fullimageurl), new file(basepath + full_image_name + ".jpg" )); string getbgimageurl = getbgimageurl(pagesource); fileutils.copyurltofile( new url(getbgimageurl), new file(basepath + bg_image_name + ".jpg" )); initmovearray(driver); restoreimage(full_image_name); restoreimage(bg_image_name); bufferedimage fullbi = imageio.read( new file(basepath + "result/" + full_image_name + "result3.jpg" )); bufferedimage bgbi = imageio.read( new file(basepath + "result/" + bg_image_name + "result3.jpg" )); for ( int i = 0 ; i < bgbi.getwidth(); i++){ for ( int j = 0 ; j < bgbi.getheight(); j++) { int [] fullrgb = new int [ 3 ]; fullrgb[ 0 ] = (fullbi.getrgb(i, j) & 0xff0000 ) >> 16 ; fullrgb[ 1 ] = (fullbi.getrgb(i, j) & 0xff00 ) >> 8 ; fullrgb[ 2 ] = (fullbi.getrgb(i, j) & 0xff ); int [] bgrgb = new int [ 3 ]; bgrgb[ 0 ] = (bgbi.getrgb(i, j) & 0xff0000 ) >> 16 ; bgrgb[ 1 ] = (bgbi.getrgb(i, j) & 0xff00 ) >> 8 ; bgrgb[ 2 ] = (bgbi.getrgb(i, j) & 0xff ); if (difference(fullrgb, bgrgb) > 255 ){ return i; } } } throw new runtimeexception( "未找到需要平移的位置" ); } private static int difference( int [] a, int [] b){ return math.abs(a[ 0 ] - b[ 0 ]) + math.abs(a[ 1 ] - b[ 1 ]) + math.abs(a[ 2 ] - b[ 2 ]); } /** * 获取move数组 * @param driver */ private static void initmovearray(webdriver driver){ if (movearrayinit){ return ; } document document = jsoup.parse(driver.getpagesource()); elements elements = document.select( "[class=gt_cut_bg gt_show]" ).first().children(); int i = 0 ; for (element element : elements){ pattern pattern = pattern.compile( ".*background-position: (.*?)px (.*?)px.*" ); matcher matcher = pattern.matcher(element.tostring()); if (matcher.find()){ string width = matcher.group( 1 ); string height = matcher.group( 2 ); movearray[i][ 0 ] = integer.parseint(width); movearray[i++][ 1 ] = integer.parseint(height); } else { throw new runtimeexception( "解析异常" ); } } movearrayinit = true ; } /** *还原图片 * @param type */ private static void restoreimage(string type) throws ioexception { //把图片裁剪为2 * 26份 for ( int i = 0 ; i < 52 ; i++){ cutpic(basepath + type + ".jpg" ,basepath + "result/" + type + i + ".jpg" , -movearray[i][ 0 ], -movearray[i][ 1 ], 10 , 58 ); } //拼接图片 string[] b = new string[ 26 ]; for ( int i = 0 ; i < 26 ; i++){ b[i] = string.format(basepath + "result/" + type + "%d.jpg" , i); } mergeimage(b, 1 , basepath + "result/" + type + "result1.jpg" ); //拼接图片 string[] c = new string[ 26 ]; for ( int i = 0 ; i < 26 ; i++){ c[i] = string.format(basepath + "result/" + type + "%d.jpg" , i + 26 ); } mergeimage(c, 1 , basepath + "result/" + type + "result2.jpg" ); mergeimage( new string[]{basepath + "result/" + type + "result1.jpg" , basepath + "result/" + type + "result2.jpg" }, 2 , basepath + "result/" + type + "result3.jpg" ); //删除产生的中间图片 for ( int i = 0 ; i < 52 ; i++){ new file(basepath + "result/" + type + i + ".jpg" ).deleteonexit(); } new file(basepath + "result/" + type + "result1.jpg" ).deleteonexit(); new file(basepath + "result/" + type + "result2.jpg" ).deleteonexit(); } /** * 获取原始图url * @param pagesource * @return */ private static string getfullimageurl(string pagesource){ string url = null ; document document = jsoup.parse(pagesource); string style = document.select( "[class=gt_cut_fullbg_slice]" ).first().attr( "style" ); pattern pattern = pattern.compile( "url\\(\"(.*)\"\\)" ); matcher matcher = pattern.matcher(style); if (matcher.find()){ url = matcher.group( 1 ); } url = url.replace( ".webp" , ".jpg" ); system.out.println(url); return url; } /** * 获取带背景的url * @param pagesource * @return */ private static string getbgimageurl(string pagesource){ string url = null ; document document = jsoup.parse(pagesource); string style = document.select( ".gt_cut_bg_slice" ).first().attr( "style" ); pattern pattern = pattern.compile( "url\\(\"(.*)\"\\)" ); matcher matcher = pattern.matcher(style); if (matcher.find()){ url = matcher.group( 1 ); } url = url.replace( ".webp" , ".jpg" ); system.out.println(url); return url; } public static boolean cutpic(string srcfile, string outfile, int x, int y, int width, int height) { fileinputstream is = null ; imageinputstream iis = null ; try { if (! new file(srcfile).exists()) { return false ; } is = new fileinputstream(srcfile); string ext = srcfile.substring(srcfile.lastindexof( "." ) + 1 ); iterator<imagereader> it = imageio.getimagereadersbyformatname(ext); imagereader reader = it.next(); iis = imageio.createimageinputstream(is); reader.setinput(iis, true ); imagereadparam param = reader.getdefaultreadparam(); rectangle rect = new rectangle(x, y, width, height); param.setsourceregion(rect); bufferedimage bi = reader.read( 0 , param); file tempoutfile = new file(outfile); if (!tempoutfile.exists()) { tempoutfile.mkdirs(); } imageio.write(bi, ext, new file(outfile)); return true ; } catch (exception e) { e.printstacktrace(); return false ; } finally { try { if (is != null ) { is.close(); } if (iis != null ) { iis.close(); } } catch (ioexception e) { e.printstacktrace(); return false ; } } } /** * 图片拼接 (注意:必须两张图片长宽一致哦) * @param files 要拼接的文件列表 * @param type 1横向拼接,2 纵向拼接 * @param targetfile 输出文件 */ private static void mergeimage(string[] files, int type, string targetfile) { int length = files.length; file[] src = new file[length]; bufferedimage[] images = new bufferedimage[length]; int [][] imagearrays = new int [length][]; for ( int i = 0 ; i < length; i++) { try { src[i] = new file(files[i]); images[i] = imageio.read(src[i]); } catch (exception e) { throw new runtimeexception(e); } int width = images[i].getwidth(); int height = images[i].getheight(); imagearrays[i] = new int [width * height]; imagearrays[i] = images[i].getrgb( 0 , 0 , width, height, imagearrays[i], 0 , width); } int newheight = 0 ; int newwidth = 0 ; for ( int i = 0 ; i < images.length; i++) { // 横向 if (type == 1 ) { newheight = newheight > images[i].getheight() ? newheight : images[i].getheight(); newwidth += images[i].getwidth(); } else if (type == 2 ) { // 纵向 newwidth = newwidth > images[i].getwidth() ? newwidth : images[i].getwidth(); newheight += images[i].getheight(); } } if (type == 1 && newwidth < 1 ) { return ; } if (type == 2 && newheight < 1 ) { return ; } // 生成新图片 try { bufferedimage imagenew = new bufferedimage(newwidth, newheight, bufferedimage.type_int_rgb); int height_i = 0 ; int width_i = 0 ; for ( int i = 0 ; i < images.length; i++) { if (type == 1 ) { imagenew.setrgb(width_i, 0 , images[i].getwidth(), newheight, imagearrays[i], 0 , images[i].getwidth()); width_i += images[i].getwidth(); } else if (type == 2 ) { imagenew.setrgb( 0 , height_i, newwidth, images[i].getheight(), imagearrays[i], 0 , newwidth); height_i += images[i].getheight(); } } //输出想要的图片 imageio.write(imagenew, targetfile.split( "\\." )[ 1 ], new file(targetfile)); } catch (exception e) { throw new runtimeexception(e); } } } |
pom文件依赖如下
1
2
3
4
5
6
7
8
9
10
11
|
<dependency> <groupid>org.seleniumhq.selenium</groupid> <artifactid>selenium-server</artifactid> <version> 3.0 . 1 </version> </dependency> <!-- https: //mvnrepository.com/artifact/org.jsoup/jsoup --> <dependency> <groupid>org.jsoup</groupid> <artifactid>jsoup</artifactid> <version> 1.7 . 2 </version> </dependency> |
最后
完整代码已上传至github,地址:https://github.com/wycm/selenium-geetest-crack
附上一张滑动效果图
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://www.jianshu.com/p/1466f1ba3275