Bubbles~blog

用爱发电

基于SVM的简单XSS检测模型实践(续)

其实这个模型之前已经做过,不过那时候只是弄了个大概,做了个非常简单的模型,真要说起来完成的部分也就数据的特征化向量化,这次把后面的调整部分一并做一下

走进SVM

SVM是一种主要用于监督学习的分类算法,简单来说,当目标是只有二维的向量时,那么SVM要完成的就是尝试用一条直线将训练集中的两类数据分隔开,当然如果只是分隔开可能会找到很多直线,而SVM找到的这条直线距两个不同的类的距离是一样远的,也就是恰好在两个类的中间,更严格一点来说,这条直线距两个类里最近的元素的距离都是最远的。

当然,这只是最简单的情况,还有很多时候可能这两个类无法被一条直线分隔开,这时候就要引入更多的维度了,核函数开始发挥它的用场,在此我们就不展开讨论了,有兴趣的可以去深入研究,其实对具体的实现上自己也是半吊子

附上传送门 支持向量机导论

其实具体算法没看懂问题也不大,在工程上我们会用就行了,分类算法的非黑即白的划分方式也比较适合我们的这一任务

构建模型识别XSS

环境准备

我们要用到的是python的Scikit-Learn机器学习模块,里面包含了很多算法,使用起来非常方便,直接pip安装即可,同时需要的依赖还有numpy和Scipy,都是相关的工具库

搜集数据

在此我们需要大量的正常的web访问日志和包含xss攻击的web访问日志,比较直接的方法就是直接拿着扫描器像是awvs等之类的使用xss扫描去扫你的服务器得到payload样本,不过我试过以后发现数据集的大小并不是很尽如人意,可能也就差不多数千条这样,而且也没找到足够的正常web日志数据集,github上也有一些payloads的集合质量挺不错,不过数量也并不多,虽然也可以根据这些数据集写脚本自己继续制造payload,不过图方便我这里还是直接用了兜哥的数据集
附上传送门
含xss的web日志
正常web日志

这两个都包含这20万份记录,算是比较好的训练集了,也算是偷个懒吧,毕竟咱也没有环境去产生这么大量的数据

特征化

接下来就要将我们的数据进行特征化,也就是将它们变成向量的形式,同时也是数量化,毕竟这样才方便机器去识别理解我们的数据

比如说,我们来看几个典型的xss

<script>alert(123)</script>
 
<img src=x onerror=alert(xss)/>
 
<img src="javascript:alert('XSS');">
 
<SCRIPT a=">" SRC="http://hacker.gg/xss.js"></SCRIPT>

可以看到攻击语句中实际上包含着许多的特征,比如括号啊,标签啊,单双引号,script等等,我们现在要做的就是计量它们

import re
def get_len(url):
    return len(url)
 
def get_url_count(url):
    if re.search('(http://)|(https://)', url, re.IGNORECASE) :
        return 1
    else:
        return 0
 
def get_evil_char(url):
    return len(re.findall("[<>,\'\"()/]", url, re.IGNORECASE))
 
def get_evil_word_1(url):
    return len(re.findall("(alert)|(script=)|(eval)|(src=)|(prompt)",url,re.IGNORECASE))
 
def get_evil_word_2(url):
    return len(re.findall("(%3c)|(%3e)|(%20)",url,re.IGNORECASE))
 
def get_evil_word_3(url):
    return len(re.findall("(iframe)|(href)|(javascript)|(data)",url,re.IGNORECASE))
 
def get_evil_word_4(url):
    return len(re.findall("(onerror)|(onload)|(onfocus)|(onmouseover)",url,re.IGNORECASE))
 
def get_evil_word_5(url):
    return len(re.findall("(string.fromcharcode)|(document.cookie)",url,re.IGNORECASE))
 
def get_feature(url):
    return [get_len(url),get_url_count(url),get_evil_char(url),get_evil_word_1(url),get_evil_word_2(url),get_evil_word_3(url),get_evil_word_4(url),get_evil_word_5(url)]

当然,这里我们不可能将xss的所有可能方式都照顾到,在这挂个黑名单出来,这也是机器学习的方式跟传统的检测机制不同的地方,它只是去分析规律,在大量样本中不断学习罢了

在这里我将这些特征大致分类了一下,算是应对不同的payload,将很多可能的恶意字符串分开也是为了建立更多的维度,它们的选择大致也是对应相应的payload形式,但是也要注意不要建立太多特征以免造成过拟合

标准化

当然仅仅特征化是不够的,我们可以看到我们的特征向量大的大小的小,跨度比较大,url长度可能长达五六十而我们的特征词可能也就几个甚至0,所以我们要对他们进行一个放缩处理,在这里我们直接将所有的特征值放缩到0到1之间,当然你也可以选择其他的方法来完成这一过程,在sklearn的preprocessing模块里有很多方法

因为我们的特征值里可能会有很多值为0的项,所有选择这种方法比较有利于维持我们的特征

因为是放缩到0到1,所以此处选择MinMaxScaler函数

from sklearn import preprocessing
 
min_max_scaler = preprocessing.MinMaxScaler()
 
x_min_max=min_max_scaler.fit_transform(x)

现在我们的特征值就全部处于0和1的区间内了,其实这也算是归一化了

有人可能也想尝试使用正则化处理,不过个人觉得在这个模型里的效果应该不是太好,而且也加大了复杂度,如果是在逻辑回归里倒是会有不错的表现

标记数据

这一步一般在机器学习的任务里算是比较乏味的,特别是如果你搜集的数据是散乱的时候,这里因为我们拿到的数据集已经是分割好的,所以也就省去了我们的劳动,现在还有很多专门做这种给数据打标签的生意的

def labels(filename,data,label):
        with open(filename) as f:
            for line in f:
                data.append(get_feature(line))
                if label:
                    y.append(1)
                else:
                    y.append(0)
        return data
 
labels('xss-200000.txt',x,1)
labels('good-xss-200000.txt',x,0)

划分train与test

现在我们需要把数据划分为训练集与测试集,一般默认的比例是6比4,当然还有交叉验证集也可选,我们暂且先这么用着

此处用到的是sklearn的cross_validation模块,从名字我们也可以看出它的用途了
它包含很多划分数据集的方法,我们一般常用的是train_test_split,它是用来从数据集中随机地按比例选取训练集和验证集,其他的方式根据不同的需求自然也有不同的作用

用法是

from sklearn import cross_validation
 
x_train, x_test, y_train, y_test = cross_validation.train_test_split(x,y, test_size=0.4, random_state=0)

test_size就是验证集所占比例大小,random_state是生成随机数的种子,不同的种子采样结果就不同,相同的种子则一样

训练模型

之前我做这一步的时候直接使用的是线性核,也就是linear,因为这样也比较简单而且效果还不错,不过正常情况下我们对于模型的选择肯定是经过对比再得到结果的,svm中也有很多核函数可供选择,不过一般用的最多的还是线性核与高斯核,我们在此依然是针对它们二者进行比较并调参

在coursera的教程里我们也见过了使用验证曲线来进行参数选择的方法,不过在这我们将尝试另一种叫做Grid search的方法,简单来说其实就是设定一组参数值然后进行暴力穷举所有的参数组合来寻找最佳的参数,这也是寻找很多数据竞赛里常用的调参方法

然后我们再看支持向量机的分类模型SVC有哪些参数

首先核函数的选择上我们有kernel和rbf两个选择,然后就是核函数内的参数,对于线性核我们不需要设置什么,而对于高斯核我们还需要设置它的核函数的系数,一般而言取特征数分之一附近比较合适,这里我就选择0.1和0.2,然后是SVM的惩罚系数C,默认是1,这里我选择0.1,1,10

关于rbf的调参有兴趣的可以去看看相关文章,这里附上一篇传送门
[支持向量机高斯核调参小结](http://www.cnblogs.com/pinard/p/6126077.html)

下面我们将备选参数列出来

tuned_parameters = [{'kernel': ['rbf'], 'gamma': [0.1, 0.2],
                     'C': [0.1,1,10]},
                    {'kernel': ['linear'], 'C': [0.1,1,10]}]

然后我们来选定评估的标准,在coursera的教程里我们也知道了对于一个模型的表现好坏我们可以用准确率(precision)和召回率(recall)来评定,所以在Grid search的过程中我们将计算每种参数组合得到的模型的组合的准确率和召回率并进行比较

当然,在评估模型之前我们也不能忘了做交叉验证,也就是Cross Validation,在coursera的课程里也接触过它,不过使用的还是比较初级的验证方法,只是把数据集随机分成了三组:训练集,验证集和测试集,这样不同的划分还是可能会有不同的结果,而且用于训练的数据也变少了,现在一般使用k折交叉验证(k-fold cross validation)

k 折交叉验证通过对 k 个不同分组训练的结果进行平均来减少方差,因此模型的性能对数据的划分就不那么敏感。

第一步,不重复抽样将原始数据随机分为 k 份

第二步,每一次挑选其中 1 份作为测试集,剩余 k-1 份作为训练集用于模型训练

第三步,重复第二步k次,这样每个子集都有一次机会作为测试集,其余机会作为训练集,
在每个训练集上训练后得到一个模型,用这个模型在相应的测试集上测试,计算并保存模型的评估指标

第四步,计算k组测试结果的平均值作为模型精度的估计,并作为当前k折交叉验证下模型的性能指标。

如果你对集成学习的stacking有所了解你会发现二者的思想其实是差不多的,了解的越多以后你会发现机器学习其实还是很有趣的

回到我们的模型上来,交叉验证其实已经在GridsearchCV类里封装好了,我们只用输入参数cv即可,一般而言cv取5就够了,当然也有很多人会选择10甚至更多,这也意味着更多的资源的损耗,会让你的训练过程慢很多,实话说真要是要求高的话做cv也是门学问,我们这里只不过简单调用了GridsearchCV类里的cv方法,实际问题里可能会遇到很多仅仅使用这种简单的cv方法无法解决的问题,所以优化的道路还很长很远

from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
 
scores = ['precision', 'recall']
 
for score in scores:
    print("#  对于评估参数 %s" % score)
    print()
 
    # 调用 GridSearchCV,开始传参,选用SVM的分类模型SVC,后面跟上要组合的参数,cv值以及评估参数
    clf = GridSearchCV(SVC(), tuned_parameters, cv=5,
                       scoring='%s_macro' % score)
    # 用训练集训练这个学习器 clf
    clf.fit(x_train, y_train)
 
    print("Best parameters set found on development set:")
    print()
 
    # 再调用 clf.best_params_ 就能直接得到最好的参数搭配结果
    print(clf.best_params_)
 
    print()
    print("Grid scores on development set:")
    print()
    means = clf.cv_results_['mean_test_score']
    stds = clf.cv_results_['std_test_score']
 
    #看一下具体的参数间不同数值的组合后得到的分数是多少
    for mean, std, params in zip(means, stds, clf.cv_results_['params']):
        print("%0.3f (+/-%0.03f) for %r" % (mean, std * 2, params))
 
    print()
    y_true, y_pred = y_test, clf.predict(x_test)
 
    # 在测试集上的预测结果与真实值的分数
    print(classification_report(y_true, y_pred))
 
    print()

我们再来看看结果,说实话这个确实要训练挺久的,所以也不是很推荐给它设定很多的备选值,我这几种参数就已经有九种组合方式了,再增加需要的时间也更多,,当然真要有兴趣把这些范围再继续细分也可以,慢慢等就行了

先来看准确率的验证结果

可以看到表现最好的是系数为0.1的高斯核与C为10的组合

再来看召回率

这里表现最好的是系数为0.1的高斯核与C为1的组合

我们也注意到后面的评估参数里还有个f1-score,这其实是准确率和召回率的调和均值,也就是个综合的评价指标

针对我们的需求,既然是做一个xss的检测模型,那么当然就要力求把所有的xss攻击都分辨出来,也就是务必要做到找的全,所以我们更注重召回率的结果,本来这两个差的也不多,所以我们调参的结果就是选择使用系数为0.1的高斯核与参数C为1的SVC分类模型

到这我们的模型选择就做完了,看似我们好像做了很多的调试,实际上我们的模型依然是很简单的,其实对于这种分类问题反应比较好的是一些基于树的模型,像是随机森林,GBDT等,说实话这些模型还是比较复杂的,不过毕竟现在集成学习才是王道,随机森林和GBDT其实也是这种思想的代表,还有现在在数据竞赛里异常火热的xgboost,我们使用的svm倒是可以作为弱分类器当ensemble learning的base model,不过效果可能没有决策树要好

有兴趣的可以拿GBDT或者xgboost重试这个模型,效果可能不一定会更好,但是更复杂倒是真的,因为svm本身也算一个强分类器了

实话说我自己对于集成学习也是一知半解,想了解更多的建议去找相关书籍看看

模型评估

虽然之前做Grid search的时候我们已经做过了评估,不过这里我们还是做一个结果的展现在sklearn中也给我们提供了相关的模块,这里我们使用的是它的metric模块

就直观效果而言,我们可以选择accuracy_score函数,它会使用我们训练的模型对test集进行验证,最终输出predict结果的准确率

from sklearn import metrics
 
y_pred = clf.predict(x_test)
 
print (metrics.accuracy_score(y_test, y_pred))

当然,准确率(precision)和召回率(recall)也是我们衡量一个模型的重要标准,通常我们都希望这两个值越大越好

这些也在metric模块里得到了很好的支持

print (metrics.precision_score(y_test, y_pred))
 
print (metrics.recall_score(y_test, y_pred))

一般测试的情况下准确率还是能达到95%以上的,表现还算尚佳

保存模型

最后如果模型的训练结果让你还满意的话你就可以将它保存下来了

from sklearn.externals import  joblib
 
joblib.dump(clf,"xss_svm_module.m")

取用的时候直接load即可

clf=joblib.load("xss_svm_module.m")

End

虽然乍看一下准确率貌似挺高,但实际上哪怕达到了99.99%也依然是不安全的,都是可以随便绕过的,而且它对于各种编码混淆后的payload的检测也不理想,毕竟这种payload特征提取是很困难的,所以我们这里的模型也没有针对它们进行检测,哪怕你添加了特征对于我们这个模型而言意义也不大,很容易就会被忽略甚至抛弃,还可能影响我们的结果,所以要说单单拿机器学习生成的模型去检测目前来看还是不现实的,跟其他的检测机制相结合还有一点实用性,不过机器学习在安全领域的应用应该还是有很大的发展空间的,而且我们的模型依然是一个简单的模型,真正实际中的训练可能会调用多重的分类器进行训练与集成,这也意味着资源消耗的倍增,我的破机子跑起来可能都疼,所以这次也只是注重一个练习,优化之路依然漫漫