网络安全的机器学习秘籍-一-

82 阅读38分钟

网络安全的机器学习秘籍(一)

原文:annas-archive.org/md5/ccb0ce37f50c14ac9195f73f7edac92e

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

当前的网络威胁是每个组织面临的关键问题之一。本书使用了多种 Python 库,如 TensorFlow、Keras、scikit-learn 等,揭示了网络安全研究人员面临的常见及不常见的挑战。

本书将帮助读者实施智能解决方案,以应对现有的网络安全挑战,并构建前沿的实现方案,以满足日益复杂的组织需求。通过本书的学习,读者将能够使用机器学习ML)算法,通过基于实例的方法,遏制网络安全威胁。

本书适合的人群

本书面向网络安全专业人员和安全研究人员,帮助他们通过实现机器学习算法和技术,提升计算机安全技能。基于实例的本书也适合那些希望将智能技术引入网络安全领域的数据科学家和机器学习开发人员。本书要求具备 Python 的基础知识,并且熟悉网络安全的基本概念。

本书的内容

第一章,网络安全的机器学习,介绍了用于网络安全的机器学习基本技术。

第二章,基于机器学习的恶意软件检测,展示了如何对样本进行静态和动态分析。你还将学习如何应对网络安全领域中机器学习面临的重要挑战,如类别不平衡和假阳性率FPR)约束。

第三章,高级恶意软件检测,涵盖了恶意软件分析的更高级概念。我们还将讨论如何处理混淆和打包的恶意软件,如何扩展 N-gram 特征的收集,以及如何使用深度学习来检测甚至创造恶意软件。

第四章,社交工程中的机器学习,解释了如何使用机器学习构建一个 Twitter 钓鱼机器人。你还将学习如何使用深度学习来让目标说出你希望他们说的任何话。本章还会讲解一个谎言检测周期,并展示如何训练递归神经网络RNN),使其能够生成与训练数据集中的评论相似的新评论。

第五章,使用机器学习进行渗透测试,涵盖了广泛的机器学习技术,用于渗透测试和安全防护措施。它还涵盖了一些更专业的主题,如去匿名化 Tor 流量、通过击键动态识别未授权访问,以及检测恶意网址。

第六章,自动入侵检测,介绍了使用机器学习设计和实现几种入侵检测系统。它还讨论了依赖示例、成本敏感、极度不平衡且具有挑战性的信用卡欺诈问题。

第七章,利用机器学习保护和攻击数据,涵盖了如何利用机器学习保护和攻击数据的相关方案。它还讨论了如何应用机器学习来攻击硬件安全,利用人工智能攻击 物理不可克隆函数PUFs)。

第八章,安全与隐私的人工智能,解释了如何使用 TensorFlow Federated 框架中的联邦学习模型。它还包括加密计算基础的操作步骤,并展示了如何使用 Keras 和 TensorFlow Privacy 实现并训练一个差分隐私的深度神经网络来处理 MNIST 数据集。

附录 提供了创建基础设施以应对网络安全数据上机器学习挑战的指南。本章还提供了使用虚拟 Python 环境的指南,允许你在不同的 Python 项目上无缝工作,避免包冲突。

如何最大化利用本书

你需要具备基本的 Python 知识和网络安全知识。

下载示例代码文件

你可以从你的账户下载本书的示例代码文件,访问 www.packt.com。如果你是在其他地方购买的本书,你可以访问 www.packtpub.com/support 并注册,直接将文件通过电子邮件发送给你。

你可以按照以下步骤下载代码文件:

  1. 登录或注册 www.packt.com

  2. 选择 "支持" 标签。

  3. 点击 "代码下载"。

  4. 在搜索框中输入书名并按照屏幕上的指示操作。

文件下载完成后,请确保使用最新版本的工具解压缩或提取文件夹:

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,链接为 github.com/PacktPublishing/Machine-Learning-for-Cybersecurity-Cookbook。如果代码有更新,GitHub 上的现有仓库也会更新。

我们还提供了来自我们丰富书籍和视频目录中的其他代码包,访问 github.com/PacktPublishing/ 查看!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载: static.packt-cdn.com/downloads/9781789614671_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:指示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号名。例如:“将标签附加到X_outliers。”

一段代码的设置如下:

from sklearn.model_selection import train_test_split
import pandas as pd

任何命令行输入或输出都如下所示:

pip install sklearn pandas

粗体:表示新术语、重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇以这种方式出现在文本中。示例:“超参数调整的最基本方法叫做网格搜索。”

警告或重要提示以这种方式呈现。提示和技巧以这种方式呈现。

章节

本书中有几个常见的标题(准备工作如何做...原理...还有更多...,和另见)。

为了清晰地说明如何完成一个教程,使用以下这些章节:

准备工作

本节介绍您在执行教程时的预期,并描述了设置所需软件或任何必要的初始设置。

如何做…

本节包含完成教程所需的步骤。

它是如何工作的…

本节通常包含对上一节的详细解释。

还有更多…

本节包含关于教程的更多信息,帮助您更好地理解该教程。

另见

本节提供了与教程相关的其他有用信息链接。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提到书名,并通过customercare@packtpub.com联系我们。

勘误:尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现了错误,我们非常感激您能向我们报告。请访问www.packt.com/submit-erra…,选择您的书籍,点击勘误提交表单链接,并填写相关细节。

盗版:如果您在互联网上发现任何我们作品的非法复制品,我们非常感激您能提供该材料的网址或网站名称。请通过copyright@packt.com与我们联系,并提供该材料的链接。

如果您有意成为作者:如果您在某个领域有专业知识,并且有意写书或为书籍贡献内容,请访问authors.packtpub.com

评论

请留下评论。在您阅读并使用本书后,为什么不在购买该书的网站上留下评论呢?潜在的读者可以通过查看并利用您的公正意见来做出购买决定,我们在 Packt 能了解您对我们产品的看法,作者也可以看到您对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问packt.com

第一章:网络安全中的机器学习

在本章中,我们将介绍机器学习的基本技术。我们将在全书中使用这些技术来解决有趣的网络安全问题。我们将涵盖基础算法,如聚类和梯度提升树,并解决常见的数据挑战,如数据不平衡和假阳性约束。在网络安全领域,机器学习实践者处于一个独特且令人兴奋的位置,能够利用大量数据并在不断发展的环境中创造解决方案。

本章涵盖以下内容:

  • 训练-测试分割你的数据

  • 标准化你的数据

  • 使用主成分分析PCA)总结大型数据

  • 使用马尔可夫链生成文本

  • 使用 scikit-learn 进行聚类

  • 训练 XGBoost 分类器

  • 使用 statsmodels 分析时间序列

  • 使用 Isolation Forest 进行异常检测

  • 使用哈希向量器和 tf-idf 与 scikit-learn 进行自然语言处理NLP

  • 使用 scikit-optimize 进行超参数调整

技术要求

在本章中,我们将使用以下内容:

  • scikit-learn

  • Markovify

  • XGBoost

  • statsmodels

安装说明和代码可以在github.com/PacktPublishing/Machine-Learning-for-Cybersecurity-Cookbook/tree/master/Chapter01找到。

训练-测试分割你的数据

在机器学习中,我们的目标是创建一个能够执行从未被明确教过的任务的程序。我们实现这一目标的方法是利用我们收集的数据来训练拟合一个数学或统计模型。用于拟合模型的数据称为训练数据。训练得到的模型随后用于预测未来的、以前未见过的数据。通过这种方式,程序能够在没有人工干预的情况下处理新情况。

对于机器学习实践者来说,一个主要的挑战是过拟合的风险——即创建一个在训练数据上表现良好,但无法对新的、从未见过的数据进行推广的模型。为了应对过拟合问题,机器学习实践者会预留出一部分数据,称为测试数据,并仅用于评估训练模型的性能,而不是将其包含在训练数据集中。精心预留测试集是训练网络安全分类器的关键,在这里,过拟合是一个无处不在的危险。一个小小的疏忽,例如仅使用来自某一地区的良性数据,可能导致分类器效果差。

有多种方法可以验证模型性能,比如交叉验证。为了简化,我们将主要关注训练-测试分割。

准备工作

本教程的准备工作包括在pip中安装 scikit-learn 和pandas包。安装命令如下:

pip install sklearn pandas

此外,我们还提供了north_korea_missile_test_database.csv数据集,以供本教程使用。

如何操作……

以下步骤演示了如何将一个数据集(包含特征X和标签y)拆分为训练集和测试集:

  1. 首先导入train_test_split模块和pandas库,并将特征读取到X中,将标签读取到y中:
from sklearn.model_selection import train_test_split
import pandas as pd

df = pd.read_csv("north_korea_missile_test_database.csv")
y = df["Missile Name"]
X = df.drop("Missile Name", axis=1)
  1. 接下来,随机将数据集及其标签拆分为一个训练集(占原始数据集的 80%)和一个测试集(占原始数据集的 20%):
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=31
)
  1. 我们再次应用train_test_split方法,以获得一个验证集,X_valy_val
X_train, X_val, y_train, y_val = train_test_split(
    X_train, y_train, test_size=0.25, random_state=31
)
  1. 我们最终得到了一个训练集,占原始数据的 60%,一个验证集占 20%,一个测试集占 20%。

以下截图显示了输出结果:

它是如何工作的……

我们从读取数据集开始,数据集包含朝鲜的历史和持续的导弹实验。我们的目标是根据剩余特征(如设施和发射时间)预测导弹类型。这是步骤 1 的内容。在步骤 2 中,我们应用 scikit-learn 的train_test_split方法将Xy细分为一个训练集X_trainy_train,以及一个测试集X_testy_testtest_size = 0.2参数表示测试集占原始数据的 20%,其余部分放入训练集中。random_state参数允许我们复现相同的随机生成的拆分。接下来,关于步骤 3,需要注意的是,在实际应用中,我们通常希望比较几个不同的模型。使用测试集选择最佳模型的危险在于,我们可能会过度拟合测试集。这类似于数据钓鱼的统计学错误。为了应对这一危险,我们创建了一个额外的数据集,称为验证集。我们在训练集上训练模型,使用验证集进行比较,最后使用测试集来获得我们选择的模型的准确性能指标。因此,在步骤 3 中,我们选择参数,使得从数学角度来看,最终结果包含 60%的训练集,20%的验证集和 20%的测试集。最后,我们通过使用len函数来计算数组的长度,来仔细检查我们的假设(步骤 4)。

标准化你的数据

对于许多机器学习算法,性能对特征的相对尺度非常敏感。因此,通常需要对特征进行标准化。标准化特征意味着将其所有值平移,使其均值为 0,并对其进行缩放,使其方差为 1。

标准化在某些情况下非常有用,特别是在处理文件的 PE 头信息时。PE 头信息包含极大的数值(例如,SizeOfInitializedData 字段),也包含非常小的数值(例如,节区的数量)。对于某些机器学习模型,如神经网络,特征间的巨大差异会降低模型的表现。

准备工作

本示例的准备工作包括在 pip 中安装 scikit-learnpandas 包。请执行以下步骤:

pip install sklearn pandas

此外,你将在本仓库中找到一个名为 file_pe_headers.csv 的数据集,供本示例使用。

如何实现...

在接下来的步骤中,我们使用 scikit-learn 的 StandardScaler 方法来标准化数据:

  1. 首先,导入所需的库并收集数据集 X
import pandas as pd

data = pd.read_csv("file_pe_headers.csv", sep=",")
X = data.drop(["Name", "Malware"], axis=1).to_numpy()

数据集 X 如下所示:

  1. 接下来,使用 StandardScaler 实例对 X 进行标准化:
from sklearn.preprocessing import StandardScaler

X_standardized = StandardScaler().fit_transform(X)

标准化后的数据集如下所示:

工作原理...

我们从读取数据集开始(步骤 1),该数据集包含一组 PE 文件的 PE 头信息。不同的列差异很大,有些列的数据量达到数十万文件,而有些则只有个位数。因此,某些模型,如神经网络,在处理这些非标准化数据时表现不佳。在步骤 2 中,我们实例化了StandardScaler(),然后应用.fit_transform(X)X 进行重新缩放。最终,我们获得了一个重新缩放的数据集,其中的列(对应特征)的均值为 0,方差为 1。

使用主成分分析(PCA)对大数据进行总结

假设你想要构建一个预测模型,预测某人在 45 岁时的预期净资产。需要考虑的变量有很多:智商、当前净资产、婚姻状况、身高、地理位置、健康状况、教育背景、职业状态、年龄等等,甚至可能包括 LinkedIn 连接数量或 SAT 分数等变量。

拥有如此多特征的难题是多方面的。首先,数据量庞大,这将导致高存储成本和计算时间。其次,拥有庞大的特征空间时,模型准确性依赖于大量的数据。也就是说,信号与噪声之间的区别变得更加困难。因此,在处理像这样的高维数据时,我们通常会采用降维技术,例如 PCA。关于该主题的更多信息,请参考 en.wikipedia.org/wiki/Principal_component_analysis

PCA 使我们能够将原始特征转换为较少的新的特征,这些特征是由原始特征组成的,并且具有最大的解释能力。此外,由于新特征是旧特征的线性组合,这使得我们能够对数据进行匿名化处理,这在处理例如金融信息时非常方便。

准备工作

这个实例的准备工作包括安装scikit-learnpandas包,可以使用pip安装。命令如下:

pip install sklearn pandas

此外,我们将使用与前一个实例相同的数据集,malware_pe_headers.csv

如何操作...

在本节中,我们将演示如何在数据上使用 PCA 的一个实例:

  1. 首先导入必要的库并读取数据集:
from sklearn.decomposition import PCA
import pandas as pd

data = pd.read_csv("file_pe_headers.csv", sep=",")
X = data.drop(["Name", "Malware"], axis=1).to_numpy()
  1. 在应用 PCA 之前,先标准化数据集:
from sklearn.preprocessing import StandardScaler

X_standardized = StandardScaler().fit_transform(X)
  1. 实例化一个PCA实例,并使用它来降低我们数据的维度:
pca = PCA()
pca.fit_transform(X_standardized)
  1. 评估降维的效果:
print(pca.explained_variance_ratio_)

以下截图显示了输出结果:

它是如何工作的...

我们首先读取数据集并进行标准化,方法参照标准化数据的步骤(步骤 1 和 2)。 (在应用 PCA 之前,必须使用标准化数据)。接下来,我们实例化一个新的 PCA 转换器,并使用它来进行转换学习(fit)并将转换应用于数据集,使用fit_transform(步骤 3)。在步骤 4 中,我们分析我们的转换。特别需要注意的是,pca.explained_variance_ratio_的元素表示在每个方向上所占的方差比例。总和为 1,表示如果我们考虑数据所在的整个空间,所有的方差都已经被解释。然而,通过仅选择前几个方向,我们就能解释大部分的方差,同时减少维度。在我们的例子中,前 40 个方向就解释了 90%的方差:

sum(pca.explained_variance_ratio_[0:40])

这将产生以下输出:

0.9068522354673663

这意味着我们可以将特征的数量从 78 减少到 40,同时保留 90%的方差。这意味着 PE 头部的许多特征是高度相关的,这是可以理解的,因为这些特征并非设计为独立的。

使用马尔可夫链生成文本

马尔可夫链是简单的随机模型,其中一个系统可以处于多个状态之一。要知道系统下一个状态的概率分布,只需知道系统当前的状态即可。这与一种系统不同,在这种系统中,后续状态的概率分布可能依赖于系统的过去历史。这个简化假设使得马尔可夫链能够轻松应用于许多领域,并且效果出奇的好。

在这个实例中,我们将使用马尔可夫链生成虚假评论,这对渗透测试评论系统的垃圾信息检测器非常有用。在后续的实例中,您将把技术从马尔可夫链升级到 RNN。

准备工作

本食谱的准备工作包括在pip中安装markovifypandas包。命令如下:

pip install markovify pandas

此外,本章的代码库中包含一个 CSV 数据集,airport_reviews.csv,它应与本章的代码放在一起。

如何操作…

让我们通过执行以下步骤来看看如何使用马尔可夫链生成文本:

  1. 从导入markovify库和我们希望模仿风格的文本文件开始:
import markovify
import pandas as pd

df = pd.read_csv("airport_reviews.csv")

作为示例,我选择了一组机场评论作为我的文本:

"The airport is certainly tiny! ..."
  1. 接下来,将单独的评论合并成一个大的文本字符串,并使用机场评论文本构建一个马尔可夫链模型:
from itertools import chain

N = 100
review_subset = df["content"][0:N]
text = "".join(chain.from_iterable(review_subset))
markov_chain_model = markovify.Text(text)

在幕后,库会根据文本计算过渡词的概率。

  1. 使用马尔可夫链模型生成五个句子:
for i in range(5):
    print(markov_chain_model.make_sentence())
  1. 由于我们使用的是机场评论,执行前面的代码后,我们将得到以下输出:
On the positive side it's a clean airport transfer from A to C gates and outgoing gates is truly enormous - but why when we arrived at about 7.30 am for our connecting flight to Venice on TAROM.
The only really bother: you may have to wait in a polite manner.
Why not have bus after a short wait to check-in there were a lots of shops and less seating.
Very inefficient and hostile airport. This is one of the time easy to access at low price from city center by train.
The distance between the incoming gates and ending with dirty and always blocked by never ending roadworks.

令人惊讶的逼真!尽管这些评论需要筛选出最好的。

  1. 生成3个句子,每个句子的长度不超过140个字符:
for i in range(3):
    print(markov_chain_model.make_short_sentence(140))

使用我们的示例,我们将看到以下输出:

However airport staff member told us that we were put on a connecting code share flight.
Confusing in the check-in agent was friendly.
I am definitely not keen on coming to the lack of staff . Lack of staff . Lack of staff at boarding pass at check-in.

它是如何工作的…

我们从导入 Markovify 库开始,这个库用于马尔可夫链计算,并读取文本,这将为我们的马尔可夫模型提供信息(步骤 1)。在步骤 2 中,我们使用文本创建马尔可夫链模型。以下是文本对象初始化代码中的相关片段:

class Text(object):

    reject_pat = re.compile(r"(^')|('$)|\s'|'\s|[\"(\(\)\[\])]")

    def __init__(self, input_text, state_size=2, chain=None, parsed_sentences=None, retain_original=True, well_formed=True, reject_reg=''):
        """
        input_text: A string.
        state_size: An integer, indicating the number of words in the model's state.
        chain: A trained markovify.Chain instance for this text, if pre-processed.
        parsed_sentences: A list of lists, where each outer list is a "run"
              of the process (e.g. a single sentence), and each inner list
              contains the steps (e.g. words) in the run. If you want to simulate
              an infinite process, you can come very close by passing just one, very
              long run.
        retain_original: Indicates whether to keep the original corpus.
        well_formed: Indicates whether sentences should be well-formed, preventing
              unmatched quotes, parenthesis by default, or a custom regular expression
              can be provided.
        reject_reg: If well_formed is True, this can be provided to override the
              standard rejection pattern.
        """

最重要的参数是state_size = 2,这意味着马尔可夫链将计算连续单词对之间的转换。为了生成更逼真的句子,可以增加该参数,但代价是句子看起来不那么原始。接下来,我们应用训练好的马尔可夫链生成一些示例句子(步骤 3 和 4)。我们可以清楚地看到,马尔可夫链捕捉到了文本的语气和风格。最后,在步骤 5 中,我们使用我们的马尔可夫链生成一些模仿机场评论风格的推文

使用 scikit-learn 进行聚类

聚类是一类无监督机器学习算法,其中数据的部分被基于相似性进行分组。例如,聚类可能由在 n 维欧几里得空间中紧密相邻的数据组成。聚类在网络安全中很有用,可以用来区分正常和异常的网络活动,并帮助将恶意软件分类为不同的家族。

准备工作

本食谱的准备工作包括在pip中安装scikit-learnpandasplotly包。命令如下:

pip install sklearn plotly pandas

此外,仓库中为本食谱提供了一个名为file_pe_header.csv的数据集。

如何操作…

在接下来的步骤中,我们将看到 scikit-learn 的 K-means 聚类算法在玩具 PE 恶意软件分类上的演示:

  1. 首先导入并绘制数据集:
import pandas as pd
import plotly.express as px

df = pd.read_csv("file_pe_headers.csv", sep=",")
fig = px.scatter_3d(
    df,
    x="SuspiciousImportFunctions",
    y="SectionsLength",
    z="SuspiciousNameSection",
    color="Malware",
)
fig.show()

以下截图显示了输出:

  1. 提取特征和目标标签:
y = df["Malware"]
X = df.drop(["Name", "Malware"], axis=1).to_numpy()
  1. 接下来,导入 scikit-learn 的聚类模块,并将 K-means 模型(包含两个聚类)拟合到数据:
from sklearn.cluster import KMeans

estimator = KMeans(n_clusters=len(set(y)))
estimator.fit(X)
  1. 使用我们训练好的算法预测聚类:
y_pred = estimator.predict(X)
df["pred"] = y_pred
df["pred"] = df["pred"].astype("category")
  1. 为了查看算法的表现,绘制算法的聚类结果:
fig = px.scatter_3d(
    df,
    x="SuspiciousImportFunctions",
    y="SectionsLength",
    z="SuspiciousNameSection",
    color="pred",
)
fig.show()

以下截图显示了输出:

结果虽然不完美,但我们可以看到聚类算法捕捉到了数据集中的大部分结构。

它是如何工作的...

我们首先从一组样本中导入 PE 头部信息的数据集(步骤 1)。该数据集包含两类 PE 文件:恶意软件和良性文件。然后,我们使用 plotly 创建一个漂亮的交互式 3D 图(步骤 1)。接着,我们准备好将数据集用于机器学习。具体来说,在步骤 2 中,我们将X设为特征,将 y 设为数据集的类别。由于数据集有两个类别,我们的目标是将数据分成两个组,以便与样本分类相匹配。我们使用 K-means 算法(步骤 3),有关此算法的更多信息,请参阅:en.wikipedia.org/wiki/K-means_clustering。在经过充分训练的聚类算法下,我们准备好对测试集进行预测。我们应用聚类算法来预测每个样本应该属于哪个聚类(步骤 4)。在步骤 5 中观察结果时,我们发现聚类捕捉到了大量的潜在信息,因为它能够很好地拟合数据。

训练 XGBoost 分类器

梯度提升被广泛认为是解决一般机器学习问题时最可靠、最准确的算法。我们将在未来的食谱中利用 XGBoost 来创建恶意软件检测器。

准备开始

本食谱的准备工作包括在pip中安装 scikit-learn、pandasxgboost包。安装命令如下:

pip install sklearn xgboost pandas

此外,仓库中提供了名为file_pe_header.csv的数据集,供本食谱使用。

如何实现...

在接下来的步骤中,我们将演示如何实例化、训练和测试 XGBoost 分类器:

  1. 开始读取数据:
import pandas as pd

df = pd.read_csv("file_pe_headers.csv", sep=",")
y = df["Malware"]
X = df.drop(["Name", "Malware"], axis=1).to_numpy()
  1. 接下来,进行训练-测试数据集划分:
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
  1. 创建一个 XGBoost 模型实例,并在训练集上训练它:
from xgboost import XGBClassifier

XGB_model_instance = XGBClassifier()
XGB_model_instance.fit(X_train, y_train)
  1. 最后,评估它在测试集上的表现:
from sklearn.metrics import accuracy_score

y_test_pred = XGB_model_instance.predict(X_test)
accuracy = accuracy_score(y_test, y_test_pred)
print("Accuracy: %.2f%%" % (accuracy * 100))

以下截图显示了输出:

它是如何工作的...

我们首先读取数据(步骤 1)。然后,我们创建一个训练-测试分割(步骤 2)。接着,我们实例化一个带有默认参数的 XGBoost 分类器,并将其拟合到训练集(步骤 3)。最后,在步骤 4 中,我们使用 XGBoost 分类器对测试集进行预测。然后,我们计算 XGBoost 模型预测的准确性。

使用 statsmodels 分析时间序列

时间序列是指在连续的时间点上获取的数值序列。例如,股市每分钟的价格构成了一个时间序列。在网络安全领域,时间序列分析对于预测网络攻击非常有用,例如内部员工窃取数据,或一群黑客在为下一次攻击做准备时的行为模式。

让我们看看使用时间序列进行预测的几种技术。

准备就绪

本示例的准备工作包括在 pip 中安装 matplotlibstatsmodelsscipy 包。安装命令如下:

pip install matplotlib statsmodels scipy

如何操作...

在接下来的步骤中,我们展示了几种使用时间序列数据进行预测的方法:

  1. 首先生成一个时间序列:
from random import random

time_series = [2 * x + random() for x in range(1, 100)]
  1. 绘制你的数据:
%matplotlib inline
import matplotlib.pyplot as plt

plt.plot(time_series)
plt.show()

以下截图展示了输出结果:

  1. 我们可以使用多种技术来预测时间序列的后续值:

    • 自回归AR):
from statsmodels.tsa.ar_model import AR

model = AR(time_series)
model_fit = model.fit()
y = model_fit.predict(len(time_series), len(time_series))
    • 移动平均MA):
from statsmodels.tsa.arima_model import ARMA

model = ARMA(time_series, order=(0, 1))
model_fit = model.fit(disp=False)
y = model_fit.predict(len(time_series), len(time_series))
    • 简单指数平滑SES):
from statsmodels.tsa.holtwinters import SimpleExpSmoothing

model = SimpleExpSmoothing(time_series)
model_fit = model.fit()
y = model_fit.predict(len(time_series), len(time_series))

结果预测如下:

它是如何工作的...

在第一步中,我们生成了一个简单的时间序列。该序列由一条线上的值组成,并添加了一些噪声。接下来,在第 2 步中我们绘制了时间序列。你可以看到它非常接近一条直线,而且对时间点 处的时间序列值做出的合理预测是 。为了创建时间序列值的预测,我们考虑了三种不同的方案(第 3 步)来预测时间序列的未来值。在自回归模型中,基本思想是时间序列在时间 t 的值是该时间序列在之前时刻值的线性函数。更准确地说,有一些常数 ,以及一个数字 ,使得:

作为一个假设的例子, 可能是 3,意味着可以通过知道时间序列的最后 3 个值来轻松计算其值。

在移动平均模型中,时间序列被建模为围绕均值波动。更准确地说,设 是一组独立同分布的正态变量,且设 是常数。那么,时间序列可以通过以下公式建模:

因此,它在预测我们生成的嘈杂线性时间序列时表现较差。

最后,在简单指数平滑中,我们提出一个平滑参数,。然后,我们模型的估计值是根据以下公式计算得出的:

换句话说,我们跟踪一个估计值,,并使用当前的时间序列值略微调整它。调整的强度由参数控制。

使用隔离森林进行异常检测

异常检测是识别数据集中不符合预期模式的事件。在应用中,这些事件可能至关重要。例如,它们可能是网络入侵或欺诈的发生。我们将利用隔离森林来检测此类异常。隔离森林依赖于一个观察结果:隔离异常值很容易,而描述一个正常数据点则更为困难。

准备工作

该配方的准备工作包括在pip中安装matplotlibpandasscipy包。命令如下:

pip install matplotlib pandas scipy

如何做到这一点...

在接下来的步骤中,我们演示如何应用隔离森林算法来检测异常:

  1. 导入所需的库并设置随机种子:
import numpy as np
import pandas as pd

random_seed = np.random.RandomState(12)
  1. 生成一组正常观测数据,用作训练数据:
X_train = 0.5 * random_seed.randn(500, 2)
X_train = np.r_[X_train + 3, X_train]
X_train = pd.DataFrame(X_train, columns=["x", "y"])
  1. 生成一个测试集,仍然由正常观测数据组成:
X_test = 0.5 * random_seed.randn(500, 2)
X_test = np.r_[X_test + 3, X_test]
X_test = pd.DataFrame(X_test, columns=["x", "y"])
  1. 生成一组异常观测数据。这些数据来自与正常观测数据不同的分布:
X_outliers = random_seed.uniform(low=-5, high=5, size=(50, 2))
X_outliers = pd.DataFrame(X_outliers, columns=["x", "y"])
  1. 让我们看看我们生成的数据:
%matplotlib inline
import matplotlib.pyplot as plt

p1 = plt.scatter(X_train.x, X_train.y, c="white", s=50, edgecolor="black")
p2 = plt.scatter(X_test.x, X_test.y, c="green", s=50, edgecolor="black")
p3 = plt.scatter(X_outliers.x, X_outliers.y, c="blue", s=50, edgecolor="black")
plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.legend(
    [p1, p2, p3],
    ["training set", "normal testing set", "anomalous testing set"],
    loc="lower right",
)

plt.show()

以下截图显示了输出结果:

  1. 现在在我们的训练数据上训练一个隔离森林模型:
from sklearn.ensemble import IsolationForest

clf = IsolationForest()
clf.fit(X_train)
y_pred_train = clf.predict(X_train)
y_pred_test = clf.predict(X_test)
y_pred_outliers = clf.predict(X_outliers)
  1. 让我们看看算法的表现。将标签附加到X_outliers
X_outliers = X_outliers.assign(pred=y_pred_outliers)
X_outliers.head()

以下是输出结果:

xypred
03.9475042.8910031
10.413976-2.025841-1
2-2.644476-3.480783-1
3-0.518212-3.386443-1
42.9776692.2153551
  1. 让我们绘制隔离森林预测的异常值,看看它捕获了多少:
p1 = plt.scatter(X_train.x, X_train.y, c="white", s=50, edgecolor="black")
p2 = plt.scatter(
    X_outliers.loc[X_outliers.pred == -1, ["x"]],
    X_outliers.loc[X_outliers.pred == -1, ["y"]],
    c="blue",
    s=50,
    edgecolor="black",
)
p3 = plt.scatter(
    X_outliers.loc[X_outliers.pred == 1, ["x"]],
    X_outliers.loc[X_outliers.pred == 1, ["y"]],
    c="red",
    s=50,
    edgecolor="black",
)

plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.legend(
    [p1, p2, p3],
    ["training observations", "detected outliers", "incorrectly labeled outliers"],
    loc="lower right",
)

plt.show()

以下截图显示了输出结果:

  1. 现在让我们看看它在正常测试数据上的表现。将预测标签附加到X_test
X_test = X_test.assign(pred=y_pred_test)
X_test.head()

以下是输出结果:

xypred
03.9445753.866919-1
12.9848533.1421501
23.5017352.1682621
32.9063003.2338261
43.2732253.2617901
  1. 现在让我们绘制结果,看看我们的分类器是否正确地标记了正常的测试数据:
p1 = plt.scatter(X_train.x, X_train.y, c="white", s=50, edgecolor="black")
p2 = plt.scatter(
    X_test.loc[X_test.pred == 1, ["x"]],
    X_test.loc[X_test.pred == 1, ["y"]],
    c="blue",
    s=50,
    edgecolor="black",
)
p3 = plt.scatter(
    X_test.loc[X_test.pred == -1, ["x"]],
    X_test.loc[X_test.pred == -1, ["y"]],
    c="red",
    s=50,
    edgecolor="black",
)

plt.xlim((-6, 6))
plt.ylim((-6, 6))
plt.legend(
    [p1, p2, p3],
    [
        "training observations",
        "correctly labeled test observations",
        "incorrectly labeled test observations",
    ],
    loc="lower right",
)

plt.show()

以下截图显示了输出结果:

显然,我们的 Isolation Forest 模型在捕捉异常点方面表现得相当不错。尽管存在一些假阴性(正常点被错误分类为异常点),但通过调整模型的参数,我们或许能减少这些问题。

它是如何工作的……

第一步简单地加载必要的库,这些库将使我们能够快速、轻松地操作数据。在步骤 2 和 3 中,我们生成一个由正常观察值组成的训练集和测试集。这些数据具有相同的分布。而在步骤 4 中,我们通过创建异常值来生成其余的测试集。这个异常数据集的分布与训练数据和其余的测试数据不同。绘制我们的数据时,我们看到一些异常点与正常点看起来无法区分(步骤 5)。这保证了由于数据的性质,我们的分类器将有相当大比例的误分类,在评估其性能时,我们必须记住这一点。在步骤 6 中,我们使用默认参数拟合一个 Isolation Forest 实例到训练数据。

请注意,算法并未接收到任何关于异常数据的信息。我们使用训练好的 Isolation Forest 实例来预测测试数据是正常的还是异常的,类似地,也预测异常数据是正常的还是异常的。为了检查算法的表现,我们将预测标签附加到 X_outliers(步骤 7),然后绘制 Isolation Forest 实例在异常值上的预测(步骤 8)。我们看到它能够捕捉到大部分的异常值。那些被错误标记的异常值与正常观察值无法区分。接下来,在步骤 9,我们将预测标签附加到 X_test,为分析做准备,然后绘制 Isolation Forest 实例在正常测试数据上的预测(步骤 10)。我们看到它正确地标记了大多数正常观察值。与此同时,也有相当数量的正常观察值被错误分类(用红色显示)。

根据我们愿意容忍多少误报,我们可能需要对分类器进行微调,以减少假阳性的数量。

使用哈希向量化器和 tf-idf 以及 scikit-learn 进行自然语言处理

我们在数据科学中常常发现,我们希望分析的对象是文本。例如,它们可能是推文、文章或网络日志。由于我们的算法需要数值输入,我们必须找到一种方法将这些文本转化为数值特征。为此,我们使用了一系列技术。

一个词元是文本的一个单位。例如,我们可以指定我们的词元是单词、句子或字符。计数向量化器接受文本输入,然后输出一个包含文本词元计数的向量。哈希向量化器是计数向量化器的一种变体,旨在以更快和更可扩展的方式实现,但牺牲了可解释性和哈希冲突。尽管它很有用,仅仅获取文档语料库中出现的单词计数可能会误导。原因是,通常,像thea这样的不重要词(称为停用词)频繁出现,因此信息含量较低。正因如此,我们通常会为词语赋予不同的权重以抵消这个问题。主要的技术是tf-idf,即词频-逆文档频率。其主要思想是我们考虑某个词出现的次数,但根据它在多少个文档中出现过来进行折扣。

在网络安全领域,文本数据无处不在;事件日志、对话记录以及函数名列表只是其中的一些例子。因此,能够处理此类数据至关重要,这是你在本食谱中将要学习的内容。

准备工作

该食谱的准备工作包括在pip中安装 scikit-learn 包。安装命令如下:

pip install sklearn

此外,包含#Anonops IRC 频道中对话摘录的日志文件anonops_short.log也包含在本章的代码库中。

如何进行…

在接下来的步骤中,我们将把一组文本数据转换为数值形式,以便于机器学习算法使用:

  1. 首先,导入一个文本数据集:
with open("anonops_short.txt", encoding="utf8") as f:
    anonops_chat_logs = f.readlines()
  1. 接下来,使用哈希向量化器计算文本中的单词数量,然后使用 tf-idf 进行加权:
from sklearn.feature_extraction.text import HashingVectorizer
from sklearn.feature_extraction.text import TfidfTransformer

my_vector = HashingVectorizer(input="content", ngram_range=(1, 2))
X_train_counts = my_vector.fit_transform(anonops_chat_logs,)
tf_transformer = TfidfTransformer(use_idf=True,).fit(X_train_counts)
X_train_tf = tf_transformer.transform(X_train_counts)
  1. 最终结果是一个稀疏矩阵,每一行是一个表示文本之一的向量:
X_train_tf

<180830 x 1048576 sparse matrix of type <class 'numpy.float64'>' with 3158166 stored elements in Compressed Sparse Row format> print(X_train_tf)

以下是输出结果:

它是如何工作的...

我们从加载#Anonops 文本数据集开始(第 1 步)。Anonops IRC 频道与匿名黑客活动组织有关。特别地,聊天参与者曾在过去通过 Anonops 计划和宣布他们未来的目标。因此,一个经过精心设计的机器学习系统,通过对这类数据进行训练,可以预测网络攻击。在第 2 步中,我们实例化了一个哈希向量化器。哈希向量化器为我们提供了文本中 1-gram 和 2-gram 的计数,换句话说,就是单个单词和相邻的两个单词(词元)。然后,我们应用了一个 tf-idf 转换器,为哈希向量化器提供的计数赋予适当的权重。我们的最终结果是一个大型稀疏矩阵,表示文本中 1-gram 和 2-gram 的出现次数,并根据重要性加权。最后,我们检查了在 Scipy 中展示的稀疏矩阵前端。

使用 scikit-optimize 进行超参数调优

在机器学习中,超参数是指在训练过程开始之前就已设定的参数。例如,梯度提升模型的学习率选择和多层感知机的隐藏层大小都是超参数的例子。与之对比,其他参数的值是通过训练过程中学习得出的。超参数选择非常重要,因为它可能对模型的表现产生巨大影响。

最基本的超参数调优方法叫做网格搜索。在这种方法中,你为每个超参数指定一组潜在的值,然后尝试所有的组合,直到找到最佳的组合。这个暴力法虽然全面,但计算量大。也有更为复杂的方法。在这个食谱中,你将学习如何使用scikit-optimize进行超参数的贝叶斯优化。与基本的网格搜索不同,在贝叶斯优化中,并不是尝试所有参数值,而是从指定的分布中抽取一个固定数量的参数设置。更多细节请参见scikit-optimize.github.io/notebooks/bayesian-optimization.html

准备开始

这个食谱的准备工作包括安装特定版本的scikit-learn,安装xgboost,以及通过pip安装scikit-optimize。相关命令如下:

pip install scikit-learn==0.20.3 xgboost scikit-optimize pandas

如何实现...

在接下来的步骤中,你将加载标准的wine数据集,并使用贝叶斯优化来调优 XGBoost 模型的超参数:

  1. 从 scikit-learn 加载wine数据集:
from sklearn import datasets

wine_dataset = datasets.load_wine()
X = wine_dataset.data
y = wine_dataset.target
  1. 导入 XGBoost 和分层 K 折交叉验证:
import xgboost as xgb
from sklearn.model_selection import StratifiedKFold
  1. scikit-optimize导入BayesSearchCV并指定要测试的参数设置数量:
from skopt import BayesSearchCV

n_iterations = 50
  1. 指定你的估计器。在这种情况下,我们选择 XGBoost,并将其设置为能够执行多类别分类:
estimator = xgb.XGBClassifier(
    n_jobs=-1,
    objective="multi:softmax",
    eval_metric="merror",
    verbosity=0,
    num_class=len(set(y)),
)
  1. 指定参数搜索空间:
search_space = {
    "learning_rate": (0.01, 1.0, "log-uniform"),
    "min_child_weight": (0, 10),
    "max_depth": (1, 50),
    "max_delta_step": (0, 10),
    "subsample": (0.01, 1.0, "uniform"),
    "colsample_bytree": (0.01, 1.0, "log-uniform"),
    "colsample_bylevel": (0.01, 1.0, "log-uniform"),
    "reg_lambda": (1e-9, 1000, "log-uniform"),
    "reg_alpha": (1e-9, 1.0, "log-uniform"),
    "gamma": (1e-9, 0.5, "log-uniform"),
    "min_child_weight": (0, 5),
    "n_estimators": (5, 5000),
    "scale_pos_weight": (1e-6, 500, "log-uniform"),
}
  1. 指定要执行的交叉验证类型:
cv = StratifiedKFold(n_splits=3, shuffle=True)
  1. 使用你定义的设置来定义BayesSearchCV
bayes_cv_tuner = BayesSearchCV(
    estimator=estimator,
    search_spaces=search_space,
    scoring="accuracy",
    cv=cv,
    n_jobs=-1,
    n_iter=n_iterations,
    verbose=0,
    refit=True,
)
  1. 定义一个callback函数来输出参数搜索的进度:
import pandas as pd
import numpy as np

def print_status(optimal_result):
    """Shows the best parameters found and accuracy attained of the search so far."""
    models_tested = pd.DataFrame(bayes_cv_tuner.cv_results_)
    best_parameters_so_far = pd.Series(bayes_cv_tuner.best_params_)
    print(
        "Model #{}\nBest accuracy so far: {}\nBest parameters so far: {}\n".format(
            len(models_tested),
            np.round(bayes_cv_tuner.best_score_, 3),
            bayes_cv_tuner.best_params_,
        )
    )

    clf_type = bayes_cv_tuner.estimator.__class__.__name__
    models_tested.to_csv(clf_type + "_cv_results_summary.csv")
  1. 执行参数搜索:
result = bayes_cv_tuner.fit(X, y, callback=print_status)

如你所见,以下显示了输出结果:

Model #1
 Best accuracy so far: 0.972
 Best parameters so far: {'colsample_bylevel': 0.019767840658391753, 'colsample_bytree': 0.5812505808116454, 'gamma': 1.7784704701058755e-05, 'learning_rate': 0.9050859661329937, 'max_delta_step': 3, 'max_depth': 42, 'min_child_weight': 1, 'n_estimators': 2334, 'reg_alpha': 0.02886003776717955, 'reg_lambda': 0.0008507166793122457, 'scale_pos_weight': 4.801764874750116e-05, 'subsample': 0.7188797743009225}

 Model #2
 Best accuracy so far: 0.972
 Best parameters so far: {'colsample_bylevel': 0.019767840658391753, 'colsample_bytree': 0.5812505808116454, 'gamma': 1.7784704701058755e-05, 'learning_rate': 0.9050859661329937, 'max_delta_step': 3, 'max_depth': 42, 'min_child_weight': 1, 'n_estimators': 2334, 'reg_alpha': 0.02886003776717955, 'reg_lambda': 0.0008507166793122457, 'scale_pos_weight': 4.801764874750116e-05, 'subsample': 0.7188797743009225}

<snip>

Model #50
 Best accuracy so far: 0.989
 Best parameters so far: {'colsample_bylevel': 0.013417868502558758, 'colsample_bytree': 0.463490250419848, 'gamma': 2.2823050161337873e-06, 'learning_rate': 0.34006478878384533, 'max_delta_step': 9, 'max_depth': 41, 'min_child_weight': 0, 'n_estimators': 1951, 'reg_alpha': 1.8321791726476395e-08, 'reg_lambda': 13.098734837402576, 'scale_pos_weight': 0.6188077759379964, 'subsample': 0.7970035272497132}

如何实现...

在步骤 1 和 2 中,我们导入了一个标准数据集——wine数据集,以及分类所需的库。接下来的步骤更为有趣,我们指定了想要进行超参数搜索的时间,具体来说,就是指定我们希望尝试多少种参数组合。搜索时间越长,结果通常越好,但也有过拟合和延长计算时间的风险。在步骤 4 中,我们选择 XGBoost 作为模型,然后指定类别数量、问题类型和评估指标。这个部分将取决于问题的类型。例如,对于回归问题,我们可能会设置eval_metric = 'rmse',并且一同去掉num_class

除了 XGBoost 之外,超参数优化器还可以选择其他模型。在下一步(第 5 步)中,我们指定了每个参数的概率分布,这些参数将会被探索。这也是使用BayesSearchCV优于简单网格搜索的一个优势,因为它允许你更智能地探索参数空间。接下来,我们指定交叉验证方案(第 6 步)。由于我们正在进行分类问题,因此指定分层折叠是合理的。然而,对于回归问题,StratifiedKFold应该被替换为KFold

还要注意,为了测量结果的准确性,较大的划分数更为理想,尽管这会带来计算上的开销。在第 7 步中,你可以看到一些可以更改的额外设置。特别地,n_jobs允许你并行化任务。输出的详细程度以及评分方法也可以进行调整。为了监控搜索过程和我们超参数调优的性能,我们在第 8 步定义了一个回调函数,用于打印出进度。网格搜索的结果也会保存在 CSV 文件中。最后,我们运行超参数搜索(第 9 步)。输出结果让我们能够观察每次超参数搜索迭代的参数和性能。

在本书中,我们将避免调整分类器的超参数。原因部分是为了简洁,部分原因是因为在这里进行超参数调优会是过早优化,因为从最终用户的角度来看,算法的性能并没有特定的要求或目标。既然我们已经展示了如何进行调优,你可以轻松地将这个方法应用于当前的任务。

另一个需要记住的用于超参数调优的著名库是hyperopt

第二章:基于机器学习的恶意软件检测

在本章中,我们将开始认真地将数据科学应用于网络安全。我们将从学习如何对样本进行静态和动态分析开始。在此基础上,我们将学习如何对样本进行特征化,以便构建一个具有信息量的特征的数据集。本章的亮点是学习如何使用我们学到的特征化技能构建静态恶意软件检测器。最后,您将学习如何解决网络安全领域中常见的机器学习挑战,如类别不平衡和假阳性率FPR)限制。

本章涵盖以下内容:

  • 恶意软件静态分析

  • 恶意软件动态分析

  • 使用机器学习检测文件类型

  • 测量两个字符串之间的相似度

  • 测量两个文件之间的相似度

  • 提取 N-gram

  • 选择最佳 N-gram

  • 构建静态恶意软件检测器

  • 解决类别不平衡问题

  • 处理类型 I 和类型 II 错误

技术要求

在本章中,我们将使用以下工具:

  • YARA

  • pefile

  • PyGitHub

  • Cuckoo 沙箱

  • 自然语言工具包NLTK

  • imbalanced-learn

代码和数据集可以在 github.com/PacktPublishing/Machine-Learning-for-Cybersecurity-Cookbook/tree/master/Chapter02 找到。

恶意软件静态分析

在静态分析中,我们在不执行样本的情况下进行检查。通过这种方式可以获得大量信息,从文件名称到更复杂的信息,如专用的 YARA 签名。我们将介绍通过静态分析样本可以获取的各种特征。尽管静态分析强大且方便,但它并不是万能的,主要是因为软件可能被混淆。因此,我们将在后续章节中使用动态分析和其他技术。

计算样本的哈希值

不深入探讨哈希的复杂性,哈希本质上是一个简短且唯一的字符串签名。例如,我们可以对文件的字节序列进行哈希处理,从而得到该文件的唯一代码。这使我们能够快速比较两个文件,查看它们是否相同。

市面上有许多哈希算法,因此我们将重点介绍最重要的几种,即 SHA256 和 MD5。需要注意的是,MD5 因哈希碰撞(即两个不同的对象具有相同的哈希值)而存在已知的漏洞,因此使用时需要小心。在本教程中,我们将使用一个可执行文件,并计算它的 MD5 和 SHA256 哈希值。

准备工作

本教程的准备工作包括下载一个测试文件,即来自 www.python.org/ftp/python/3.7.2/python-3.7.2-amd64.exe 的 Python 可执行文件。

如何操作...

在以下步骤中,我们将展示如何获取文件的哈希值:

  1. 首先,导入库并选择您希望计算哈希的文件:
import sys
import hashlib

filename = "python-3.7.2-amd64.exe"
  1. 实例化 MD5 和 SHA256 对象,并指定我们将读取的块的大小:
BUF_SIZE = 65536
md5 = hashlib.md5()
sha256 = hashlib.sha256()
  1. 然后,我们将文件以 64 KB 为块进行读取,并增量构建哈希值:
with open(filename, "rb") as f:
    while True:
        data = f.read(BUF_SIZE)
        if not data:
            break
        md5.update(data)
        sha256.update(data)
  1. 最后,输出计算结果的哈希值:
print("MD5: {0}".format(md5.hexdigest()))
print("SHA256: {0}".format(sha256.hexdigest()))

这将产生以下输出:

MD5: ff258093f0b3953c886192dec9f52763
SHA256: 0fe2a696f5a3e481fed795ef6896ed99157bcef273ef3c4a96f2905cbdb3aa13

如何工作…

本节将解释前面章节中提供的步骤:

  • 在步骤 1 中,我们导入了hashlib,一个用于哈希计算的标准 Python 库。我们还指定了我们将要计算哈希的文件——在这个例子中,文件是python-3.7.2-amd64.exe

  • 在步骤 2 中,我们实例化一个md5对象和一个sha256对象,并指定我们将读取的块的大小。

  • 在步骤 3 中,我们使用.update(data)方法。这个方法允许我们增量计算哈希,因为它计算的是连接字符串的哈希。换句话说,hash.update(a)后跟hash.update(b)等同于hash.update(a+b)

  • 在步骤 4 中,我们以十六进制数字的形式输出哈希值。

我们还可以验证我们的计算结果是否与其他来源提供的哈希计算一致,比如 VirusTotal 和官方 Python 网站。MD5 哈希值显示在 Python 网页上(www.python.org/downloads/release/python-372/):

可以通过将文件上传到 VirusTotal(www.virustotal.com/gui/home)来计算 SHA256 哈希值:

YARA

YARA 是一种计算机语言,允许安全专家方便地指定规则,然后用该规则对所有符合条件的样本进行分类。一个最小的规则包含一个名称和一个条件,例如,以下内容:

 rule my_rule_name { condition: false }

这个规则不会匹配任何文件。相反,下面的规则将匹配每个样本:

 Rule my_rule_name { condition: true }

一个更有用的例子是匹配任何大于 100 KB 的文件:

 Rule over_100kb { condition: filesize > 100KB }

另一个例子是检查某个特定文件是否为 PDF 文件。为此,我们需要检查文件的魔术数字是否与 PDF 文件的魔术数字匹配。魔术数字是文件开头的一段字节序列,表示文件类型。在 PDF 文件的情况下,该序列为25 50 44 46

 rule is_a_pdf {

 strings:
   $pdf_magic = {25 50 44 46}

 condition:
   $pdf_magic at 0
 }

现在,让我们看看如何将规则应用于文件。

准备工作

本教程的准备工作包括在设备上安装 YARA。安装说明可以在yara.readthedocs.io/en/stable/找到。对于 Windows,您需要下载 YARA 的可执行文件。

如何操作……

在以下步骤中,我们将向您展示如何创建 YARA 规则并对文件进行测试:

  1. 将您的规则(如下面所示)复制到文本文件中并命名为rules.yara
 rule is_a_pdf
 {
        strings:
               $pdf_magic = {25 50 44 46}
        condition:
               $pdf_magic at 0
 }

 rule dummy_rule1
 {
        condition:
               false
 }

 rule dummy_rule2
 {
        condition:
               true
 }
  1. 接下来,选择一个你希望用规则进行检查的文件。称之为target_file。在终端中执行Yara rules.yara target_file,如下所示:
Yara rule.yara PythonBrochure

结果应该如下所示:

is_a_pdf target_file
dummy_rule2 target_rule

工作原理...

如你所见,在步骤 1中,我们复制了几条 YARA 规则。第一条规则检查文件的魔术数字,看看它们是否与 PDF 文件的魔术数字匹配。其他两条规则是简单的规则——一条匹配所有文件,另一条不匹配任何文件。然后,在步骤 2中,我们使用 YARA 程序将这些规则应用到目标文件上。通过打印输出,我们看到文件匹配了一些规则,但没有匹配其他规则,这符合有效的 YARA 规则集的预期。

检查 PE 头

便携式可执行文件 (PE) 是一种常见的 Windows 文件类型。PE 文件包括.exe.dll.sys文件。所有 PE 文件都有一个 PE 头,这是代码的一个头部部分,指示 Windows 如何解析随后的代码。PE 头中的字段通常作为特征被用于恶意软件的检测。为了方便提取 PE 头的众多值,我们将使用pefile Python 模块。在本食谱中,我们将解析一个文件的 PE 头,然后打印出其中一些重要部分。

准备开始

本食谱的准备工作包括在pip中安装pefile包。在你的 Python 环境的终端中运行以下命令:

pip install pefile

此外,从www.python.org/ftp/python/3.7.2/python-3.7.2-amd64.exe下载测试文件 Python 可执行文件。

如何操作...

在接下来的步骤中,我们将解析一个文件的 PE 头,并打印出其中一些重要部分:

  1. 导入 PE 文件并使用它来解析你希望检查的文件的 PE 头:
import pefile

desired_file = "python-3.7.2-amd64.exe"
pe = pefile.PE(desired_file)
  1. 列出 PE 文件的导入项:
for entry in pe.DIRECTORY_ENTRY_IMPORT:
    print(entry.dll)
    for imp in entry.imports:
        print("\t", hex(imp.address), imp.name)

这里显示了一小部分输出:

  1. 列出 PE 文件的各个部分:
for section in pe.sections:
    print(
        section.Name,
        hex(section.VirtualAddress),
        hex(section.Misc_VirtualSize),
        section.SizeOfRawData,
    )

之前代码的输出结果如下:

  1. 打印解析信息的完整转储:
print(pe.dump_info())

这里显示了一小部分输出:

工作原理...

我们从步骤 1开始,导入了pefile库,并指定了我们将要分析的文件。在这个例子中,文件是python-3.7.2-amd64.exe,但同样也可以轻松分析任何其他 PE 文件。接着,我们继续检查文件所导入的 DLL,以了解文件可能使用的方法,在步骤 2中进行分析。DLL 可以回答这个问题,因为 DLL 是代码库,其他应用程序可能会调用它。例如,USER32.dll是一个包含 Windows USER 的库,它是 Microsoft Windows 操作系统的一部分,提供用于构建用户界面的核心功能。该组件允许其他应用程序利用窗口管理、消息传递、输入处理和标准控件等功能。因此,从逻辑上讲,如果我们看到一个文件导入了像GetCursorPos这样的函数,那么它很可能是在查看光标的位置。在步骤 3中,我们打印了 PE 文件的各个部分。它们为程序的不同部分提供了逻辑和物理的分隔,因此为分析人员提供了有关程序的宝贵信息。最后,我们打印了文件的所有解析后的 PE 头部信息,为后续用于特征工程做好准备(步骤 4)。

提取 PE 头部特征

在本节中,我们将从 PE 头部提取特征,用于构建恶意/良性样本分类器。我们将继续使用pefile Python 模块。

准备开始

本配方的准备工作包括在pip中安装pefile包。命令如下:

pip install pefile

此外,良性和恶意文件已经提供给你,位于根目录下的PE Samples Dataset文件夹中。将所有名为Benign PE Samples*.7z的压缩包解压到名为Benign PE Samples的文件夹中。将所有名为Malicious PE Samples*.7z的压缩包解压到名为Malicious PE Samples的文件夹中。

如何操作...

在接下来的步骤中,我们将收集 PE 头部的显著部分:

  1. 导入pefile和用于列举样本的模块:
import pefile
from os import listdir
from os.path import isfile, join

directories = ["Benign PE Samples", "Malicious PE Samples"]
  1. 我们定义了一个函数来收集文件的节名称,并对其进行预处理以提高可读性和标准化:
def get_section_names(pe):
    """Gets a list of section names from a PE file."""
    list_of_section_names = []
    for sec in pe.sections:
        normalized_name = sec.Name.decode().replace("\x00", "").lower()
        list_of_section_names.append(normalized_name)
    return list_of_section_names
  1. 我们定义了一个便捷函数来预处理和标准化我们的导入信息:
def preprocess_imports(list_of_DLLs):
    """Normalize the naming of the imports of a PE file."""
    return [x.decode().split(".")[0].lower() for x in list_of_DLLs]
  1. 然后,我们定义一个函数,使用pefile收集文件中的导入信息:
def get_imports(pe):
    """Get a list of the imports of a PE file."""
    list_of_imports = []
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        list_of_imports.append(entry.dll)
    return preprocess_imports(list_of_imports)
  1. 最后,我们准备迭代所有文件,并创建列表来存储我们的特征:
imports_corpus = []
num_sections = []
section_names = []
for dataset_path in directories:
    samples = [f for f in listdir(dataset_path) if isfile(join(dataset_path, f))]
    for file in samples:
        file_path = dataset_path + "/" + file
        try:
  1. 除了收集前述特征外,我们还会收集文件的节数:
            pe = pefile.PE(file_path)
            imports = get_imports(pe)
            n_sections = len(pe.sections)
            sec_names = get_section_names(pe)
            imports_corpus.append(imports)
            num_sections.append(n_sections)
            section_names.append(sec_names)
  1. 如果无法解析文件的 PE 头部,我们定义了一个 try-catch 语句:
        except Exception as e:
            print(e)
            print("Unable to obtain imports from " + file_path)

它是如何工作的...

如你所见,在步骤 1中,我们导入了pefile模块来列举样本。完成之后,我们定义了一个便捷函数,正如你在步骤 2中看到的那样。这样做的原因是它经常使用不同的大小写(大写/小写)导入。这会导致相同的导入看起来像是不同的导入。

在预处理导入项后,我们定义另一个函数,将文件的所有导入项收集到一个列表中。我们还将定义一个函数,用来收集文件的各个部分的名称,以便规范化这些名称,如.text.rsrc.reloc,同时包含文件的不同部分(步骤 3)。然后,文件将在我们的文件夹中进行枚举,空列表将被创建,用于存放我们将要提取的特征。预定义的函数将收集导入项(步骤 4)、部分名称以及每个文件的部分数量(步骤 56)。最后,将定义一个 try-catch 语句块,以防某个文件的 PE 头无法解析(步骤 7)。这种情况可能由于多种原因发生。一个原因是文件本身并不是 PE 文件。另一个原因是其 PE 头部故意或无意地被篡改。

恶意软件动态分析

与静态分析不同,动态分析是一种恶意软件分析技术,其中专家执行样本,然后在样本运行时研究其行为。动态分析相对于静态分析的主要优势是,它允许通过观察样本的行为来绕过混淆,而不是试图解读样本的内容和行为。由于恶意软件本质上是不安全的,研究人员会选择在虚拟机VM)中执行样本。这被称为沙箱化

准备工作

在虚拟机中自动化样本分析的最突出工具之一是 Cuckoo 沙箱。Cuckoo 沙箱的初始安装非常简单;只需运行以下命令:

pip install -U cuckoo

您必须确保还拥有可以由您的机器控制的虚拟机。配置沙箱可能具有挑战性,但可以通过cuckoo.sh/docs/的说明来完成。

现在我们展示如何利用 Cuckoo 沙箱来获取样本的动态分析。

如何做...

一旦您的 Cuckoo 沙箱设置完成,并且运行了 Web 界面,按照以下步骤收集样本的运行时信息:

  1. 打开您的 Web 界面(默认位置是127.0.0.1:8000),点击提交文件进行分析,并选择您希望分析的样本:

  1. 以下屏幕将自动出现。在其中,选择您希望对样本执行的分析类型:

  1. 点击分析以在沙箱中分析样本。结果应如下所示:

  1. 接下来,打开您分析的样本的报告:

  1. 选择行为分析标签:

显示的 API 调用顺序、注册表键更改和其他事件均可用作分类器的输入。

它是如何工作的...

从概念上讲,获取动态分析结果包括在允许分析员收集运行时信息的环境中运行样本。Cuckoo Sandbox 是一个灵活的框架,带有预构建模块来完成这一任务。我们从打开 Cuckoo Sandbox 的 Web 门户开始了我们的操作流程(步骤 1)。命令行界面CLI)也可用。我们继续提交一个样本并选择希望执行的分析类型(步骤 2步骤 3)。这些步骤也可以通过 Cuckoo CLI 来执行。接着,我们查看了分析报告(步骤 4)。此时你可以看到 Cuckoo Sandbox 的许多模块如何反映在最终的分析结果中。例如,如果安装并使用了一个捕获流量的模块,那么报告中会包含网络标签中捕获的数据。我们继续将视角集中在行为分析(步骤 5)上,特别是观察 API 调用的顺序。API 调用基本上是操作系统执行的操作。这个顺序构成了一个极好的特征集,我们将利用它在未来的操作中检测恶意软件。最后,值得注意的是,在生产环境中,可能有必要创建一个定制的沙箱,配备自定义的数据收集模块,并搭载反虚拟机检测软件,以促进成功的分析。

使用机器学习检测文件类型

黑客常用的技巧之一就是通过混淆文件类型来悄悄将恶意文件渗透到安全系统中。例如,一个(恶意的)PowerShell 脚本通常应该有.ps1的扩展名。系统管理员可以通过阻止所有带有.ps1扩展名的文件执行,来防止 PowerShell 脚本的执行。然而,狡猾的黑客可以删除或更改扩展名,使得文件的身份变得模糊。只有通过检查文件的内容,才能将其与普通文本文件区分开来。由于实际原因,人类不可能检查系统中的所有文本文件。因此,采用自动化方法就显得尤为重要。在本章中,我们将展示如何利用机器学习来检测未知文件的文件类型。我们的第一步是策划一个数据集。

从 GitHub 抓取特定类型的文件

为了策划数据集,我们将从 GitHub 上抓取我们感兴趣的特定文件类型。

准备工作

准备工作包括通过运行以下命令,在pip中安装PyGitHub包:

pip install PyGitHub

此外,你还需要 GitHub 账户凭证。

如何做到这一点...

在接下来的步骤中,我们将整理一个数据集,并利用它创建一个分类器来确定文件类型。为了演示,我们将展示如何通过抓取 GitHub 获取 PowerShell 脚本、Python 脚本和 JavaScript 文件的集合。通过这种方式获得的示例集合可以在随附的仓库中找到,文件名为PowerShellSamples.7zPythonSamples.7zJavascriptSamples.7z。首先,我们将编写用于抓取 JavaScript 文件的代码:

  1. 首先导入PyGitHub库,以便能够调用 GitHub API。我们还导入了base64模块,以便解码base64编码的文件:
import os
from github import Github
import base64
  1. 我们必须提供我们的凭证,然后指定一个查询——在这种情况下,查询 JavaScript——来选择我们的仓库:
username = "your_github_username"
password = "your_password"
target_dir = "/path/to/JavascriptSamples/"
g = Github(username, password)
repositories = g.search_repositories(query='language:javascript')
n = 5
i = 0
  1. 我们遍历符合条件的仓库:
for repo in repositories:
    repo_name = repo.name
    target_dir_of_repo = target_dir+"\\"+repo_name
    print(repo_name)
    try:
  1. 我们为每个匹配我们搜索标准的仓库创建一个目录,然后读取其内容:
        os.mkdir(target_dir_of_repo)
        i += 1
        contents = repo.get_contents("")
  1. 我们将所有仓库的目录添加到队列中,以便列出这些目录中包含的所有文件:
        while len(contents) > 1:
            file_content = contents.pop(0)
            if file_content.type == "dir":
                contents.extend(repo.get_contents(file_content.path))
            else:
  1. 如果我们发现一个非目录文件,我们检查它的扩展名是否为.js
                st = str(file_content)
                filename = st.split("\"")[1].split("\"")[0]
                extension = filename.split(".")[-1]
                if extension == "js":
  1. 如果扩展名是.js,我们将文件的副本写出:
                    file_contents = repo.get_contents(file_content.path)
                    file_data = base64.b64decode(file_contents.content)
                    filename = filename.split("/")[-1]
                    file_out = open(target_dir_of_repo+"/"+filename, "wb")
                    file_out.write(file_data)
      except:
        pass
    if i==n:
        break
  1. 一旦完成,方便的方法是将所有 JavaScript 文件移到一个文件夹中。

    要获取 PowerShell 示例,请运行相同的代码,并修改以下内容:

target_dir = "/path/to/JavascriptSamples/"
repositories = g.search_repositories(query='language:javascript')

接下来:

target_dir = "/path/to/PowerShellSamples/"
repositories = g.search_repositories(query='language:powershell').

同样,对于 Python 文件,我们进行如下操作:

target_dir = "/path/to/PythonSamples/"
repositories = g.search_repositories(query='language:python').

工作原理……

我们从导入PyGitHub库开始,进行步骤 1,这样我们就能方便地调用 GitHub API。这些 API 将允许我们抓取并探索仓库的世界。我们还导入了base64模块,以解码我们从 GitHub 下载的base64编码的文件。请注意,GitHub 对普通用户的 API 调用次数有限制。因此,如果你尝试在短时间内下载太多文件,你的脚本可能无法获取所有文件。我们的下一步是向 GitHub 提供我们的凭证(步骤 2),并指定我们正在寻找具有 JavaScript 语言的仓库,使用query='language:javascript'命令。我们列举出所有符合我们搜索条件的 JavaScript 仓库,如果符合条件,我们继续在这些仓库中查找以.js结尾的文件,并创建本地副本(步骤 3 到 6)。由于这些文件是base64编码的,我们确保在步骤 7 中将它们解码为纯文本。最后,我们展示如何调整脚本,以便抓取其他类型的文件,比如 Python 和 PowerShell 文件(步骤 8)。

按文件类型分类

现在我们已经有了一个数据集,我们希望训练一个分类器。由于相关文件是脚本文件,我们将问题作为一个自然语言处理(NLP)问题来处理。

准备工作

这个步骤的准备工作包括在pip中安装scikit-learn包。安装说明如下:

pip install sklearn

此外,我们还为您提供了JavascriptSamples.7zPythonSamples.7zPowerShellSamples.7z压缩包中每种文件类型的样本,以防您希望补充自己的数据集。将其解压到不同的文件夹中,以便按照以下步骤操作。

如何实现...

以下代码可在github.com/PacktPublishing/Machine-Learning-for-Cybersecurity-Cookbook/blob/master/Chapter02/Classifying%20Files%20by%20Type/File%20Type%20Classifier.ipynb找到。我们利用这些数据构建分类器,以预测文件为 JavaScript、Python 或 PowerShell:

  1. 首先导入必要的库并指定我们将用于训练和测试的样本的路径:
import os
from sklearn.feature_extraction.text import HashingVectorizer, TfidfTransformer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.pipeline import Pipeline

javascript_path = "/path/to/JavascriptSamples/"
python_path = "/path/to/PythonSamples/"
powershell_path = "/path/to/PowerShellSamples/"
  1. 接下来,我们读取所有文件类型。同时,我们创建一个标签数组,分别代表 JavaScript、Python 和 PowerShell 脚本,分别为-1、0 和 1:
corpus = []
labels = []
file_types_and_labels = [(javascript_path, -1), (python_path, 0), (powershell_path, 1)]
for files_path, label in file_types_and_labels:
    files = os.listdir(files_path)
    for file in files:
        file_path = files_path + "/" + file
        try:
            with open(file_path, "r") as myfile:
                data = myfile.read().replace("\n", "")
        except:
            pass
        data = str(data)
        corpus.append(data)
        labels.append(label)
  1. 我们继续创建训练-测试分割和管道,该管道将对文件执行基本的自然语言处理,然后使用随机森林分类器:
X_train, X_test, y_train, y_test = train_test_split(
    corpus, labels, test_size=0.33, random_state=11
)
text_clf = Pipeline(
    [
        ("vect", HashingVectorizer(input="content", ngram_range=(1, 3))),
        ("tfidf", TfidfTransformer(use_idf=True,)),
        ("rf", RandomForestClassifier(class_weight="balanced")),
    ]
)
  1. 我们将管道拟合到训练数据上,然后在测试数据上进行预测。最后,我们打印出准确度和混淆矩阵:
text_clf.fit(X_train, y_train)
y_test_pred = text_clf.predict(X_test)
print(accuracy_score(y_test, y_test_pred))
print(confusion_matrix(y_test, y_test_pred))

这将导致以下输出:

工作原理...

利用我们在从 GitHub 抓取特定类型文件配方中建立的数据集,我们将文件放置在不同的目录中,根据其文件类型,并指定路径以准备构建我们的分类器(步骤 1)。本配方的代码假设"JavascriptSamples"目录和其他目录包含样本,并且没有子目录。我们将所有文件读入一个语料库,并记录它们的标签(步骤 2)。我们对数据进行训练-测试分割,并准备一个管道,该管道将对文件执行基本的自然语言处理,然后使用随机森林分类器(步骤 3)。这里选择的分类器是为了说明目的,而不是为了暗示对于这类数据的最佳分类器选择。最后,我们执行创建机器学习分类器过程中的基本但重要的步骤,包括将管道拟合到训练数据上,然后通过测量其在测试集上的准确性和混淆矩阵来评估其性能(步骤 4)。

测量两个字符串之间的相似度

要检查两个文件是否相同,我们使用标准的加密哈希函数,例如 SHA256 和 MD5。然而,有时我们也想知道两个文件在多大程度上相似。为此,我们使用相似性哈希算法。在这里我们将演示的是ssdeep

首先,让我们看看如何使用ssdeep比较两个字符串。这对于检测文本或脚本中的篡改以及抄袭非常有用。

准备工作

本食谱的准备工作包括在pip中安装ssdeep包。安装过程稍微复杂,并且在 Windows 上并不总是能成功。安装说明可以在python-ssdeep.readthedocs.io/en/latest/installation.html找到。

如果你只有一台 Windows 机器,并且安装ssdeep没有成功,那么一种可能的解决方法是通过运行ssdeep在 Ubuntu 虚拟机上,然后在pip中安装它,使用以下命令:

pip install ssdeep

如何操作...

  1. 首先导入ssdeep库并创建三个字符串:
import ssdeep

str1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
str2 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore Magna aliqua."
str3 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore aliqua."
str4 = "Something completely different from the other strings."
  1. 对字符串进行哈希:
hash1 = ssdeep.hash(str1)
hash2 = ssdeep.hash(str2)
hash3 = ssdeep.hash(str3)
hash4 = ssdeep.hash(str4)

作为参考,

hash1 是u'3:f4oo8MRwRJFGW1gC6uWv6MQ2MFSl+JuBF8BSnJi:f4kPvtHMCMubyFtQ'

hash2 是u'3:f4oo8MRwRJFGW1gC6uWv6MQ2MFSl+JuBF8BS+EFECJi:f4kPvtHMCMubyFIsJQ'

hash3 是u'3:f4oo8MRwRJFGW1gC6uWv6MQ2MFSl+JuBF8BS6:f4kPvtHMCMubyF0,并且

hash4 是u'3:60QKZ+4CDTfDaRFKYLVL:ywKDC2mVL'

  1. 接下来,我们查看这些字符串的相似度分数:
ssdeep.compare(hash1, hash1)
ssdeep.compare(hash1, hash2)
ssdeep.compare(hash1, hash3)
ssdeep.compare(hash1, hash4)

数值结果如下:

100
39
37
0

它是如何工作的...

ssdeep的基本思路是结合多个传统哈希,这些哈希的边界由输入的上下文决定。这些哈希集合可以用来识别已知文件的修改版本,即使它们已经通过插入、修改或删除进行了更改。

在本食谱中,我们首先创建了四个测试字符串,作为一个玩具示例,来说明字符串的变化如何影响其相似度度量(步骤 1)。第一个字符串str1仅仅是 Lorem Ipsum 的第一句。第二个字符串str2在“magna”中的m字母大小写上有所不同。第三个字符串str3完全缺少了“magna”这个词。最后,第四个字符串是完全不同的字符串。我们的下一步,步骤 2,是使用相似度哈希库ssdeep对这些字符串进行哈希处理。请注意,相似的字符串具有明显相似的相似度哈希。这与传统哈希形成鲜明对比,在传统哈希中,即使是一个小小的修改也会产生完全不同的哈希。接下来,我们通过ssdeep(步骤 3)得出这些字符串之间的相似度分数。特别注意,ssdeep的相似度分数是一个介于 0 和 100 之间的整数,100 表示完全相同,0 表示完全不同。两个完全相同的字符串的相似度分数是 100。改变一个字母的大小写会显著降低相似度分数至 39,因为字符串相对较短。删除一个单词将相似度分数降低到 37。两个完全不同的字符串的相似度为 0。

虽然还有其他一些在某些情况下更好的模糊哈希可供选择,但由于ssdeep的速度和它作为事实标准的地位,它仍然是首选。

测量两个文件之间的相似度

现在,我们将了解如何应用ssdeep来衡量两个二进制文件之间的相似性。这个概念有很多应用,尤其是在聚类中将相似性度量作为距离。

准备工作

本示例的准备工作包括通过pip安装ssdeep包。安装过程有点复杂,且在 Windows 上并不总是有效。可以参考python-ssdeep.readthedocs.io/en/latest/installation.html上的说明。

如果你只有一台 Windows 机器且无法运行,那么一个可能的解决方案是在 Ubuntu 虚拟机上通过安装pip并使用以下命令运行ssdeep

pip install ssdeep

此外,请从www.python.org/ftp/python/3.7.2/python-3.7.2-amd64.exe下载测试文件,如 Python 可执行文件。

如何操作...

在下面的示例中,我们篡改一个二进制文件。然后我们将其与原文件进行比较,发现ssdeep认为这两个文件高度相似但并不完全相同:

  1. 首先,我们下载 Python 的最新版本,python-3.7.2-amd64.exe。我将创建一个副本,将其重命名为python-3.7.2-amd64-fake.exe,并在末尾添加一个空字节:
truncate -s +1 python-3.7.2-amd64-fake.exe
  1. 使用hexdump,我可以通过查看操作前后的文件来验证操作是否成功:
hexdump -C python-3.7.2-amd64.exe |tail -5

这将产生以下输出:


018ee0f0  e3 af d6 e9 05 3f b7 15  a1 c7 2a 5f b6 ae 71 1f  |.....?....*_..q.|
018ee100  6f 46 62 1c 4f 74 f5 f5  a1 e6 91 b7 fe 90 06 3e  |oFb.Ot.........>|
018ee110  de 57 a6 e1 83 4c 13 0d  b1 4a 3d e5 04 82 5e 35  |.W...L...J=...⁵|
018ee120  ff b2 e8 60 2d e0 db 24  c1 3d 8b 47 b3 00 00 00  |...`-..$.=.G....|

也可以使用以下命令通过第二个文件进行验证:

hexdump -C python-3.7.2-amd64-fake.exe |tail -5

这将产生以下输出:

018ee100  6f 46 62 1c 4f 74 f5 f5  a1 e6 91 b7 fe 90 06 3e  |oFb.Ot.........>|
018ee110  de 57 a6 e1 83 4c 13 0d  b1 4a 3d e5 04 82 5e 35  |.W...L...J=...⁵|
018ee120  ff b2 e8 60 2d e0 db 24  c1 3d 8b 47 b3 00 00 00  |...`-..$.=.G....|
018ee130  00                                                |.|
018ee131
  1. 现在,我将使用ssdeep对两个文件进行哈希,并比较结果:
import ssdeep

hash1 = ssdeep.hash_from_file("python-3.7.2-amd64.exe")
hash2 = ssdeep.hash_from_file("python-3.7.2-amd64-fake.exe")
ssdeep.compare(hash1, hash2)

上述代码的输出是99

它是如何工作的...

这个场景模拟了篡改文件后,利用相似性哈希检测篡改的存在,并衡量差异的大小。我们从一个标准的 Python 可执行文件开始,然后通过在末尾添加一个空字节进行篡改(步骤 1)。在现实中,黑客可能会在合法程序中插入恶意代码。我们通过hexdump双重检查篡改是否成功,并检查篡改的性质(步骤 2)。然后,我们对原始文件和篡改后的文件进行相似性计算,观察到发生了微小的变化(步骤 3)。仅使用标准哈希,我们将无法知道这两个文件之间的关系,除了得出它们不是同一个文件的结论。知道如何比较文件使我们能够在机器学习算法中对恶意软件和良性文件进行聚类,并将它们分组为家族。

提取 N-gram

在文本的标准定量分析中,N-gram 是由 N 个标记(例如,单词或字符)组成的序列。例如,给定文本The quick brown fox jumped over the lazy dog,如果我们的标记是单词,则 1-gram 是thequickbrownfoxjumpedoverthelazydog。2-gram 是the quickquick brownbrown fox,依此类推。3-gram 是the quick brownquick brown foxbrown fox jumped,依此类推。就像文本的局部统计信息使我们能够构建马尔可夫链以进行统计预测和从语料库生成文本一样,N-gram 使我们能够建模语料库的局部统计特性。我们的最终目标是利用 N-gram 的计数帮助我们预测样本是恶意的还是良性的。在本食谱中,我们演示了如何从样本中提取 N-gram 计数。

准备工作

本食谱的准备工作包括在pip中安装nltk包,安装说明如下:

pip install nltk

此外,下载一个测试文件,例如来自www.python.org/ftp/python/3.7.2/python-3.7.2-amd64.exe的 Python 可执行文件。

如何做到这一点...

在接下来的步骤中,我们将枚举一个示例文件的所有 4-gram,并选择其中 50 个最频繁的:

  1. 我们首先导入collections库以方便计数,并从nltk库导入ngrams库以简化 N-gram 的提取:
import collections
from nltk import ngrams
  1. 我们指定要分析的文件:
file_to_analyze = "python-3.7.2-amd64.exe"
  1. 我们定义了一个便捷函数来读取文件的字节:
def read_file(file_path):
    """Reads in the binary sequence of a binary file."""
    with open(file_path, "rb") as binary_file:
        data = binary_file.read()
    return data
  1. 我们编写一个便捷函数来处理字节序列并获取 N-gram:
def byte_sequence_to_Ngrams(byte_sequence, N):
    """Creates a list of N-grams from a byte sequence."""
    Ngrams = ngrams(byte_sequence, N)
    return list(Ngrams)
  1. 我们编写一个函数来处理文件并获取其 N-gram 的计数:
def binary_file_to_Ngram_counts(file, N):
    """Takes a binary file and outputs the N-grams counts of its binary sequence."""
    filebyte_sequence = read_file(file)
    file_Ngrams = byte_sequence_to_Ngrams(filebyte_sequence, N)
    return collections.Counter(file_Ngrams)
  1. 我们指定所需的值为 N=4,并获取文件中所有 4-gram 的计数:
extracted_Ngrams = binary_file_to_Ngram_counts(file_to_analyze, 4)
  1. 我们列出了文件中最常见的 10 个 4-gram:
print(extracted_Ngrams.most_common(10))

结果如下:

[((0, 0, 0, 0), 24201), ((139, 240, 133, 246), 1920), ((32, 116, 111, 32), 1791), ((255, 255, 255, 255), 1663), ((108, 101, 100, 32), 1522), ((100, 32, 116, 111), 1519), ((97, 105, 108, 101), 1513), ((105, 108, 101, 100), 1513), ((70, 97, 105, 108), 1505), ((101, 100, 32, 116), 1503)]

它是如何工作的...

在文献和工业界中已经确定,最常见的 N-gram 也是恶意软件分类算法中最具信息量的。因此,在本教程中,我们将编写函数来提取文件的 N-gram。我们首先导入一些有助于提取 N-gram 的库(第 1 步)。特别地,我们导入collections库和nltk中的ngrams库。collections库允许我们将 N-gram 列表转换为 N-gram 的频次计数,而ngrams库则允许我们获取有序字节列表并得到 N-gram 列表。我们指定要分析的文件,并编写一个函数来读取给定文件的所有字节(第 2 步和第 3 步)。在开始提取之前,我们定义几个便利函数。特别地,我们编写一个函数来获取文件的字节序列并输出其 N-gram 列表(第 4 步),并编写一个函数来获取文件并输出其 N-gram 计数(第 5 步)。现在我们准备传入文件并提取其 N-gram。我们这样做以提取文件的 4-gram 计数(第 6 步),然后展示其中最常见的 10 个及其计数(第 7 步)。我们看到一些 N-gram 序列,如(0,0,0,0)和(255,255,255,255),可能不太有信息量。因此,我们将在下一个教程中利用特征选择方法去除这些不太有信息量的 N-gram。

选择最佳 N-gram

不同 N-gram 的数量随着 N 的增大呈指数增长。即使对于一个固定的小 N,如 N=3,也有256x256x256=16,777,216种可能的 N-gram。这意味着 N-gram 特征的数量巨大,几乎不可能实际使用。因此,我们必须选择一个较小的 N-gram 子集,这些子集将对我们的分类器最有价值。在这一部分中,我们展示了三种选择最具信息量 N-gram 的不同方法。

准备工作

本教程的准备工作包括在pip中安装scikit-learnnltk包。安装说明如下:

pip install sklearn nltk

此外,善意和恶意文件已在仓库根目录下的PE Samples Dataset文件夹中提供。将所有名为Benign PE Samples*.7z的压缩包解压到名为Benign PE Samples的文件夹中。将所有名为Malicious PE Samples*.7z的压缩包解压到名为Malicious PE Samples的文件夹中。

如何实现...

在接下来的步骤中,我们展示了三种选择最具信息量 N-gram 的方法。本教程假设已包括前一个教程中的binaryFileToNgramCounts(file, N)和所有其他辅助函数:

  1. 首先指定包含我们样本的文件夹,指定我们的N,并导入模块以枚举文件:
from os import listdir
from os.path import isfile, join

directories = ["Benign PE Samples", "Malicious PE Samples"]
N = 2
  1. 接下来,我们从所有文件中统计所有的 N-gram:
Ngram_counts_all_files = collections.Counter([])
for dataset_path in directories:
    all_samples = [f for f in listdir(dataset_path) if isfile(join(dataset_path, f))]
    for sample in all_samples:
        file_path = join(dataset_path, sample)
        Ngram_counts_all_files += binary_file_to_Ngram_counts(file_path, N)
  1. 我们将K1=1000个最常见的 N-gram 收集到一个列表中:
K1 = 1000
K1_most_frequent_Ngrams = Ngram_counts_all_files.most_common(K1)
K1_most_frequent_Ngrams_list = [x[0] for x in K1_most_frequent_Ngrams]
  1. 一个辅助方法featurize_sample将用于获取一个样本并输出其字节序列中最常见 N-gram 的出现次数:
def featurize_sample(sample, K1_most_frequent_Ngrams_list):
    """Takes a sample and produces a feature vector.
    The features are the counts of the K1 N-grams we've selected.
    """
    K1 = len(K1_most_frequent_Ngrams_list)
    feature_vector = K1 * [0]
    file_Ngrams = binary_file_to_Ngram_counts(sample, N)
    for i in range(K1):
        feature_vector[i] = file_Ngrams[K1_most_frequent_Ngrams_list[i]]
    return feature_vector
  1. 我们遍历目录,并使用前面的 featurize_sample 函数来特征化我们的样本。同时,我们创建一组标签:
directories_with_labels = [("Benign PE Samples", 0), ("Malicious PE Samples", 1)]
X = []
y = []
for dataset_path, label in directories_with_labels:
    all_samples = [f for f in listdir(dataset_path) if isfile(join(dataset_path, f))]
    for sample in all_samples:
        file_path = join(dataset_path, sample)
        X.append(featurize_sample(file_path, K1_most_frequent_Ngrams_list))
        y.append(label)
  1. 我们导入将用于特征选择的库,并指定希望缩小到多少个特征:
from sklearn.feature_selection import SelectKBest, mutual_info_classif, chi2

K2 = 10
  1. 我们对 N-gram 进行三种类型的特征选择:
  • 频率—选择最常见的 N-gram:
X = np.asarray(X)
X_top_K2_freq = X[:,:K2]
  • 互信息—通过互信息算法选择排名最高的 N-gram:
mi_selector = SelectKBest(mutual_info_classif, k=K2)
X_top_K2_mi = mi_selector.fit_transform(X, y)
  • 卡方—通过卡方算法选择排名最高的 N-gram:
chi2_selector = SelectKBest(chi2, k=K2)
X_top_K2_ch2 = chi2_selector.fit_transform(X, y)

它是如何工作的……

与之前的方案不同,在那里我们分析了单个文件的 N-gram,而在这个方案中,我们会查看大量文件,以了解哪些 N-gram 是最具信息量的特征。我们首先指定包含样本的文件夹,N 的值,并导入一些模块来列举文件(步骤 1)。接下来,我们统计数据集中的所有文件的所有 N-gram(步骤 2)。这使我们能够找到全局最常见的 N-gram。在这些 N-gram 中,我们筛选出 K1=1000 个最常见的(步骤 3)。然后,我们引入一个辅助方法 featurizeSample,用于提取样本并输出其字节序列中 K1 个最常见 N-gram 的出现次数(步骤 4)。接下来,我们遍历文件目录,并使用之前的 featurizeSample 函数来特征化样本,同时记录它们的标签,标记为恶意或良性(步骤 5)。标签的重要性在于,评估某个 N-gram 是否具有信息量,取决于能否基于其区分恶意和良性类别。

我们导入 SelectKBest 库,通过评分函数选择最佳特征,以及两种评分函数:互信息和卡方(步骤 6)。最后,我们应用三种不同的特征选择方案来选择最佳的 N-gram,并将这些知识应用于转换我们的特征(步骤 7)。在第一种方法中,我们简单地选择 K2 个最常见的 N-gram。请注意,这种选择方法在文献中经常推荐,因为它不需要标签或复杂的计算,较为简单。在第二种方法中,我们使用互信息来缩小 K2 个特征,而在第三种方法中,我们使用卡方来进行选择。

构建静态恶意软件检测器

在本节中,我们将看到如何将之前讨论的方案组合起来,构建一个恶意软件检测器。我们的恶意软件检测器将同时采用从 PE 头部提取的特征以及从 N-gram 派生的特征。

准备工作

本方案的准备工作包括在 pip 中安装 scikit-learnnltkpefile 包。安装说明如下:

pip install sklearn nltk pefile

另外,在存储库根目录的"PE Samples Dataset"文件夹中已为您提供了良性和恶意文件。请将名为"Benign PE Samples*.7z"的所有存档解压到名为"Benign PE Samples"的文件夹中。将名为"Malicious PE Samples*.7z"的所有存档解压到名为"Malicious PE Samples"的文件夹中。

如何做...

在接下来的步骤中,我们将展示一个完整的工作流程,我们将从原始样本开始,对其进行特征提取,将结果向量化,将它们组合在一起,最后训练和测试分类器:

  1. 首先,列举我们的样本并分配它们的标签:
import os
from os import listdir

directories_with_labels = [("Benign PE Samples", 0), ("Malicious PE Samples", 1)]
list_of_samples = []
labels = []
for dataset_path, label in directories_with_labels:
    samples = [f for f in listdir(dataset_path)]
    for sample in samples:
        file_path = os.path.join(dataset_path, sample)
        list_of_samples.append(file_path)
        labels.append(label)
  1. 我们执行分层的训练测试分离:
from sklearn.model_selection import train_test_split

samples_train, samples_test, labels_train, labels_test = train_test_split(
    list_of_samples, labels, test_size=0.3, stratify=labels, random_state=11
)
  1. 我们引入之前章节中的便捷函数,以获取特征:
import collection
from nltk import ngrams
import numpy as np
import pefile

def read_file(file_path):
    """Reads in the binary sequence of a binary file."""
    with open(file_path, "rb") as binary_file:
        data = binary_file.read()
    return data

def byte_sequence_to_Ngrams(byte_sequence, N):
    """Creates a list of N-grams from a byte sequence."""
    Ngrams = ngrams(byte_sequence, N)
    return list(Ngrams)

def binary_file_to_Ngram_counts(file, N):
    """Takes a binary file and outputs the N-grams counts of its binary sequence."""
    filebyte_sequence = read_file(file)
    file_Ngrams = byte_sequence_to_Ngrams(filebyte_sequence, N)
    return collections.Counter(file_Ngrams)

def get_NGram_features_from_sample(sample, K1_most_frequent_Ngrams_list):
    """Takes a sample and produces a feature vector.
    The features are the counts of the K1 N-grams we've selected.
    """
    K1 = len(K1_most_frequent_Ngrams_list)
    feature_vector = K1 * [0]
    file_Ngrams = binary_file_to_Ngram_counts(sample, N)
    for i in range(K1):
        feature_vector[i] = file_Ngrams[K1_most_frequent_Ngrams_list[i]]
    return feature_vector

def preprocess_imports(list_of_DLLs):
    """Normalize the naming of the imports of a PE file."""
    temp = [x.decode().split(".")[0].lower() for x in list_of_DLLs]
    return " ".join(temp)

def get_imports(pe):
    """Get a list of the imports of a PE file."""
    list_of_imports = []
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        list_of_imports.append(entry.dll)
    return preprocess_imports(list_of_imports)

def get_section_names(pe):
    """Gets a list of section names from a PE file."""
    list_of_section_names = []
    for sec in pe.sections:
        normalized_name = sec.Name.decode().replace("\x00", "").lower()
        list_of_section_names.append(normalized_name)
    return "".join(list_of_section_names)
  1. 我们选择前 100 个最常见的二元组作为我们的特征:
N = 2
Ngram_counts_all = collections.Counter([])
for sample in samples_train:
    Ngram_counts_all += binary_file_to_Ngram_counts(sample, N)
K1 = 100
K1_most_frequent_Ngrams = Ngram_counts_all.most_common(K1)
K1_most_frequent_Ngrams_list = [x[0] for x in K1_most_frequent_Ngrams]
  1. 我们提取每个样本中的 N-gram 计数、节名称、导入以及节的数量,跳过无法解析 PE 头部的样本:
imports_corpus_train = []
num_sections_train = []
section_names_train = []
Ngram_features_list_train = []
y_train = []
for i in range(len(samples_train)):
    sample = samples_train[i]
    try:
        NGram_features = get_NGram_features_from_sample(
            sample, K1_most_frequent_Ngrams_list
        )
        pe = pefile.PE(sample)
        imports = get_imports(pe)
        n_sections = len(pe.sections)
        sec_names = get_section_names(pe)
        imports_corpus_train.append(imports)
        num_sections_train.append(n_sections)
        section_names_train.append(sec_names)
        Ngram_features_list_train.append(NGram_features)
        y_train.append(labels_train[i])
    except Exception as e:
        print(sample + ":")
        print(e)
  1. 我们使用哈希向量化器,然后使用tfidf将导入和节名称(均为文本特征)转换为数值形式:
from sklearn.feature_extraction.text import HashingVectorizer, TfidfTransformer
from sklearn.pipeline import Pipeline

imports_featurizer = Pipeline(
    [
       ("vect", HashingVectorizer(input="content", ngram_range=(1, 2))),
        ("tfidf", TfidfTransformer(use_idf=True,)),
    ]
)
section_names_featurizer = Pipeline(
    [
        ("vect", HashingVectorizer(input="content", ngram_range=(1, 2))),
        ("tfidf", TfidfTransformer(use_idf=True,)),
    ]
)
imports_corpus_train_transformed = imports_featurizer.fit_transform(
    imports_corpus_train
)
section_names_train_transformed = section_names_featurizer.fit_transform(
    section_names_train
)
  1. 我们将向量化的特征合并为单个数组:
from scipy.sparse import hstack, csr_matrix

X_train = hstack(
    [
        Ngram_features_list_train,
        imports_corpus_train_transformed,
        section_names_train_transformed,
        csr_matrix(num_sections_train).transpose(),
    ]
)
  1. 我们在训练集上训练了一个随机森林分类器,并打印出其得分:
from sklearn.ensemble import RandomForestClassifier

clf = RandomForestClassifier(n_estimators=100)
clf = clf.fit(X_train, y_train)
  1. 我们收集测试集的特征,就像我们对训练集所做的一样:
imports_corpus_test = []
num_sections_test = []
section_names_test = []
Ngram_features_list_test = []
y_test = []
for i in range(len(samples_test)):
    file = samples_test[i]
    try:
        NGram_features = get_NGram_features_from_sample(
            sample, K1_most_frequent_Ngrams_list
        )
        pe = pefile.PE(file)
        imports = get_imports(pe)
        n_sections = len(pe.sections)
        sec_names = get_section_names(pe)
        imports_corpus_test.append(imports)
        num_sections_test.append(n_sections)
        section_names_test.append(sec_names)
        Ngram_features_list_test.append(NGram_features)
        y_test.append(labels_test[i])
    except Exception as e:
        print(sample + ":")
        print(e)
  1. 我们将先前训练过的转换器应用于向量化文本特征,然后在生成的测试集上测试我们的分类器:
imports_corpus_test_transformed = imports_featurizer.transform(imports_corpus_test)
section_names_test_transformed = section_names_featurizer.transform(section_names_test)
X_test = hstack(
    [
        Ngram_features_list_test,
        imports_corpus_test_transformed,
        section_names_test_transformed,
        csr_matrix(num_sections_test).transpose(),
    ]
)
print(clf.score(X_test, y_test))

我们的分类器得分如下:

0.8859649122807017

工作原理...

本节中有几个值得注意的新想法。我们首先列举我们的样本并为它们分配其相应的标签(步骤 1)。由于我们的数据集不平衡,使用分层的训练测试分离是合理的(步骤 2)。在分层的训练测试分离中,创建一个训练集和测试集,其中每个类的比例与原始集中的比例相同。这确保了训练集不会由于偶然事件而只包含一个类。接下来,我们加载将用于对样本进行特征提取的函数。我们像以前的方法一样使用我们的特征提取技术来计算最佳的 N-gram 特征(步骤 4),然后遍历所有文件以提取所有特征(步骤 5)。然后,我们使用基本的自然语言处理方法对先前获取的 PE 头部特征,如节名称和导入,进行向量化(步骤 6)。

获得了所有这些不同的特征后,我们现在可以将它们合并在一起,这一步通过使用scipy的 hstack 来完成,将不同的特征合并为一个大的稀疏scipy数组(步骤 7)。接下来,我们继续训练一个使用默认参数的随机森林分类器(步骤 8),然后对我们的测试集重复提取过程(步骤 9)。在步骤 10 中,我们最终测试我们的训练好的分类器,并获得一个有前景的起始分数。总的来说,这个配方为一个恶意软件分类器提供了基础,可以扩展成一个强大的解决方案。

解决类别不平衡问题

在将机器学习应用于网络安全时,我们经常面对严重不平衡的数据集。例如,获取大量正常样本可能比收集恶意样本容易得多。反过来,你可能在一个因法律原因而禁止保存正常样本的企业工作。在这两种情况下,你的数据集都会严重偏向于某一类。因此,旨在最大化准确度的简单机器学习方法将导致一个几乎将所有样本预测为来自过度代表类的分类器。有几种技术可以用来解决类别不平衡的问题。

准备工作

本配方的准备工作包括安装scikit-learnimbalanced-learn的 pip 包。安装说明如下:

pip install sklearn imbalanced-learn

如何操作...

在接下来的步骤中,我们将演示几种处理不平衡数据的方法:

  1. 首先加载训练和测试数据,导入决策树,以及我们将用来评估性能的一些库:
from sklearn import tree
from sklearn.metrics import balanced_accuracy_score
import numpy as np
import scipy.sparse
import collections

X_train = scipy.sparse.load_npz("training_data.npz")
y_train = np.load("training_labels.npy")
X_test = scipy.sparse.load_npz("test_data.npz")
y_test = np.load("test_labels.npy")
  1. 训练和测试一个简单的决策树分类器:
dt = tree.DecisionTreeClassifier()
dt.fit(X_train, y_train)
dt_pred = dt.predict(X_test)
print(collections.Counter(dt_pred))
print(balanced_accuracy_score(y_test, dt_pred))

这将产生以下输出:

Counter({0: 121, 1: 10})
0.8333333333333333

接下来,我们测试几种提高性能的技术。

  1. **加权:**我们将分类器的类别权重设置为"balanced",并训练和测试这个新分类器:
dt_weighted = tree.DecisionTreeClassifier(class_weight="balanced")
dt_weighted.fit(X_train, y_train)
dt_weighted_pred = dt_weighted.predict(X_test)
print(collections.Counter(dt_weighted_pred))
print(balanced_accuracy_score(y_test, dt_weighted_pred))

这将产生以下输出:

Counter({0: 114, 1: 17})
0.9913793103448276
  1. **对小类别进行上采样:**我们从类别 0 和类别 1 中提取所有测试样本:
from sklearn.utils import resample

X_train_np = X_train.toarray()
class_0_indices = [i for i, x in enumerate(y_train == 0) if x]
class_1_indices = [i for i, x in enumerate(y_train == 1) if x]
size_class_0 = sum(y_train == 0)
X_train_class_0 = X_train_np[class_0_indices, :]
y_train_class_0 = [0] * size_class_0
X_train_class_1 = X_train_np[class_1_indices, :]
  1. 我们对类别 1 的元素进行有放回的上采样,直到类别 1 和类别 0 的样本数量相等:
X_train_class_1_resampled = resample(
    X_train_class_1, replace=True, n_samples=size_class_0
)
y_train_class_1_resampled = [1] * size_class_0
  1. 我们将新上采样的样本合并成一个训练集:
X_train_resampled = np.concatenate([X_train_class_0, X_train_class_1_resampled])
y_train_resampled = y_train_class_0 + y_train_class_1_resampled
  1. 我们在上采样的训练集上训练并测试一个随机森林分类器:
from scipy import sparse

X_train_resampled = sparse.csr_matrix(X_train_resampled)
dt_resampled = tree.DecisionTreeClassifier()
dt_resampled.fit(X_train_resampled, y_train_resampled)
dt_resampled_pred = dt_resampled.predict(X_test)
print(collections.Counter(dt_resampled_pred))
print(balanced_accuracy_score(y_test, dt_resampled_pred))

这将产生以下输出:

Counter({0: 114, 1: 17})
0.9913793103448276
  1. **对大类别进行下采样:**我们执行与上采样相似的步骤,只不过这次我们对大类别进行下采样,直到它与小类别的样本数量相等:
X_train_np = X_train.toarray()
class_0_indices = [i for i, x in enumerate(y_train == 0) if x]
class_1_indices = [i for i, x in enumerate(y_train == 1) if x]
size_class_1 = sum(y_train == 1)
X_train_class_1 = X_train_np[class_1_indices, :]
y_train_class_1 = [1] * size_class_1
X_train_class_0 = X_train_np[class_0_indices, :]
X_train_class_0_downsampled = resample(
    X_train_class_0, replace=False, n_samples=size_class_1
)
y_train_class_0_downsampled = [0] * size_class_1
  1. 我们从下采样数据中创建一个新的训练集:
X_train_downsampled = np.concatenate([X_train_class_1, X_train_class_0_downsampled])
y_train_downsampled = y_train_class_1 + y_train_class_0_downsampled
  1. 我们在这个数据集上训练一个随机森林分类器:
X_train_downsampled = sparse.csr_matrix(X_train_downsampled)
dt_downsampled = tree.DecisionTreeClassifier()
dt_downsampled.fit(X_train_downsampled, y_train_downsampled)
dt_downsampled_pred = dt_downsampled.predict(X_test)
print(collections.Counter(dt_downsampled_pred))
print(balanced_accuracy_score(y_test, dt_downsampled_pred))

这将产生以下输出:

Counter({0: 100, 1: 31})
0.9310344827586207
  1. **包含内置平衡采样器的分类器:**我们使用imbalanced-learn包中的分类器,这些分类器在训练估计器之前会对数据子集进行重采样:
from imblearn.ensemble import BalancedBaggingClassifier

balanced_clf = BalancedBaggingClassifier(
    base_estimator=tree.DecisionTreeClassifier(),
    sampling_strategy="auto",
    replacement=True,
)
balanced_clf.fit(X_train, y_train)
balanced_clf_pred = balanced_clf.predict(X_test)
print(collections.Counter(balanced_clf_pred))
print(balanced_accuracy_score(y_test, balanced_clf_pred))

这将产生以下输出:

Counter({0: 113, 1: 18})
0.9494252873563218

如何实现…

我们首先加载一个预定义的数据集(第 1 步),使用scipy.sparse.load_npz加载函数来加载之前保存的稀疏矩阵。下一步是对我们的数据训练一个基本的决策树模型(第 2 步)。为了评估性能,我们使用平衡准确度分数,这是一种常用于处理不平衡数据集分类问题的衡量标准。根据定义,平衡准确度是对每个类别召回率的平均值。最好的值是 1,而最差的值是 0。

在接下来的步骤中,我们采用不同的技术来解决类别不平衡问题。我们的第一个方法是利用类别权重来调整决策树,以适应不平衡的数据集(第 3 步)。平衡模式使用* y 的值自动调整权重,该权重与输入数据中类别的频率成反比,公式为n_samples / (n_classes * np.bincount(y))*。在第 4 到第 7 步中,我们使用上采样来处理类别不平衡。这是通过随机复制少数类的观察值,以加强少数类的信号。

有几种方法可以做到这一点,但最常见的做法是像我们之前那样进行有放回的重采样。上采样的两个主要问题是,它会增加数据集的大小,并且由于多次在相同样本上训练,它可能导致过拟合。在第 8 到第 10 步中,我们进行了主要类别的下采样。这意味着我们不会使用所有样本,而是使用足够的样本来平衡类别。

这个技术的主要问题在于我们被迫使用一个较小的训练集。我们最终的方案,也是最复杂的方案,是使用一个包括内部平衡采样器的分类器,即来自imbalanced-learnBalancedBaggingClassifier(第 11 步)。总体来看,我们发现每一种应对类别不平衡的方法都提高了平衡准确度分数。

处理类型 I 和类型 II 错误

在许多机器学习的情境中,一种错误可能比另一种更重要。例如,在一个多层防御系统中,要求某一层具有低假警报(低假阳性)率可能是合理的,虽然这会牺牲一定的检测率。在这一部分中,我们提供了一种确保假阳性率(FPR)不超过预定限制的方法,具体通过使用阈值化来实现。

准备工作

准备这一方法需要在pip中安装scikit-learnxgboost。安装说明如下:

pip install sklearn xgboost

如何做到这一点……

在接下来的步骤中,我们将加载一个数据集,训练一个分类器,然后调整阈值以满足假阳性率的约束:

  1. 我们加载一个数据集并指定所需的假阳性率(FPR)为 1%或更低:
import numpy as np
from scipy import sparse
import scipy

X_train = scipy.sparse.load_npz("training_data.npz")
y_train = np.load("training_labels.npy")
X_test = scipy.sparse.load_npz("test_data.npz")
y_test = np.load("test_labels.npy")
desired_FPR = 0.01
  1. 我们编写方法来计算FPRTPR
from sklearn.metrics import confusion_matrix

def FPR(y_true, y_pred):
    """Calculate the False Positive Rate."""
    CM = confusion_matrix(y_true, y_pred)
    TN = CM[0][0]
    FP = CM[0][1]
    return FP / (FP + TN)

def TPR(y_true, y_pred):
    """Calculate the True Positive Rate."""
    CM = confusion_matrix(y_true, y_pred)
    TP = CM[1][1]
    FN = CM[1][0]
    return TP / (TP + FN)
  1. 我们编写一个方法,通过阈值化将概率向量转换为布尔向量:
def perform_thresholding(vector, threshold):
    """Threshold a vector."""
    return [0 if x >= threshold else 1 for x in vector]
  1. 我们训练一个 XGBoost 模型并计算训练数据的概率预测:
from xgboost import XGBClassifier

clf = XGBClassifier()
clf.fit(X_train, y_train)
clf_pred_prob = clf.predict_proba(X_train)
  1. 让我们检查一下我们的预测概率向量:
print("Probabilities look like so:")
print(clf_pred_prob[0:5])
print()

这将产生以下输出:

Probabilities look like so:
[[0.9972162 0.0027838 ]
[0.9985584 0.0014416 ]
[0.9979202 0.00207978]
[0.96858877 0.03141126]
[0.91427565 0.08572436]]
  1. 我们遍历 1000 个不同的阈值,计算每个阈值的 FPR,当满足FPR<=desiredFPR时,我们选择那个阈值:
M = 1000
print("Fitting threshold:")
for t in reversed(range(M)):
    scaled_threshold = float(t) / M
    thresholded_prediction = perform_thresholding(clf_pred_prob[:, 0], scaled_threshold)
    print(t, FPR(y_train, thresholded_prediction), TPR(y_train, thresholded_prediction))
    if FPR(y_train, thresholded_prediction) <= desired_FPR:
        print()
        print("Selected threshold: ")
        print(scaled_threshold)
        break

这将产生以下输出:

Fitting threshold:
999 1.0 1.0
998 0.6727272727272727 1.0
997 0.4590909090909091 1.0
996 0.33181818181818185 1.0
 <snip>
 649 0.05454545454545454 1.0
648 0.004545454545454545 0.7857142857142857
Selected threshold: 0.648

工作原理……

我们通过加载一个先前已特征化的数据集并指定一个 1%的期望 FPR 约束来开始这个过程(步骤 1)。实际使用的值高度依赖于具体情况和所考虑的文件类型。这里有几点需要考虑:如果文件是极为常见,但很少是恶意的,例如 PDF 文件,那么期望的 FPR 必须设置得非常低,例如 0.01%。

如果系统得到了额外系统的支持,这些系统可以在没有人工干预的情况下再次验证其判定结果,那么较高的 FPR 可能不会有害。最后,客户可能会有一个偏好,这将建议一个推荐值。我们在步骤 2 中定义了一对便捷函数,用于计算 FPR 和 TPR——这些函数非常实用且可重复使用。我们定义的另一个便捷函数是一个函数,它将接受我们的阈值,并用它对一个数值向量进行阈值处理(步骤 3)。

在步骤 4 中,我们在训练数据上训练模型,并在训练集上确定预测概率。你可以在步骤 5 中看到这些预测结果。当有大量数据集时,使用验证集来确定适当的阈值将减少过拟合的可能性。最后,我们计算将来分类时要使用的阈值,以确保满足 FPR 约束(步骤 6)。