《WestWorld》第一季第二集有一句很有意思的台词:游客William来到西部世界公园,遇到一个美女接待员,但无法区分对方是否是人类,于是产生了如下对话:
对于很多编写机器行为脚本的攻击者而言,这可能是他们非法行为背后的逻辑——当你无法识别的时候,我还算非法吗?
其实非法永远是非法,作为一个网站维护人员,你需要做的就是将它识别出来
以浏览器行为为例,基于哪些特征量,用何种技术,如何准确识别,都不是那么容易的事情,因为机器行为是变化且持续的。唯一容易的事情,可能是处理方式上——不论是弹验证码还是直接阻断请求,处理掉就好了,而实在不必像剧中William对待Dolores那样,“此情无计可消除”。
笔者公司在浏览器人机行为识别上,目前的做法是基于鼠标&键盘事件分析:例如在浏览器不刷新的前提下,捕获鼠标历史坐标点、移动线路情况、鼠标键盘点击的次数等,看这些指标:
- 是否不满足设定阈值
- 是否在黑名单中
但是各有各的问题:
- 阈值无法动态变化,只能人工检测到异常,再手工去后台调整
- 脚本更换数据,可能就不在黑名单了
可以说是非常尴尬了。
但这种做法依然有它的合理之处:每次登录的鼠标轨迹完全一样,可以说100%是机器;从开始捕获到验证通过时间小得惊人(例如<0.1s),是机器的概率,也非常之大(也许98%?)...总而言之,这种做法可以概括为:通过开发人员的“拍脑袋”,来想象一些“不像人类操作”的场景,给出一个“自己认为的人类极限值”,从而“描述并阻止机器行为”。
这种做法,绝对是可以杜绝部分机器行为渗入的——因为正常人确实干不出这事。但是一旦你的脑袋拍歪了,或者“想象力枯竭”,那么这便是漏洞所在。
因此笔者在思考,能否利用机器学习中的聚类分析,对用户的浏览器行为进行一定程度辅助判定?
上图来自极验官网“产品特性”截图。很明显,人机行为在一定数据量级下,是存在边界的。从图的分类效果上看,似乎是用到了支持向量机(SVM)。
当然,不排除这只是一张产品宣传效果图。但笔者更倾向于不是。因为很久之前在InfoQ看过一个新浪安全团队的视频:反机器人行为系统漫谈,在16:38秒左右有提到一个“幂律分布”的概念,有兴趣的读者可以一看。是否可以证明大部分非法的请求是存在一些特征,从而区别于其他请求的。
所以,通过机器学习/对人的行为特征进行大量的分析,建立安全模型去区分人与机器程序理论可行:利用深度学习构建的神经网络是可以不断地自主学习的,在不断的验证过程中不断的学习新的特征分析。
在此笔者以K-means聚类算法做了一些测试。
聚类和分类在机器学习中是两个概念。
- 分类是我们根据已知的一些样本(包括属性与类标号)来得到分类模型(即得到样本属性与类标号之间的函数),然后通过此目标函数来对只包含属性的样本数据进行分类。属于supervised learning(监督学习)。
- 聚类则事先并不知道任何样本的类别标号,希望通过某种算法来把一组未知类别的样本划分成若干类别,聚类的时候,我们并不关心某一类是什么,我们需要实现的目标只是把相似的东西聚到一起,这在机器学习中被称作 unsupervised learning (无监督学习)
以本文讨论的案例为例。如果我们的训练样本里面,包含人机识别的结果,且正确率100%,那么可以基于分类算法,如SVM,得到一个分类器函数,对未来的新数据进行人机判定;而如果这个训练样本里,不包含人机识别结果,或者结果并不准确呢?那我们可以通过选取对行为最有影响的特征量做聚类分析。
公司目前的人机识别系统并非100%正确率,但80%还是能保证。因此正好可以用其结果和聚类结果做一个对比,关注两点即可:
- 各自识别出的人类行为和机器行为数量
- 聚类结果和公司人机识别结果的一致率是多少
算法选择##
以人机识别为例,红色是恶意程序,绿色是正常用户。我们用肉眼可以一眼看出,有两个分类。但如何让机器看出?
而这就是K-means要解决的问题:
输入:聚类个数k,以及包含 n个数据对象的数据库。
输出:满足方差最小标准的k个聚类。
K-means算法是很典型的基于距离的聚类算法,采用距离作为相似性的评价指标,即认为两个对象的距离越近,其相似度就越大。该算法认为簇是由距离靠近的对象组成的,因此把得到紧凑且独立的簇作为最终目标。
直接上图更容易理解:
从上图中可以看到,A,B,C,D,E是五个在图中点。而灰色的点是种子点,也就是用来找点群的点。以人机识别为例。假定结果只有人和机器,那么就有两个种子点,所以K=2。则:
- 随机在图中取K(这里K=2)个种子点
- 然后对图中的所有点求到这K个种子点的距离,假如点Pi离种子点Si最近,那么Pi属于Si点群。(上图中,可以看到A,B属于上面的种子点,C,D,E属于下面中部的种子点)
- 接下来,移动种子点到属于他的“点群”的中心。(见图上的第三步)
- 然后重复第2)和第3)步,直到,种子点没有移动(可以看到图中的第四步上面的种子点聚合了A,B,C,下面的种子点聚合了D,E)
环境准备##
- Anaconda3(包含机器学习常用lib的python sdk,这里直接用python3)
- JetBrains PyCharm Community Edition (python ide)
- 测试数据
测试数据选用了4个维度,共4w条数据:
1.鼠标点击次数,以下记为mc
2.键盘点击次数,以下记为kc
3.鼠标移动次数,以下记为mmc
4.最近三次鼠标的坐标点(x1,y1),(x2,y2),(x3,y3),以下记为m3p。这三个点各自独立作为维度,笔者认为意义不大(单个x or y无法反映其特征),因此取了其构成三角形之面积(最近一次提交上送的轨迹大多差不多,则面积不该过于突兀)。
另,由于数据来源于公司,出于保密考虑,不贴出具体数据和格式,只贴笔者原创的测试代码。
测试案例##
笔者对维度4的引入效果持疑,因此做有无维度4两类测试,看哪种更接近公司人机识别系统结果。
- 只选前3个维度
# -*- coding: utf-8 -*-
# 导入相应的包
from pyexcel_xls import get_data
import re
from sklearn.cluster import KMeans
import scipy.io as sio
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
xls_data = get_data(r"test.xlsx")
# print ("Get data type:", type(xls_data))
# print (xls_data['ub'])
all_num = len(xls_data['ub']);
ub_dim_list = [[] for i in range(all_num)]
# print(ub_dim_list)
ub_res_list = []
count = 0
for row_data in xls_data['ub']:
# print (row_data[0])
ub_data = eval(row_data[0])
ub_res_list.append(row_data[1])
# ub_dim_list[count].append(polyArea(mp_dim_list))
ub_dim_list[count].append(ub_data['kc'])
ub_dim_list[count].append(ub_data['mc'])
ub_dim_list[count].append(ub_data['mmc'])
count += 1
# print (ub_dim_list)
# print('人机识别结果:', ub_res_list)
# 聚类,2个聚簇
clf = KMeans(n_clusters=2)
y_pred = clf.fit_predict(ub_dim_list)
y_pred = y_pred.tolist()
# print('机器学习结果:', y_pred)
import numpy as np
from matplotlib import pyplot as plt
from scipy import io as spio
from sklearn.decomposition import pca
from sklearn.preprocessing import StandardScaler
# '''归一化数据并作图'''
scaler = StandardScaler()
scaler.fit(ub_dim_list)
x_train = scaler.transform(ub_dim_list)
print(x_train)
# '''拟合数据'''
K=3 # 要降的维度
model = pca.PCA(n_components=K).fit(x_train) # 拟合数据,n_components定义要降的维度
print(model)
Z = model.transform(x_train) # transform就会执行降维操作
print(Z)
# print(Z[:,0])
fig = plt.figure()
ax_pca = fig.add_subplot(1, 1, 1, projection='3d')
ax_pca.view_init(elev=10., azim=11)
ax_pca.scatter(Z[:,0], Z[:,1], Z[:,2], c=y_pred)
# '''拟合数据'''
K=2 # 要降的维度
model = pca.PCA(n_components=K).fit(x_train) # 拟合数据,n_components定义要降的维度
print(model)
Z = model.transform(x_train) # transform就会执行降维操作
print(Z)
# print(Z[:,0])
fig = plt.figure()
ax_pca = fig.add_subplot(1, 1, 1)
ax_pca.scatter(Z[:,0], Z[:,1], c=y_pred)
rjsb_num = [0, 0]
for i in ub_res_list:
if (i == 0):
rjsb_num[0] += 1
else:
rjsb_num[1] += 1
print('人机识别结果: 人类:' + str(rjsb_num[0]) + ',机器:' + str(rjsb_num[1]) + ',机器行为占比:' + '%.2f%%' % (
rjsb_num[1] / all_num * 100))
ml_num = [0, 0]
for i in y_pred:
if (i == 0):
ml_num[0] += 1
else:
ml_num[1] += 1
print('机器学习结果: 人类:' + str(ml_num[0]) + ',机器:' + str(ml_num[1]) + ',机器行为占比:' + '%.2f%%' % (ml_num[1] / all_num * 100))
right_num = 0
for i in range(0, all_num):
if ((ub_res_list[i] == y_pred[i] and ub_res_list[i] == 0) or (ub_res_list[i] != 0 and y_pred[i] == 1)):
right_num += 1
print('以人机识别为参考系,机器学习正确率:%.2f%%' % (right_num / all_num * 100))
plt.show()
其输出为:
人机识别结果: 人类:37903,机器:2097,机器行为占比:5.24%
机器学习结果: 人类:39999,机器:1,机器行为占比:0.00%
以人机识别为参考系,机器学习正确率:94.75%
这里有点令人惊讶:假定人机识别的结果全部正确,那么聚类的“正确率”高达94.75%!——而实际上它判定出4w条行为样本中,只有1个机器行为。
造成这一情况的主要原因是,样本中机器行为的数量,可能本来就偏少——因此就算认为这4w条全部是人类行为,正确率都可以高于94.75%
不能说一定不可能,只能说难以令人信服(数据是完全随机的)。因此决定加入维度4测试
- 加入维度4
# -*- coding: utf-8 -*-
# 导入相应的包
from pyexcel_xls import get_data
import re
from sklearn.cluster import KMeans
import scipy.io as sio
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# 计算多边形面积
def getArea(pts):
s = 0
_len = len(pts)
x, y = zip(*pts)
j = _len - 1
for i in range(_len):
s = s + (x[j] + x[i]) * (y[j] - y[i])
j = i
return abs(s / 2.)
xls_data = get_data(r"test.xlsx")
# print ("Get data type:", type(xls_data))
# print (xls_data['ub'])
all_num = len(xls_data['ub']);
ub_dim_list = [[] for i in range(all_num)]
# print(ub_dim_list)
ub_res_list = []
count = 0
for row_data in xls_data['ub']:
# print (row_data[0])
ub_data = eval(row_data[0])
# print(ub_data['mp'])
mp_dim_split = re.split(';|,', ub_data['m3p'])
mp_dim_list = [[] for i in range(3)]
mp_dim = [0.0, 0.0]
for i in range(0, len(mp_dim_split)):
if (i % 2 == 0):
mp_dim[0] = float(mp_dim_split[i])
mp_dim[1] = float(mp_dim_split[i + 1])
mp_dim_list[int(i / 2)].extend(mp_dim)
# print(mp_dim_list)
# print(polyArea(mp_dim_list))
ub_res_list.append(row_data[1])
ub_dim_list[count].append(getArea(mp_dim_list))
ub_dim_list[count].append(ub_data['kc'])
ub_dim_list[count].append(ub_data['mc'])
ub_dim_list[count].append(ub_data['mmc'])
count += 1
# print (ub_dim_list)
# print('人机识别结果:', ub_res_list)
# 聚类,2个聚簇
clf = KMeans(n_clusters=2)
y_pred = clf.fit_predict(ub_dim_list)
y_pred = y_pred.tolist()
# print('机器学习结果:', y_pred)
import numpy as np
from matplotlib import pyplot as plt
from scipy import io as spio
from sklearn.decomposition import pca
from sklearn.preprocessing import StandardScaler
'''归一化数据并作图'''
scaler = StandardScaler()
scaler.fit(ub_dim_list)
x_train = scaler.transform(ub_dim_list)
print(x_train)
'''拟合数据'''
K=3 # 要降的维度
model = pca.PCA(n_components=K).fit(x_train) # 拟合数据,n_components定义要降的维度
print(model)
Z = model.transform(x_train) # transform就会执行降维操作
print(Z)
# print(Z[:,0])
fig = plt.figure()
ax_pca = fig.add_subplot(1, 1, 1, projection='3d')
ax_pca.view_init(elev=10., azim=11)
ax_pca.scatter(Z[:,0], Z[:,1], Z[:,2], c=y_pred)
'''拟合数据'''
K=2 # 要降的维度
model = pca.PCA(n_components=K).fit(x_train) # 拟合数据,n_components定义要降的维度
print(model)
Z = model.transform(x_train) # transform就会执行降维操作
print(Z)
# print(Z[:,0])
fig = plt.figure()
ax_pca = fig.add_subplot(1, 1, 1)
ax_pca.scatter(Z[:,0], Z[:,1], c=y_pred)
rjsb_num = [0, 0]
for i in ub_res_list:
if (i == 0):
rjsb_num[0] += 1
else:
rjsb_num[1] += 1
print('人机识别结果: 人类:' + str(rjsb_num[0]) + ',机器:' + str(rjsb_num[1]) + ',机器行为占比:' + '%.2f%%' % (
rjsb_num[1] / all_num * 100))
ml_num = [0, 0]
for i in y_pred:
if (i == 0):
ml_num[0] += 1
else:
ml_num[1] += 1
print('机器学习结果: 人类:' + str(ml_num[0]) + ',机器:' + str(ml_num[1]) + ',机器行为占比:' + '%.2f%%' % (ml_num[1] / all_num * 100))
right_num = 0
for i in range(0, all_num):
if ((ub_res_list[i] == y_pred[i] and ub_res_list[i] == 0) or (ub_res_list[i] != 0 and y_pred[i] == 1)):
right_num += 1
print('以人机识别为参考系,机器学习正确率:%.2f%%' % (right_num / all_num * 100))
plt.show()
其输出为:
人机识别结果: 人类:37903,机器:2097,机器行为占比:5.24%
机器学习结果: 人类:37799,机器:2201,机器行为占比:5.50%
以人机识别为参考系,机器学习正确率:89.79%
可以看出明显的人机界限。
在对机器行为的识别比例上,两者相差无几;只是有10%左右(大约4k条)的数据判定,两者看法是不一致的。
那么究竟是谁错了?目前还不得而知——这些数据来自浏览器的另一端,本身不会自带label说i'm from robot。但今后可以在测试环境,基于人的行为,同时基于自己编写的机器脚本,来做一次label确凿的比较。而如果是这样,就可以不仅于局限聚类分析了,一些有监督学习的分类算法也可以引入尝试。
另外单靠这种算法也并非万能的——攻击者也可以利用机器学习,模拟出“更合法的非法行为”。
未来势必是机器与机器的战争。
结语
笔者入坑机器学习时间较短,对一些算法的具体应用场景可能理解不够透彻。但思路上,希望能利用现有行为数据,动态的打造一个“用户行为模型”来强化公司人机识别产品。此文纯属抛砖引玉。希望有经验的朋友多多指正,谢谢。