深度解析:注册安全分析报告:黑马程序员
前言由于网站注册入口容易被黑客攻击,存在如下安全问题:暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞所以大部分网站及App都采取图形验证码或滑动验证码等交互解
前言
由于网站注册入口容易被黑客攻击,存在如下安全问题:
暴力破解密码,造成用户信息泄露
短信盗刷的安全问题,影响业务及导致用户投诉
带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞
所以大部分网站及App 都采取图形验证码或滑动验证码等交互解决方案, 但在机器学习能力提高的当下,连百度这样的大厂都遭受攻击导致点名批评,图形验证及交互验证方式的安全性到底如何?请看具体分析。
一、 黑马程序员PC端注册入口
简介:黑马程序员是传智教育(股票代码:003032)旗下高端IT教育品牌,传智教育是国内首个实现A股IPO的教育企业。依托于传智雄厚的教育资源,秉承传智教育“为中华民族伟大复兴而讲课,为千万学生少走弯路而著书”的使命,致力于培养高精尖数字化人才,为国家建设现代化经济体系的战略目标培养科技人才。
二、 安全性分析报告:
采用腾讯的智能验证,包含点击和滑动验证,容易被模拟器绕过甚至逆向后暴力攻击,滑动拼图识别率在 95% 以上。
三、 测试方法:
前端界面分析,这是腾讯v2版本,特点是矩形缺口,滑块和其它图形混合在一起,需要做切割处理,智能版本为头几次直接过,无需滑动,我们采用模拟器的方式,关键点主要模拟器交互、距离识别和轨道算法3部分。
1. 模拟器交互部分
public RetEntity send
(
WebDriver driver, String areaCode, String phone
)
{
RetEntity retEntity
=
new RetEntity
(
)
;
WebElement phoneElemet, loginElemet
;
try
{
driver.get
(
INDEX_URL
)
;
// 点击注册
driver.findElement
(
By.className
(
"user_dl"
))
.findElement
(
By.tagName
(
"a"
))
.click
(
)
;
Thread.sleep
(
1000
)
;
WebElement iframe
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"user_dl"
)
,
500
)
;
if
(
iframe
==
null
)
{
System.out.println
(
"user_zc|timeout!!!"
)
;
return
null
;
}
driver.switchTo
(
)
.frame
(
iframe
)
;
// 输入手机号
phoneElemet
=
driver.findElement
(
By.id
(
"userphone"
))
;
phoneElemet.clear
(
)
;
for
(
int i
=
0
;
i
<
phone.length
(
)
;
i++
)
{
char c
=
phone.charAt
(
i
)
;
phoneElemet.sendKeys
(
c +
""
)
;
}
Thread.sleep
(
2
*
1000
)
;
// 点击获取验证码
loginElemet
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"TencentCaptcha"
)
,
500
)
;
loginElemet.click
(
)
;
Thread.sleep
(
2
*
1000
)
;
// 计算移动距离
RetEntity ret
=
tClient.moveExec
(
driver
)
;
if
(
ret.getRet
(
)
==
-1
)
{
System.out.println
(
"moveExec ret="
+ ret
)
;
return
null
;
}
// send msg ret
Thread.sleep
(
2000
)
;
iframe
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"user_dl"
)
,
1
)
;
if
(
iframe
==
null
)
{
System.out.println
(
"user_zc|timeout!!!"
)
;
return
null
;
}
driver.switchTo
(
)
.frame
(
iframe
)
;
Thread.sleep
(
3000
)
;
String str
=
(
loginElemet
!=
null
)
? loginElemet.getAttribute
(
"value"
)
:
null
;
if
(
str
!=
null
)
{
retEntity.setMsg
(
str
)
;
}
if
(
str.contains
(
"重新发送"
))
{
retEntity.setRet
(
0
)
;
}
return
retEntity
;
}
catch
(
Throwable e
)
{
StringBuffer er
=
new StringBuffer
(
"send() "
+ e.toString
(
)
+
"
\n
"
)
;
for
(
StackTraceElement elment
:
e.getStackTrace
(
))
er.append
(
elment.toString
(
)
+
"
\n
"
)
;
logger.error
(
er.toString
(
))
;
System.out.println
(
er.toString
(
))
;
retEntity.setRet
(
-1
)
;
retEntity.setMsg
(
e.toString
(
))
;
return
retEntity
;
}
}
/**
* v2 版本
*
* @param driver
* @return
*/
public RetEntity moveExec
(
WebDriver driver
)
{
File bFile
=
null
;
File sFile
=
null
;
RetEntity retEntity
=
new RetEntity
(
)
;
retEntity.setRet
(
-1
)
;
try
{
// 获取到验证区域
WebElement iframe
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"tcaptcha_iframe_dy"
)
,
100
)
;
if
(
iframe
==
null
)
{
System.out.println
(
"moveExec() tcaptcha_iframe|timeout!!!"
)
;
retEntity.setRet
(
-99
)
;
retEntity.setMsg
(
"tcaptcha_iframe|timeout!!"
)
;
return
retEntity
;
}
driver.switchTo
(
)
.frame
(
iframe
)
;
String bgUrl
=
null
;
System.out.println
(
"get bgUrl begin ........"
)
;
for
(
int i
=
0
;
i
<
10
;
i++
)
{
// 获取带阴影的背景图
WebElement wegSlideBg
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"slideBg"
)
,
300
)
;
String cssValue
=
(
wegSlideBg
!=
null
)
? wegSlideBg.getCssValue
(
"background-image"
)
:
null
;
bgUrl
=
(
cssValue
!=
null
&&
cssValue.contains
(
"
\"
"
))
? cssValue.split
(
"
\"
"
)
[
1
]
:
null
;
System.out.println
(
" |-i="
+ i +
",bgUrl="
+ bgUrl
)
;
if
(
bgUrl
!=
null
&&
bgUrl.length
(
)
>
0
)
{
break
;
}
else
{
sleep
(
500
)
;
}
}
// 获取带阴影的背景图
if
(
bgUrl
==
null
)
{
retEntity.setMsg
(
"bgUrl="
+ bgUrl
)
;
return
retEntity
;
}
Long
time
=
System.currentTimeMillis
(
)
;
// 获取小图 URL
(
替换img_index
=
1
为
img_index
=
0
)
String slUrl
=
bgUrl.replaceAll
(
"img_index=1"
,
"img_index=0"
)
;
bFile
=
new File
(
dataPath +
time
+
"-b.png"
)
;
sFile
=
new File
(
dataPath +
time
+
"-s.png"
)
;
Map
<
String, byte
[
]
>
retMap
=
getTwoImg
(
driver, bgUrl, slUrl, bFile, sFile
)
;
if
(
retMap
!=
null
)
{
byte
[
]
bigBytes
=
retMap.get
(
"big"
)
;
byte
[
]
smallBytes
=
retMap.get
(
"small"
)
;
String distanceStr
=
null, width
=
null
;
String ckSum
=
GenChecksumUtil.genChecksum
(
bigBytes
)
;
String
[
]
outArray
=
openCv2.getOpenCvDistance
(
ckSum, bigBytes, smallBytes,
"tencent_v2"
,
3
)
;
distanceStr
=
(
outArray
!=
null
&&
outArray.length
>=
2
)
? outArray
[
1
]
:
null
;
width
=
(
outArray
!=
null
&&
outArray.length
>=
2
)
? outArray
[
0
]
:
null
;
Double left
=
27.0
*
672
/
340
;
// 起点距左边距离
Double act
=
(
Double.parseDouble
(
distanceStr
)
- left - Double.parseDouble
(
width
))
*
340.0
/
672.0
;
Integer distance
=
act.intValue
(
)
;
System.out.println
(
"moveExec() distance("
+ distanceStr +
")="
+ distance
)
;
if
(
distance
==
null
||
distance
<=
0
)
{
return
retEntity
;
}
WebElement moveElemet
=
ChromeDriverManager.waitElement
(
driver, By.className
(
"tc-slider-normal"
)
,
500
)
;
sleep
(
500
)
;
// 滑动
ActionMove.move
(
driver, moveElemet, distance
)
;
sleep
(
400
)
;
// 滑动结果
String gtInfo
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"statusSuccess"
)
,
100
)
.getText
(
)
;
if
(
gtInfo
==
null
||
""
.equals
(
gtInfo
))
{
sleep
(
200
)
;
gtInfo
=
ChromeDriverManager.waitElement
(
driver, By.id
(
"statusError"
)
,
100
)
.getText
(
)
;
}
System.out.println
(
"moveExec() gtInfo="
+ gtInfo
)
;
// driver.switchTo
(
)
.frame
(
oldWin
)
;
if
(
gtInfo.contains
(
"验证成功"
))
{
retEntity.setRet
(
0
)
;
retEntity.setMsg
(
"moveExec:"
+ gtInfo
)
;
}
else
if
(
gtInfo.contains
(
"再试一次"
)
||
gtInfo.contains
(
"恍惚了"
)
||
gtInfo.contains
(
"半路丢了"
))
{
retEntity.setRet
(
-1
)
;
retEntity.setMsg
(
"失败"
)
;
}
else
{
retEntity.setMsg
(
gtInfo
)
;
}
}
else
{
logger.error
(
"retMap="
+ retMap
)
;
retEntity.setMsg
(
"retMap="
+ retMap
)
;
}
// 切回主页面
driver.switchTo
(
)
.defaultContent
(
)
;
return
retEntity
;
}
catch
(
Exception e
)
{
StringBuffer er
=
new StringBuffer
(
"moveExec() "
+ e.toString
(
)
+
"
\n
"
)
;
for
(
StackTraceElement elment
:
e.getStackTrace
(
))
er.append
(
elment.toString
(
)
+
"
\n
"
)
;
logger.error
(
er.toString
(
))
;
System.out.println
(
er.toString
(
))
;
retEntity.setRet
(
-99
)
;
retEntity.setMsg
(
er.toString
(
))
;
return
retEntity
;
}
finally
{
if
(
retEntity.getRet
(
)
==
0
)
{
System.out.println
(
"moveExec() del file..."
)
;
if
(
bFile
!=
null
)
bFile.delete
(
)
;
if
(
sFile
!=
null
)
sFile.delete
(
)
;
}
}
2. 距离识别
/**
*
* @param ckSum
* @param bigBytes
* @param smallBytes
* @param factory
* @return
{
width, maxX
}
*/
public String
[
]
getOpenCvDistance
(
String ckSum, byte bigBytes
[
]
, byte smallBytes
[
]
, String factory, int border
)
{
try
{
String basePath
=
ConstTable.codePath + factory +
"/"
;
File baseFile
=
new File
(
basePath
)
;
if
(
!
baseFile.isDirectory
(
))
{
baseFile.mkdirs
(
)
;
}
// 小图文件
File smallFile
=
new File
(
basePath + ckSum +
"_s.png"
)
;
FileUtils.writeByteArrayToFile
(
smallFile, smallBytes
)
;
// 大图文件
File bigFile
=
new File
(
basePath + ckSum +
"_b.png"
)
;
FileUtils.writeByteArrayToFile
(
bigFile, bigBytes
)
;
// 边框清理
(
去干扰
)
byte
[
]
clearBoder
=
(
border
>
0
)
? ImageIOHelper.clearBoder
(
smallBytes, border
)
:
smallBytes
;
File tpFile
=
new File
(
basePath + ckSum +
"_t.png"
)
;
FileUtils.writeByteArrayToFile
(
tpFile, clearBoder
)
;
String resultFile
=
basePath + ckSum +
"_o.png"
;
return
getWidth
(
tpFile.getAbsolutePath
(
)
, bigFile.getAbsolutePath
(
)
, resultFile
)
;
}
catch
(
Throwable e
)
{
logger.error
(
"getMoveDistance() ckSum="
+ ckSum +
" "
+ e.toString
(
))
;
for
(
StackTraceElement elment
:
e.getStackTrace
(
))
{
logger.error
(
elment.toString
(
))
;
}
return
null
;
}
}
/**
* Open Cv 图片模板匹配
*
* @param tpPath
* 模板图片路径
* @param bgPath
* 目标图片路径
* @return
{
width, maxX
}
*/
private String
[
]
getWidth
(
String tpPath, String bgPath, String resultFile
)
{
try
{
Rect rectCrop
=
clearWhite
(
tpPath
)
;
Mat g_tem
=
Imgcodecs.imread
(
tpPath
)
;
Mat clearMat
=
g_tem.submat
(
rectCrop
)
;
Mat cvt
=
new Mat
(
)
;
Imgproc.cvtColor
(
clearMat, cvt, Imgproc.COLOR_RGB2GRAY
)
;
Mat edgesSlide
=
new Mat
(
)
;
Imgproc.Canny
(
cvt, edgesSlide, threshold1, threshold2
)
;
Mat cvtSlide
=
new Mat
(
)
;
Imgproc.cvtColor
(
edgesSlide, cvtSlide, Imgproc.COLOR_GRAY2RGB
)
;
Imgcodecs.imwrite
(
tpPath, cvtSlide
)
;
Mat g_b
=
Imgcodecs.imread
(
bgPath
)
;
Mat edgesBg
=
new Mat
(
)
;
Imgproc.Canny
(
g_b, edgesBg, threshold1, threshold2
)
;
Mat cvtBg
=
new Mat
(
)
;
Imgproc.cvtColor
(
edgesBg, cvtBg, Imgproc.COLOR_GRAY2RGB
)
;
int result_rows
=
cvtBg.rows
(
)
- cvtSlide.rows
(
)
+
1
;
int result_cols
=
cvtBg.cols
(
)
- cvtSlide.cols
(
)
+
1
;
Mat g_result
=
new Mat
(
result_rows, result_cols, CvType.CV_32FC1
)
;
Imgproc.matchTemplate
(
cvtBg, cvtSlide, g_result, Imgproc.TM_CCOEFF_NORMED
)
;
// 归一化平方差匹配法
// 归一化相关匹配法
MinMaxLocResult minMaxLoc
=
Core.minMaxLoc
(
g_result
)
;
Point maxLoc
=
minMaxLoc.maxLoc
;
Imgproc.rectangle
(
cvtBg, maxLoc, new Point
(
maxLoc.x + cvtSlide.cols
(
)
, maxLoc.y + cvtSlide.rows
(
))
, new Scalar
(
0
,
0
,
255
)
,
1
)
;
Imgcodecs.imwrite
(
resultFile, cvtBg
)
;
String width
=
String.valueOf
(
cvtSlide.cols
(
))
;
String maxX
=
String.valueOf
(
maxLoc.x + cvtSlide.cols
(
))
;
System.out.println
(
"OpenCv2.getWidth() width="
+ width +
",maxX="
+ maxX
)
;
return
new String
[
]
{
width, maxX
}
;
}
catch
(
Throwable e
)
{
System.out.println
(
"getWidth() "
+ e.toString
(
))
;
logger.error
(
"getWidth() "
+ e.toString
(
))
;
for
(
StackTraceElement elment
:
e.getStackTrace
(
))
{
logger.error
(
elment.toString
(
))
;
}
return
null
;
}
}
3. 轨道生成及移动算法
/**
* 根据距离获取滑动轨迹
*
* @param distance需要移动的距离
* @return
*/
public static List
<
Integer
>
getTrack
(
int distance
)
{
List
<
Integer
>
track
=
new ArrayList
<
Integer
>
(
)
;
// 移动轨迹
List
<
Integer
[
]
>
list
=
getXyTrack
(
distance
)
;
for
(
Integer
[
]
m
:
list
)
{
track.add
(
m
[
0
]
)
;
}
return
track
;
}
/**
* 双轴轨道生成算法,主要实现平滑加速和减速
*
* @param distance
* @return
*/
public static List
<
Integer
[
]
>
getXyTrack
(
int distance
)
{
List
<
Integer
[
]
>
track
=
new ArrayList
<
Integer
[
]
>
(
)
;
// 移动轨迹
try
{
int a
=
(
int
)
(
distance /
3.0
)
+ random.nextInt
(
10
)
;
int h
=
0
, current
=
0
;
// 已经移动的距离
BigDecimal midRate
=
new BigDecimal
(
0.7
+
(
random.nextInt
(
10
)
/
100.00
))
.setScale
(
4
, BigDecimal.ROUND_HALF_UP
)
;
BigDecimal mid
=
new BigDecimal
(
distance
)
.multiply
(
midRate
)
.setScale
(
0
, BigDecimal.ROUND_HALF_UP
)
;
// 减速阈值
BigDecimal move
=
null
;
// 每次循环移动的距离
List
<
Integer
[
]
>
subList
=
new ArrayList
<
Integer
[
]
>
(
)
;
// 移动轨迹
boolean plus
=
true
;
Double t
=
0.18
,
v
=
0.00
, v0
;
while
(
current
<=
distance
)
{
h
=
random.nextInt
(
2
)
;
if
(
current
>
distance /
2
)
{
h
=
h * -1
;
}
v0
=
v
;
v
=
v0 + a * t
;
move
=
new BigDecimal
(
v0 * t +
1
/
2
* a * t * t
)
.setScale
(
4
, BigDecimal.ROUND_HALF_UP
)
;
// 加速
if
(
move.intValue
(
)
<
1
)
move
=
new BigDecimal
(
1L
)
;
if
(
plus
)
{
track.add
(
new Integer
[
]
{
move.intValue
(
)
, h
}
)
;
}
else
{
subList.add
(
0
, new Integer
[
]
{
move.intValue
(
)
, h
}
)
;
}
current
+=
move.intValue
(
)
;
if
(
plus
&&
current
>=
mid.intValue
(
))
{
plus
=
false
;
move
=
new BigDecimal
(
0L
)
;
v
=
0.00
;
}
}
track.addAll
(
subList
)
;
int bk
=
current - distance
;
if
(
bk
>
0
)
{
for
(
int i
=
0
;
i
<
bk
;
i++
)
{
track.add
(
new Integer
[
]
{
-1, h
}
)
;
}
}
System.out.println
(
"getMoveTrack("
+ midRate +
") a="
+ a +
",distance="
+ distance +
" -> mid="
+ mid.intValue
(
)
+
" size="
+ track.size
(
))
;
return
track
;
}
catch
(
Exception e
)
{
System.out.print
(
e.toString
(
))
;
return
null
;
}
}
/**
* 模拟人工移动
*
* @param driver
* @param element页面滑块
* @param distance需要移动距离
* @throws InterruptedException
*/
public static void move
(
WebDriver driver, WebElement element, int distance
)
throws InterruptedException
{
List
<
Integer
[
]
>
track
=
getXyTrack
(
distance
)
;
if
(
track
==
null
||
track.size
(
)
<
1
)
{
System.out.println
(
"move() track="
+ track
)
;
}
int moveY, moveX
;
StringBuffer sb
=
new StringBuffer
(
)
;
try
{
Actions actions
=
new Actions
(
driver
)
;
actions.clickAndHold
(
element
)
.perform
(
)
;
Thread.sleep
(
20
)
;
long begin, cost
;
Integer
[
]
move
;
int
sum
=
0
;
for
(
int i
=
0
;
i
<
track.size
(
)
;
i++
)
{
begin
=
System.currentTimeMillis
(
)
;
move
=
track.get
(
i
)
;
moveX
=
move
[
0
]
;
sum
+=
moveX
;
moveY
=
move
[
1
]
;
if
(
moveX
<
0
)
{
if
(
sb.length
(
)
>
0
)
{
sb.append
(
","
)
;
}
sb.append
(
moveX
)
;
}
actions.moveByOffset
(
moveX, moveY
)
.perform
(
)
;
cost
=
System.currentTimeMillis
(
)
- begin
;
if
(
cost
<
3
)
{
Thread.sleep
(
3
- cost
)
;
}
}
if
(
sb.length
(
)
>
0
)
{
System.out.println
(
"-----backspace["
+ sb.toString
(
)
+
"]sum="
+
sum
+
",distance="
+ distance
)
;
}
Thread.sleep
(
180
)
;
actions.release
(
element
)
.perform
(
)
;
Thread.sleep
(
500
)
;
}
catch
(
Exception e
)
{
StringBuffer er
=
new StringBuffer
(
"move() "
+ e.toString
(
)
+
"
\n
"
)
;
for
(
StackTraceElement elment
:
e.getStackTrace
(
))
er.append
(
elment.toString
(
)
+
"
\n
"
)
;
logger.error
(
er.toString
(
))
;
System.out.println
(
er.toString
(
))
;
}
}
4. OpenCv 轮廓匹配测试样例:
四丶结语
黑马程序员主要做技术培训为主要业务的网站,应该有不少技术专家, 对机器识别应该有所了解, 但采用的是通俗的滑动验证产品, 该产品稳定并且市场占有率很高, 在一定程度上提高了用户体验, 但安全性在机器学习的今天, 已经无法应对攻击了,并且正是由于该产品通俗, 所以在网上破解的文章和教学视频也是大量存在,并且经过验证滑动产品很容易被破解, 所以除了滑动验证方式, 花样百出的产品层出不穷,但本质就是牺牲用户体验来提高安全。
很多人在短信服务刚开始建设的阶段,可能不会在安全方面考虑太多,理由有很多。
比如:“
需求这么赶,当然是先实现功能啊
”,“
业务量很小啦,系统就这么点人用,不怕的
” , “
我们怎么会被盯上呢,不可能的
”等等。
有一些理由虽然有道理,但是该来的总是会来的。前期欠下来的债,总是要还的。越早还,问题就越小,损失就越低。
所以大家在安全方面还是要重视。(血淋淋的栗子!)
#安全短信#
戳这里→
康康你手机号在过多少网站注册过!!!
谷歌图形验证码在AI 面前已经形同虚设,所以谷歌宣布退出验证码服务, 那么当所有的图形验证码都被破解时,大家又该如何做好防御呢?
>>相关阅读
《腾讯防水墙滑动拼图验证码》
《百度旋转图片验证码》
《网易易盾滑动拼图验证码》
《顶象区域面积点选验证码》
《顶象滑动拼图验证码》
《极验滑动拼图验证码》
《使用深度学习来破解 captcha 验证码》
《验证码终结者-基于CNN+BLSTM+CTC的训练部署套件》