完整源码和图片在 Github 上 https://github.com/omigo/crack-slide-captcha 一、背景 二、思路 计算滑块移动距离 模拟移动 三、实战 Go 服务 js 脚本 四、体验 五、思考 参考 一、背景 滑块验证码是一项人机识别技术,操作简单,真人体验好,机器识别效果也不差,可以有效防止脚本做任务,增加机器脚本薅羊毛的难度。但其破解也相对简单,这里演示一个Demo,通过 OpenCV 匹配找出滑块位置,计算出滑动距离,然后模拟 js 鼠标事件,在 Chrome 控制台执行脚本,完成破解动作。 二、思路 先看一下效果: 滑块验证码主要有三个元素组成:带缺口的背景大图(bg)、缺口小块(block)、滑块(slide)。 目标是把缺口移动到对应的位置,也就是说要解决两个问题:1.滑块移动距离;2.模拟移动。 计算滑块移动距离 OpenCV 有模板匹配方法,可以通过多种算法找到小图(即模板)在大图中的位置。 1. OpenCV 读取 带缺口的背景大图(bg)、缺口小块(block) 图片,原始图片颜色有 256*256*256 种。 2. 把两个图片都转成灰度图片,这样图片只有 256 种颜色了,大大降低计算量,加快匹配速度。 3. 二值化,即把图片变成只有纯黑和纯白两种颜色,进一步减小计算量,加快匹配速度。 当然,二值化之后,图片可能出现大面积黑色或大面积白色,造成匹配结果错误。所以本文没有使用二值化,而是使用灰度图像匹配。 4. 开始匹配。下面左图是灰度匹配的结果,非常准,右图是二值化后匹配的结果,匹配失败。 匹配过程有一些细节,对匹配结果会造成很大影响,例如, 1. 图片处理非常重要,例如缺口小块是不规则的,边缘透明,那么这部分不能参与匹配,要使用掩码,而且掩码要尽可能和缺口互补。 2. 匹配算法有很多种,选择合适的。 3. 匹配不可能100%准,需要在性能和准确率之间平衡,例如上面提到的是否一定要二值化。 模拟移动 WebAPI 有个 Document.createEvent() 函数,可以用来模拟各种事件。 这里需要模拟的是鼠标移动,主要控制的是反应时间、按下位置、移动速度,大多数人操作验证码在 1-2s ,先快后慢。 这里只是为了演示,论证破解的可行性,所以是匀速滑动的,按下位置是随机的。 三、实战 这里演示的是 Chrome 下破解 腾讯防水墙 可疑用户 滑块验证码 https://007.qq.com/online.html?ADTAG=index.head ,所以使用的工具和技术非常简单,Chrome 控制台+Golang+OpenCV4.5.1。 Chrome 控制台 用于执行 js,获取图片内容,调用 Golang 服务,得到滑动距离,模拟滑动。 Golang 接收图片,调用 OpenCV,匹配缺口位置,返回给客户端。 OpenCV 强大的计算机视觉算法开源库。 Go 服务 js 无法直接调用 OpenCV,但可以调用 Web服务,而且 Web 服务通用性好,可以对接不同的前端。 1. 开启一个Go服务,接收参数,并返回移动距离,注意要支持跨域。 接收的参数包括 带缺口的背景大图(bg)、缺口小块(block) 图片 Base64 格式的内容和他们在网页上显示的大小(非真实大小)。显示大小用于缩放图片,使缺口小图能和大图上的缺口大小完全一致,提高匹配成功率。 展开源码 2. 图片预处理。 1)先通过 gocv 库,读取图片,注意 缺口小图边缘透明的,所以有 4个通道(BGRA),完整读取。 展开源码 2)把图片大小调整到网页显示的大小。 1 2 3 4 5 func resize(origin gocv.Mat, cols, rows int) gocv.Mat { resized := gocv.NewMatWithSize(cols, rows, origin.Type()) gocv.Resize(origin, &resized, image.Pt(cols, rows), 0, 0, gocv.InterpolationNearestNeighbor) return resized } 3)变成灰度图像。 1 2 3 4 5 func gray(origin gocv.Mat) gocv.Mat { grayed := gocv.NewMat() gocv.CvtColor(origin, &grayed, gocv.ColorBGRToGray) return grayed } 4)二值化(可选)。 1 2 3 4 5 func threshold(origin gocv.Mat) gocv.Mat { thresholdBG := gocv.NewMat() gocv.Threshold(origin, &thresholdBG, 100, 255, gocv.ThresholdBinaryInv) return thresholdBG } 3. 匹配。 1)由于缺口小图不规则,所以需要使用掩码,掩码图像刚好就是缺口小图的 Alpha通道 即透明度,所以直接使用这个通道即可。 1 gocv.Split(resized)[3] 2)选定一种算法,不同的算法,结果可能会不一样,开始匹配,并对中间结果归一化处理,然后根据选择的算法,选择最佳匹配结果,即移动距离。 http://zhaoxuhui.top/blog/2017/06/12/%E5%9F%BA%E4%BA%8EPython%E7%9A%84OpenCV%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%8614.html 这篇文章对此讲解得非常到位,所以这里不多缀述。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func match(bg, block, mask gocv.Mat) image.Point { result := gocv.NewMatWithSize( bg.Rows()-block.Rows()+1, bg.Cols()-block.Cols()+1, gocv.MatTypeCV32FC1) defer result.Close() gocv.MatchTemplate(bg, block, &result, gocv.TmSqdiff, mask) gocv.Normalize(result, &result, 0, 1, gocv.NormMinMax) _, _, _, maxLoc := gocv.MinMaxLoc(result) log.Debug(maxLoc) return maxLoc } js 脚本 1.获取图片内容和大小。 带缺口的背景大图(bg)、缺口小块(block) 都有固定的 id,取得dom的 src 属性,然后通过 FileReader 读出图片内容。 图片大小即 clientWidth/clientHeight。 1 2 3 4 5 6 7 8 9 10 async function toDataURL( url) { const resp = await fetch(url); const blob = await resp.blob(); return new Promise((resolve, reject) => { const reader = new FileReader() reader.onloadend = () => resolve(reader.result) reader.onerror = reject reader.readAsDataURL(blob) }) } 2.调用 Go 服务,得到 移动距离。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let resp = await fetch('http://127.0.0.1:8080/getdistance', { body: JSON.stringify({ 'bg_base64': bgBase64, 'bg_width': bgElem.clientWidth, 'bg_height': bgElem.clientHeight, 'block_base64': blockBase64, 'block_width': blockElem.clientWidth, 'block_height': blockElem.clientHeight, }), // must match 'Content-Type' header headers: { 'content-type': 'application/json' }, method: 'POST', // *GET, POST, PUT, DELETE, etc. mode: 'cors' // no-cors, cors, *same-origin }) const data = await resp.json() // parses response to JSON console.log(data.distance) 3. 模拟移动。 1)确定滑动位置和滑块元素。 1 2 3 4 5 6 7 8 9 const obj = document.querySelector(slide); console.log(obj) obj.target = '_self'; const _owh = obj.getBoundingClientRect(); let _ox = _owh.width / 2, _oh = _owh.height / 2; _ox = Math.floor(Math.random() * _ox + 60); _oh = Math.floor(Math.random() * _oh + 60); _ox = _ox + _owh.x; _oh = _oh + _owh.y; 2)创建鼠标事件。 1 2 3 4 5 6 function iME(obj, event, screenXArg, screenYArg, clientXArg, clientYArg) { var mousemove = document.createEvent("MouseEvent"); mousemove.initMouseEvent(event, true, true, window, 0, screenXArg, screenYArg, clientXArg, clientYArg, 0, 0, 0, 0, 0, null); obj.dispatchEvent(mousemove); } 3)计算滑动速度,模拟移动鼠标。 1 2 3 4 5 6 7 8 9 10 11 12 13 var elem = document.querySelector(id), k = 0, interval; iME(elem, "mousedown", 0, 0, clientX, clientY); interval = setInterval(function () { k++; iME(elem, "mousemove", clientX + k, clientY, clientX + k, clientY); if (k >= distance) { clearInterval(interval); iME(elem, "mouseup", clientX + k, clientY, clientX + k, clientY); } }, 8); 四、体验 完整代码如下: match.go 展开源码 slide.js 展开源码 1. 开启 Go 服务,注意 gocv 只是包装了 OpenCV,开启服务前必须安装 OpenCV,参考 https://github.com/hybridgroup/gocv#macos 。 2. 用 Chrome 打开 腾讯防水墙主页 https://007.qq.com/,依次点击 "可疑用户"-"体验验证码",弹出滑块验证码。 3. 调出开发者工具,选择 "控制台",由于滑块验证码嵌入在 iframe 中,所以要把控制台的上下文从 "top" 切换成 "tcaptcha_iframe(cap_union_new_show)"。(注意是 cap_union_new_show, 不是 drag_ele.html。) 4. 把 slide.js 所有代码粘贴到控制台,回车执行 js,即可看到滑块验证码开始移动。如果一次不成功,再点击"体验验证码",上下文,多试几次,因为匹配不能保证100%准确。(注意每次执行执行 js 前都要切换上下文,否则会报错找不到对象。) 五、思考 1. 匹配前还可以根据验证码特点做很多优化,例如 缺口高度是固定的,而且位置都靠右,这样匹配位置可以确定在很小的范围内,大大提高匹配效率。 2. 验证码生成过程是很耗时的,一般系统都是预先生成有限个验证码图片,重复利用,所以,如果加上缓存,同样的验证码图片无需重复计算滑动距离,匹配算法变成一次简单的查缓存了。 3. 真人滑动验证码是有一定反应时间的,不如机器反应灵敏,所以真实情况破解程序要休眠一点时间。 4. 真人滑动验证码,到终点时不仅速度慢,结果还有可能不会完全吻合,相差几个像素,还有可能来回微小移动。 5. 真人滑动验证码,按下位置可能在滑块(slide)中间部位,但不会每次都是正中。 6. 移动端的验证码真人是用手指滑动的,所以按压力度也需要注意。 7. 真人滑动验证码滑动过程,即轨迹,不会完全是直线的。 所以破解验证码,不仅要计算滑动距离,还要模拟反应时间、按下位置、移动速度,还要模拟按压力度、滑动轨迹(并非绝对直线)。 除此只外,当前环境和脚本也需要伪装或隐藏,所处的位置(IP、GPS等)、使用的设备等信息在破解量大时需要不断更换。 当然破解是个对抗的过程,如果验证码没有对移动速度、滑动轨迹、设备、环境等做风控策略,那么破解就相对简单。 转换一个角色,验证码防护如果做了各个方面的策略(除上面提到的,还有其他策略),不需要面面俱到,也不需要做得非常复杂,仅仅在多个方面都做一些简单的风控,就可以拦截大部分机器脚本,大大增加破解难度。 参考 GoCV: https://github.com/hybridgroup/gocv OpenCV 模板匹配: http://zhaoxuhui.top/blog/2017/06/12/%E5%9F%BA%E4%BA%8EPython%E7%9A%84OpenCV%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%8614.html 破解腾讯拼图验证码: https://www.freebuf.com/geek/265662.html 破解顶象: https://www.freebuf.com/geek/265479.html 《腾讯防水墙滑动拼图验证码》 《百度旋转图片验证码》 《网易易盾滑动拼图验证码》 《顶象区域面积点选验证码》