分类 机器学习 下的文章

我对安全与NLP的实践和思考

先说结果,后谈过程。文章同步在个人微信公众号:我对安全与NLP的实践和思考,欢迎关注。

结果

通过对安全与NLP的实践和思考,有以下三点产出:

首先,产出一种通用解决方案和轮子,一把梭实现对各种安全场景的安全检测。通用解决方案给出一类安全问题的解决思路,打造轮子来具体解决这一类问题,而不是使用单个技术点去解决单个问题。具体来说,将安全与NLP结合,在各种安全场景中,将其安全数据统一视作文本数据,从NLP视角,统一进行文本预处理、特征化、预训练和模型训练。例如,在Webshell检测中,Webshell文件内容,在恶意软件检测中,API序列,都可以视作长文本数据,使用NLP技术进行分词、向量化、预训练等操作。同理,在Web安全中,SQLi、XSS等URL类安全数据,在DNS安全中,DGA域名、DNS隧道等域名安全数据,同样可以视作短文本数据。因此,只要安全场景中安全数据可以看作单变量文本数据,这种通用解决方案和轮子就适用,轮子开源在我的github仓库FXY中,内置多种通用特征化方法和多种通用深度学习模型,以支持多种安全场景的特征化和模型训练,达到流水线式作业。

其次,是对应用能力和底层能力的思考。之前写过一篇文章《应用型安全算法工程师的自我修养》,在我当时预期想法中,我理解的应用型,重点在于解决实际安全问题,不必苛求于对使用技术本身的理解深度,可以不具备研究型、轮子型的底层能力。映射到我自身,我做安全和算法,最初想法很好,安全和算法两者我都要做好,这里做好,仅仅指用好。之后,面试时暴露了问题,主管给出的建议是两者都要做好。这里做好,不单单指用好,还要知其所以然。举个例子,就是不仅要调包调参玩的6,还要掌握算法的底层原理,这就是底层能力。当时,懂,也不懂,似懂非懂,因为,说,永远是别人的,悟,才是自己的。在实现通用解决方案和轮子的过程中,遇到关于word2vec底层的非预期问题,才深刻体会到,底层能力对应用能力的重要性。过程中遇到的预期和非预期问题,下文会详述。现在我理解的应用型,重点还是在解决安全问题,以及对安全问题本身的理解,但应用型还需具备研究型、轮子型等上下游岗位的底层能力。安全算法是这样,其他细分安全领域也是一样,都需要底层能力,以发展技术深度。

最后,带来思考和认识的提升。从基于机器学习的XX检测,基于深度学习的XX检测,等各种单点检测,到基于NLP的通用安全检测,是一个由点到面的认知提升。从安全和算法都要做好,到安全和算法都要做好,其中蕴含着认知的提升。从之前写过一篇安全与NLP的文章《当安全遇上NLP》,到现在这篇文章。对一件事物的认识,在不同阶段应该是不一样的,甚至可能完全推翻自己之前的认识。我们能做的,是保持思考,重新认识过去的经历,提升对事物的认知和认知能力。这个提升认知的过程,类似boosting的残差逼近和强化学习的奖惩,是一个基于不知道不知道->知道不知道>知道知道->不知道知道的螺旋式迭代上升过程。

起源

促成对安全与NLP的实践和思考,起源于以下三点:

第一,也是最初的起源,之前写过一篇文章《FXY:Security-Scenes-Feature-Engineering-Toolkit》,FXY的定位是一款安全领域的特征工程框架,用于支撑上层的机器学习算法。当时是希望对每种安全场景,发挥安全领域知识,定制统计特征工程方法,预期是像写poc一样写特征化方法,像集成poc脚本到pocsuite中一样,集成特征化方法到FXY特征工程框架中,现在想来,当时的想法有点幼稚。因为全定制化开发,就算是将别人相关工作中的特征化方法,按照一定格式改写到FXY特征工程框架中来,工作量也太大。因此,怎么把FXY打造的更加实用,是我一直在思考的问题。

第二,起源于两个github项目。第一个是一个单纯的文本分类项目,作者是对3个文本数据集,使用1种特征化方法,训练13种机器学习和深度学习模型。这里我关注的点不是13种模型,学术界主刷models,我关注的是1种特征化方法,一种特征方法对三个文本数据集。第二个是早之前阿里云安全webber师傅写的基于深度学习的XSS检测的两篇文章和github仓库。这两篇文章使用1种文本数据特征化方法,训练4种模型,文章我在三个时期看过很多遍,每次的理解都不相同,但主要的思想都还是把安全数据当文本处理。当两个仓库撞在一起,想到的是将单点的XSS+word2vec+深度学习模型,向前扩展,扩展至文本数据类安全场景+NLP特征化方法+深度学习模型。对各类安全数据求同存异,‘同’是能否看作文本数据,一般只要能看作文本数据,例如域名数据、文件数据、序列数据,就可以使用统一的NLP特征化方法,‘异’是采用不同特征化方法如人工提取的特征千差万别。求同存异后,就可以用一类通用方法,统一处理多个安全场景,而不必根据每个安全场景定制化开发特征工程方法。

下图是两点起源相遇时顺手写下的idea。

image-20200410200426733.png

第三,指导思想是不断思考。第一次明确听到关于“思考”的字眼,是在实习刚入职那会,晚上七点左右主管找我的谈话,当时似乎是听懂了,现在回想起来,记得的只有两字“思考”。思考,是一种基础能力,促使我不断的对知识进行融合,思考每一种产生化学反应的可能性。

预期问题

基于以上三点起源,我开始从NLP视角重构FXY,争取打开突破口。下图为顺手记录过程中遇到的问题和解决方式。

image-20200410210736459.png

总计有20+个问题,我们把问题归类,分成安全场景、数据特征化、模型三类,其中问题较多的是数据特征化部分,重点说说这部分。

image-20200410214327089.png

按照流程的先后顺序,我们把问题划分在分词粒度、预训练前字典的建立、序列、词向量等部位。首先是分词粒度,粒度这里主要考虑字符粒度和词粒度。在不同的安全场景中,安全数据不同,采用的分词粒度也可能不同,例如用于恶意样本检测的动态API行为序列数据,需要进行单词粒度的划分。域名安全检测中的域名数据,最好采用字符粒度划分。URL安全检测中的URL数据,使用字符和单词粒度划分都可以。需要根据具体的安全场景,选择合适的粒度,FXY特征化类wordindex和word2vec中参数char_level实现了该功能。基于深度学习的XSS检测文中,是根据具体的XSS攻击模式,写成正则分词函数,对XSS数据进行划分,这是一种基于攻击模式的词粒度分词模式,但这种分词模式很难扩展到其他安全场景中。在其他安全场景中,可以根据此思路,写自定义的基于攻击模式的分词,但适用范围有限。我这里提供了两种通用词粒度分词模式,第一种是忽略特殊符号的简洁版分词模式,第二种是考虑全量特殊符号的完整版分词模式,这两种分词模式可以适用于各种安全场景中。FXY特征化类word2vec中参数punctuation的值‘concise’,‘all’和‘define’实现了两种通用分词和自定义安全分词功能。下文的实验部分,会测试不同安全场景中,使用字符粒度和词粒度,使用不同词粒度分词模式训练模型的性能对比。

其次是关于预训练前字典的建立。特征化类word2vec的预训练需求直接引发了字典建立的相关问题。在word2vec预训练前,需要考虑预训练数据的产生。基于深度学习的XSS检测文中,是通过建立一个基于黑样本数据的指定大小的字典,不在字典内的数据全部泛化为一个特定词,将泛化后的数据作为预训练的数据。这里我们将此思路扩充,增加使用全量数据建立任意大小的字典。具体到word2vec类中,参数one_class的True or False决定了预训练的数据来源是单类黑样本还是全量黑白样本,参数vocabulary_size的值决定了字典大小,如果为None,就不截断,为全量字典数据。下文的实验部分会测试是单类黑样本预训练word2vec好,还是全量数据预训练更占优势,是字典截断好,还是用全量字典来预训练好。

然后是关于序列的问题,具体地说,是长文本数据特征化需求,如下图中的webshell检测等安全场景,引发了序列截断和填充的问题。

image-20200410225942891.png

短文本数据的特征化,可以保留所有原始信息。而在某些安全场景中的长文本数据,特征化比较棘手,保留全部原始信息不太现实,需要对其进行截断,截断的方式主要有字典截断、序列软截断、序列硬截断。字典截断已经在上段说过了,序列软截断是指对不在某个范围内(参数num_words控制范围大小)的数据,直接去除或填充为某值,长文本选择直接去除,缩短整体序列的长度,尽可能保留后续更多的原始信息。如果长本文数据非常非常长,那么就算有字典截断和序列软截断,截断后的序列也可能非常长,超出了模型和算力的承受范围,此时,序列硬截断(参数max_length控制)可以发挥实际作用,直接整整齐齐截断和填充序列,保留指定长度的序列数据。这里需要注意的是,为了兼容后文将说到的“预训练+微调”训练模式中的预训练矩阵,序列填充值默认为0。

最后,是词向量的问题,具体说,是词嵌入向量问题。词嵌入向量的产生有三种方式:词序列索引+有嵌入层的深度学习模型、word2vec预训练产生词嵌入向量+无嵌入层的深度学习模型、word2vec预训练产生预训练矩阵+初始化参数为预训练矩阵的嵌入层的深度学习模型。这里我把这三种方式简单叫做微调、预训练、预训练+微调,从特征工程角度,这三种方式是产生词嵌入向量的方法,从模型角度,也可以看作是模型训练的三种方法。第一种微调的方式实现起来比较简单,直接使用keras的文本处理类Tokenizer就可以分词,转换为词序列,得到词序列索引,输入到深度学习模型中即可。第二种预训练的方式,调个gensim库中word2vec类预训练,对于不在预训练字典中的数据,其词嵌入向量直接填充为0,第三种预训练+微调的方式,稍微复杂一点,简单来说就是前两种方式的组合,用第二种方式得到预训练矩阵,作为嵌入层的初始化权重矩阵参数,用第一种方式得到词序列索引,作为嵌入层的原始输入。下文的实验部分会测试并对比按这三种方式训练模型的性能,先说结论:预训练+微调>预训练>微调。

非预期问题

预期问题,说到底都是应用层面的问题,都比较好解,非预期问题比较难顶,有些涉及到底层的知识,这正是引发我对应用能力和底层能力思考的原因。

第一个非预期问题是,已知的库和函数不能满足我们的需求。一般来说,使用keras的文本处理类Tokenizer预处理文本数据,得到词序列索引,完全没有问题。但类Tokenizer毕竟是文本数据处理类,没有考虑到安全领域的需求。比如类Tokenizer的单词分词默认会过滤所有的特殊符号,仅保留单词,而特殊符号在安全数据中是至关重要的,很多payload的构成都有着大量特殊符号,忽略特殊符号会流失部分原始信息。虽然类Tokenizer的单词分词可以不过滤特殊符号,但其分词的自由度有限,我们需要对其魔改。首先阅读了keras的文本处理源码和序列处理源码,不仅搞懂了其结构和各函数的底层实现方式,还学到了一些tricks和优质代码的特性。下图为Tokenizer类的结构。借鉴并改写Tokenizer类,加入了多种分词模式,我们实现了wordindex类。

keras-text-review.png

第二个非预期问题是,对word2vec的理解不到位,尤其是其底层原理和代码实现,导致会有一些疑惑,无法得到验证,这是潜在的问题。虽然可以直接调用gensim库中的word2vec类暂时解决问题,但我还是决定把word2vec深究深究,一方面可以答疑解惑,另一方面,就算不能调用别人的库,自己也可以造轮子自给自足。限于篇幅问题,不多讲word2vec的详细原理,原理是我们私下里花时间可以搞清楚的,不算是干货,对原理有兴趣的话,这里给大家推荐几篇优质文章,在github仓库Always-Learning中。

image-20200411153824223.png

这里,只以其中的关键点之一“负采样”来举例。word2vec本质上是一个神经网络模型,具体来说此神经网络模型是一个输入层-嵌入层-输出层的三层结构,我们用到的词嵌入向量只是神经网络模型的副产物,是模型嵌入层的权重矩阵。以word2vec实现方式之一的skip-gram方法为例,此方法本质是通过中心词预测周围词。如果有一段话,要对这段话训练一个word2vec模型,那么很明显需要输入数据,还要是打标的数据。以这段话中的某个单词为中心词为例,在一定滑动窗口内的其他单词都默认和此单词相关,此单词和周围其他单词,一对多产生多个组合,默认是相关的,因此label为1,即是输入数据的y为1,而这些单词组合的one-hot编码是输入数据的x。那么很明显label全为1,全为positive sample,需要负采样来中和。这里的负采样不是简单地从滑动窗口外采样,而是按照词频的概率,取概率最小的一批样本来做负样本(这个概念下面马上要用到),因为和中心词毫不相关,自然label为0。负采样的原理到这里简单说完,talk “talk is cheap,show me your code” is cheap,看到对应代码的实现才心里踏实。word2vec的底层代码实现有多种方式,有tensorflow1.x版的,有keras版的,为了适应新版tensorflow,我用tensorflow2.x改写了tensorflow1.x版的word2vec,几种不同框架及框架版本实现的word2vec代码在FXY仓库的tutorials文件夹。其实原理都相同,只是其中的写法不同。以原生的tensorflow2.x版为例,跟踪一下负采样的实现,tensorflow中的nce_loss函数实现了loss和负采样。

image-20200411161310765.png

以负采样参数num_sampled为线索,跟进nce_loss函数,

image-20200411162156323.png

跟进_compute_sampled_logits()函数,该函数负责采样,

image-20200411162357196.png

一直往下跟,遇到点问题,tensorflow的超底层代码太难懂,没有明确跟到负样本的产生。这里,联想上面我们说到负采样是按照词频的概率,取概率最小的一批样本来做负样本,再对应到tensorflow官方实现的word2vec代码中的下段代码,下段代码实现了词频排序。

image-20200411165843584.png

那么log_uniform_candidate_sampler函数极有可能利用参数labels、num_sampled、num_classes生成一批接近vocabulary_size大小的随机数,作为负样本。

第三个非预期问题是装备太差。由于没带电脑回家,加上疫情的影响,只好租了台辣鸡笔记本,再搭载手机热点100K以下的强势网速,收集数据,跑代码,出现一些问题。比如4G内存跑代码的时候总报内存错误,只好使用不吃内存的代码重写了报错代码。受限于网速、硬件、软件等,一方面制约了学习,另一方面又无意中优化了代码,使得在辣鸡电脑上都能正常运行。

轮子的能力和构成

解决了这一系列问题,轮子的雏形终于显现,轮子内置多种通用特征化方法和多种通用深度学习模型,以支持多种安全场景的特征化和模型训练,达到流水线式作业。最初的考虑是只做特征工程,不向后兼容,即不加入模型部分,同学们自己做模型训练。之后细想,需要向后兼容,模型部分模型不在多,但起码要有几种模型作为demo,这样就形成了一个管道,一条龙作业。

轮子的具体构成包括:安全数据类、特征类、算法类。

首先是安全数据,目前收集、测试通过了6种安全数据,之后会继续扩充。

image-20200411185410705.png

文件命名中第一个A/B标记了数据是否异源(这里对异源的定义是数据来自不同的github仓库),第二个A/B标记了训练集/测试集。

其次是特征类,目前实现了tfidf类、wordindex类和word2vec类,其中wordindex和word2vec是重点。

wordindex类主要是将安全数据转换为词序列向量,参数主要有:num_words、char_level、max_length、punctuation。num_words是序列软截断参数,如果设置为1000,则字典大小为1000个词,不在此范围内的词会被泛化。char_level取值True or False,True指按字符粒度分词,False指按单词粒度分词。max_length是序列硬截断参数,如果设置为100,则序列长度会被截断/填充到100。punctuation是分词模式参数,需要char_level=False为前提,如果值为‘concise’,即忽略所有特殊符号,值为‘all’,将所有特殊符号都分词,值为‘define’,调用自定义分词模式。

word2vec类主要是通过预训练将安全数据转换为词嵌入向量,参数主要有:punctuation、tunning、one_class、out_dimension、vocabulary_size、num_words、max_length、embedding_size。tunning值是否是”预训练+微调“模式,值为True or False。one_class指预训练的来源是单类黑样本还是全量样本,值为True or False。out_dimension指输出向量是三维向量还是二维向量,三维词嵌入向量是为了对接深度学习模型,二维词嵌入向量可以对接机器学习模型,值为2或3。vocabulary_size指预训练前的字典的大小,可以保证快速预训练。embedding_size指词嵌入向量的维度。

最后是算法类,目前包括lstm和textcnn两个模型,两个模型中内置3种训练方式,分别用于对接wordindex类、word2vec类的预训练模式、word2vec类的预训练+微调模式。

测试

使用其中part1A_url.csv和part1B_url.csv数据集,测试轮子鲁棒性的同时,给出系列参数设置及对应的结果,限于篇幅,详细测试报告在FXY仓库docs文件夹中。

测试结果分析

  1. 分词模式考虑全部特殊符号比忽略特殊符号,最终效果要好,因为在很多payload中特殊符号有一定占比。

  2. 根据攻击模式自定义分词模式,效果最好。

  3. 预训练的数据不是越多越好,字典也不是越大越好,随着数据量的增大,性能有上限,该截断就截断。

  4. 训练模式效果对比一般有:预训练+微调>预训练>微调。

  5. 无论是同源数据还是异源数据,只有其攻击模式类似,模型效果就可以泛化到。

这篇文章是对我部分工作的一个总结,把零零散散的单点串成线,站在一个更高的视角看待问题。真相在第五层,现在我以为我看到了第二层,但我可能还在第一层,我们能做的是,唯有不断学习,不断思考。

完整代码及相关文档在我的githubhttps://github.com/404notf0und/FXY

推荐阅读

  1. 应用型安全算法工程师的自我修养

  2. 当安全遇上NLP

  3. FXY:Security-Scenes-Feature-Engineering-Toolkit

  4. Always-Learning