本文经作者授权发布。
本文是作者参加某验证码攻防大赛后的记录,大赛中攻击部分用到了网页自动化测试工具Selenium。本文记录的就是相关的方法。
文 | 杨林@Tencent
滑动验证码是新型验证码的一种,区别于传统的字符型验证码,新型验证码主要通过用户行为来区分人和机器。
selenium 被广泛应用于网页自动化测试以及网页爬虫中,本次比赛我采用了 selenium 操作网页元素的方式来自动执行滑动验证码的拖拽。 selenium 在 python 下的详细使用方法参见 selenium python 使用文档。
一、selenium 的配置
1)安装
selenium 支持 python2.7 以及 python3.5 等主流 python 版本,其安装较为简单,有网的环境下,使用 pip 命令即可自动安装。
pip install selenium
无网的环境下,下载 selenium 安装包,然后通过 python 命令也可方便安装。
python setup.py install
2) webdriver
selenium 安装完成后,我们需要下载所选浏览器的 webdriver,本文以 ChromeDriver为例,为了后面在代码中更加方便的加载相应 webdriver,可将 webdriver 的路径添加至系统 PATH 变量中。
注:selenium 通过 webdriver 调用浏览器接口的方式支持多种浏览器,如:ChromeDriver,FirefoxDriver,InternetExplorerDriver,SafariDriver 等真实浏览器,以及:htmlunit、PhantomJS 两个无界面浏览器。大致区别如下:
driver类型 | 优点 | 缺点 | 应用 |
真实浏览器 | 真实模拟用户行为 | 效率、稳定性低 | 兼容性测试 |
HtmlUnit | 速度快 | js引擎不是主流浏览器支持的 | 包含少量js的页面测试 |
PhantomJS | 支持js、速度中等、模拟行为接近真实 | 不能模拟不同/特定浏览器的行为 | 非GUI的功能性测试 |
selenium 选用各浏览器时,只修改加载 webdriver 的语句即可,具体的业务逻辑代码和网页元素操作代码不需要做任何改变。
二、selenium 在 python 下的使用
1) import
通过以下语句在 python 中引用 selenium
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
2)浏览器启动测试
启动浏览器并打开网址进行测试,注意如果前面没有将 webdriver 路径添加至系统 PATH 变量下,引用时需要键入 webdriver 的完整路径。通过 ChromeOptions 可设置浏览器的初始位置和大小。
options = webdriver.ChromeOptions();
options.add_argument('--window-position=0,0'); #chrome 启动初始位置
options.add_argument('--window-size=1080,800'); #chrome 启动初始大小
driver=webdriver.Chrome(chrome_options=options)
driver.get("http://game.captcha.qq.com/hslj/html/hslj/") #比赛主页面
此时看到 Chrome 打开了比赛的 index 页面。
3)网页元素操作
selenium-webdriver 提供了通过网页元素 id、class、name、css、tagName 等方式获取网页元素的方法。用户可根据网页源码的不同情况进行选择,以下代码为自动在比赛主页输入 rtx、qq 并点击登录、开始比赛的例子。
def input_by_id(self, text=u"", element_id=""):
input_el = self.driver.find_element_by_id(element_id) #通过 id 查找网页元素
input_el.clear()
input_el.send_keys(text) #输入字符串
time.sleep(0.5)
def click_by_id(self, element_id=""):
search_el = self.driver.find_element_by_id(element_id)
search_el.click() #鼠标左键单击
time.sleep(0.5)
def click_by_class(self, element_class=""):
search_el = self.driver.find_element_by_class_name(element_class) #通过 class 查找网页元素
search_el.click() #鼠标左键单击
time.sleep(0.5)
self.input_by_id("wadeyang","user-input")
self.input_by_id("914645774", "qq-input")
self.click_by_id("login-btn")
self.click_by_class("hslj-game-btn")
三、破解
搭建好 python-selenium-webdriver 环境,并了解简单使用方法后,下面介绍一下滑动验证码的破解思路。
1)图片获取
为了计算滑动验证码的拖拽位置,我们首先需要获取原始图片和缺口图片,通过对网页 html 的分析,我们通过以下代码获取两张图片。
self.driver.switch_to.frame(self.driver.find_element_by_tag_name("iframe"))
#转换 webdriver 的工作环境至子 iframe,通过 tag_name 查找网页元素
img1ele = self.driver.find_element_by_id("totalBlock") #获取原始 img 网页元素
fatherdiv = self.driver.find_element_by_class_name("oripic")#获取缺口 img 的 div
img2ele = fatherdiv.find_element_by_tag_name("img");#获取缺口 img 网页元素
src1 = img1ele.get_attribute("src");#得到原始 img 路径
src2 = img2ele.get_attribute("src");#得到缺口 img 路径
self.driver.switch_to.default_content()#转换 webdriver 工作环境至默认
urlopen = urllib.request #urllib 模块,可通过 pip 安装,import urllib 引用
fp = urlopen.urlopen(src1) #打开原始 img 路径
data1 = fp.read() #读取数据
fp.close()
file = open('d://1.jpg', 'w b') #将原始 img 保存为 d://1.jpg
file.write(data1)
file.close()
fp = urlopen.urlopen(src2) #打开缺口 img 路径
data2 = fp.read()
fp.close()
file = open('d://2.jpg', 'w b') #将缺口 img 保存为 d://2.jpg
file.write(data2)
file.close()
2)缺口位置计算
在获得原始图片和缺口图片后,接下来需要计算缺口的水平位置,也就是需要拖拽的距离。我们首先获取两张图像素的差值。
img1 = Image.open('d://1.jpg'); #from PIL import Image 引用
img2 = Image.open('d://2.jpg');
img3 = ImageChops.difference (img1, img2); #from PIL import ImageChops 引用
再从左到右按列的方式遍历 img3 的像素,当遇像素值连续不为 0 数量超过 70 时停止,此时的 x 坐标即为滑块应拖动的距离。
3)拖拽轨迹采集
为了模拟正常用户的拖拽轨迹,我们首先需要采集正常用户拖拽验证码时的鼠标操作及运动轨迹。这里我利用了 GhostMouse Free 工具,并通过简单的 C#编程解析其保存文件的方式来将正常用户的拖拽轨迹保存成为不同的文件。c#代码如下:
using (StreamReader sr = new StreamReader("e:\data.rms"))
{
String line;
// Read and display lines from the file until the end of
// the file is reached.
bool open = false;
int filestartindex = 1;
FileStream fs = null;
StreamWriter sw = null;
int lastX = 0, lastY = 0;
String temp = null;
Queue X = new Queue();
Queue Y = new Queue();
Queue D = new Queue();
while ((line = sr.ReadLine()) != null){
if (line.IndexOf("LMouse up") != -1 && open){
open = false;
if (X.Sum() > 0){
if (File.Exists("e:\dividemousedata\" filestartindex ".txt"))
File.Delete("e:\dividemousedata\" filestartindex ".txt");
fs = new FileStream("e:\dividemousedata\" filestartindex ".txt", FileMode.OpenOrCreate, FileAccess.ReadWrite); //可以指定盘符,也可以指定任意文件名,还可以为 word 等文件
filestartindex = 1;
sw = new StreamWriter(fs); // 创建写入流
sw.WriteLine(X.Sum().ToString());
while (X.Count > 0){
string x = X.Dequeue().ToString();
string y = Y.Dequeue().ToString();
string d = D.Dequeue().ToString();
sw.WriteLine(x " " y " " d);
}
sw.Close();
fs.Close();
}
X.Clear();
Y.Clear();
D.Clear();
}
if (line.IndexOf("LMouse down") != -1){
open = true;
temp = line.Split('(')[1];
temp = temp.Split(')')[0];
lastX = int.Parse(temp.Split(',')[0]);
lastY = int.Parse(temp.Split(',')[1]);
line = sr.ReadLine();
temp = line.Split(' ')[1];
temp = temp.Split('}')[0];
float delay = float.Parse(temp);
X.Enqueue(0); Y.Enqueue(0); D.Enqueue(delay);
continue;
}
if (open){
temp = line.Split('(')[1];
temp = temp.Split(')')[0];
int posX = int.Parse(temp.Split(',')[0]);
int posY = int.Parse(temp.Split(',')[1]);
line = sr.ReadLine();
temp = line.Split(' ')[1];
temp = temp.Split('}')[0];
float delay = float.Parse(temp);
X.Enqueue(posX - lastX); Y.Enqueue(posY - lastY); D.Enqueue(delay);
lastX = posX;
lastY = posY;
}
}
}
GhostMouse Free 工具记录的文件,及最后生成的拖拽轨迹文件(第一行为拖拽的总长度;第一列为 x 位移,第二列为 y 位移,第三列为停顿时间):
4)拖拽轨迹还原
有了正常用户的拖拽轨迹文件和滑块需要拖动的距离,我们即可对两者进行匹配,并对轨迹进行还原。这里有一个 trick,因为拖拽距离为 0 至 230,所以采集足够多的轨迹文件,我们就可以不用对拖拽轨迹进行任何缩放,对其进行 100%的还原。
self.driver.switch_to.frame(self.driver.find_element_by_tag_name("iframe"))
dragger = self.driver.find_element_by_id(element_class)
action = ActionChains(self.driver)
action.click_and_hold(dragger).perform(); #鼠标左键按下不放
action.reset_actions()
for index in range(len(x)):
action.move_by_offset(x[index], y[index]).perform(); #移动一个位移
action.reset_actions()
time.sleep(d[index]); #等待停顿时间
action.release().perform(); #鼠标左键松开
action.reset_actions()
self.driver.switch_to.default_content()
四、最后
以上以滑动验证码破解为例介绍了 selenium2.0 在 python 下的应用。使用浏览器操作模拟的方式进行滑动验证码破解与破解 js 然后直接发送请求的方式进行破解相比较,主要的缺点是请求频率太慢,优点是通过简单修改即可适应不同平台的滑动验证码。
题图:pexels,CC0 授权。
点击阅读原文,查看更多 Python 教程和资源。