恶意软件全文搜索技术

恶意软件集合中的全文搜索

摘要

本文旨在提供在大型恶意软件集合中按内容进行快速搜索的技术。对于恶意软件研究人员而言,检索具有特定内容的恶意软件样本,以查找新样本的先前实例或测试新特征码,具有重要意义。我们提出了一种数据结构,可实现快速搜索,并能持续添加新的样本进行扩展。通过在真实世界恶意软件上的实验,验证了本解决方案的性能和可扩展性。

关键词 :恶意软件 · Big数据 · Content search

1 引言

最近的研究表明,恶意软件仍然是计算机系统的主要威胁,目前有7.8亿个不同的恶意软件样本,其数量每月增长数百万[4]。关于恶意软件生态系统的更详细描述见于[5],其中描述了恶意软件家族和恶意软件变种的概念。在特定时间段内在市场上活跃的数百万个恶意软件样本,尽管是不同的软件个体,但它们属于数量少得多的恶意软件家族。

恶意软件作者采用混淆技术和加壳工具,以生成多个不会被常规杀毒软件特征码检测到的恶意软件二进制文件。当目前发布的一组样本(称为一个变体)被大多数杀毒软件引擎检测到后,作者便会更改混淆方法并发布新的变体,使其二进制内容与之前的版本有足够的差异,从而逃避检测。

为了检测变形威胁,杀毒软件研究人员将搜索属于同一恶意软件变种的样本中的共同特征。鉴于恶意软件语料库的规模,识别这些样本是一个难题。人工研究人员和自动化系统都可以从能够基于内容检索已有样本的搜索系统中受益。

本文旨在提供能够快速执行此类搜索的技术,同时能够每天用新样本更新现有集合。我们的方法基于倒排索引数据结构,该结构将每个固定长度的字节序列与包含该序列的文件列表相关联。通过采用这种数据结构,我们避免了在集合中每个文件里搜索给定字节序列的朴素算法。然而,这种数据结构更难维护,我们的实验表明,通用的数据库管理系统无法达到期望的性能。

下一节列出类似的研究,并强调与我们方法的不同之处。第三节介绍了构建和维护索引数据结构的原理,而第四节论述了从二进制文件中提取的 n-grams 为何可被视为大数据。实验结果重点分析了我们算法的性能。论文以结论部分结束。

2 相关工作

一种类似的方法由 Wesley Jin 等人提出,他们提出了从二进制文件中使用1字节滑动[7]生成无偏移的唯一 n-grams 集合的思想。最初,他们尝试使用开源技术构建倒排索引,例如 PostgreSQL[6], MongoDB[12], Tokyo Cabinet[10] 或 Redis[11]。

曾两次尝试使用 PostgreSQL 创建倒排索引。第一种方法未能提供理想的插入性能,在这种情况下,3,465 个恶意软件文件在一周内无法处理完毕。在第二种方法中,他们重新设计了数据库,从而提升了插入性能,但磁盘使用量却不可接受。

然后,他们尝试使用 NoSQL 风格的开源数据库 Tokyo Cabinet 和 MongoDB 来创建倒排索引。这两种技术都提供了吸引人的特性,例如快速更新列表,但在处理 3,465 个文件的数据时,经过两天的索引处理进度仍不足 50%。

另一种现有技术 Redis,是一种内存键值存储,可用作数据库或缓存,能够提供更快的数据读取和更新速度,但其键空间的限制(最多 2^32 个键)证明不适合用于存储倒排索引。

因此,他们决定从头开始构建倒排索引。倒排索引的架构包含两个主要组件:n-grams 列表和倒排列表。n-grams 列表是一系列指向倒排列表的 64 位偏移量。某个 n-gram 的偏移量通过取其 n−1 个最高有效字节,将其右移一个字节,再将结果乘以 8 来定位。每个倒排列表条目包含以下字段:n-gram(4 字节)、压缩类型及压缩数据大小(4 字节)以及压缩后的文件标识符列表。随后,他们提出了在内存中为文件批次创建独立索引并将其刷新到磁盘的想法。索引构建算法包含三个逻辑部分:

  1. 使用 1-滑动从批次中的所有文件中提取唯一的 n-grams;
  2. 跨所有文件的公共 n-grams 使用败者树合并到单个统一列表中;
  3. 每个 n-gram 的文件标识符被压缩以形成倒排条目。

该算法的总体复杂度为 O(M · N + N · log₂(N)),其中 M 是批次中的文件数量,N 是平均文件大小。

在文章 [13] 中,n-grams 被用于可执行代码相似性,以检测恶意软件。本文提出了通过其代表性的 n-grams 将库代码标记为无效的想法,以提高恶意软件检测算法的精度。该方法可在当前上下文中应用于将过于频繁的 n-grams 标记为无效,从而有助于减小倒排列表的平均大小。

3 索引原理

3.1 朴素方法

在介绍我们的解决方案之前,我们将首先讨论朴素方法,并强调其缺点。给定一个大型文件集合,我们需要搜索一个特定的字节序列,并输出所有包含该序列的文件列表。朴素的解决方案是简单地将整个文件集合存储在单个服务器或分布式系统上,并依次执行每次搜索。

例如,如果我们有 1000 个文件,每个文件的平均大小为 100 KB,则我们的集合将占用约 100 MB。在整个集合中对给定内容进行顺序搜索可以使用像 memmem 函数[2]这样的简单方法,或使用更高级的算法如 Rabin-Karp[9] 或 Aho-Corasick[3]。无论哪种方式,搜索的运行时间都将取决于从磁盘读取数据所需的时间。在读取速度为 100MB/s 的磁盘上,整个搜索将耗时约一秒。

另一方面,如果我们把文件数量增加到 100 万(这对于恶意软件集合来说更为现实),搜索时间将线性增长至 1000 秒,这在实际情况下是不可接受的。

在本节的其余部分,我们将介绍一种基于 n-grams 和倒排索引的方法,该方法提供的搜索时间不会随着集合大小而线性增长。

3.2 二进制文件表示为 n-grams

倒排索引中使用的术语定义为 n-grams,即从文件数据中通过一个大小为 n 字节的滑动窗口获取的字节序列,该窗口每次移动一个字节的位置(图1)。这意味着将被索引的文件最小尺寸为 n 字节。从一个二进制文件中可提取的 n-grams 的最大数量为 s − n + 1,其中 s 表示文件的大小(单位:字节)。由于我们提出了一种无偏移索引方案,因此 n-grams 是唯一提取的。为了唯一地提取 n-grams,可以使用多种数据结构和算法:

示意图0

某些 n-grams 不提供有用信息。在大多数情况下,这些对应于具有特定格式的每个文件中存在的字节序列。例如,所有 PDF 文档都包含 4 字节的 0x25504446(对应字符串“%PDF”)。这类 n-grams 必须进行过滤[13],以防止出现巨大的倒排列表。此外,通过过滤这些 n-grams,倒排索引的总大小会减小。

一种用于过滤 n-grams 的朴素方法意味着在恶意软件集合中为 n-gram 的出现次数设定一个阈值。如果出现次数超过该阈值,则将该 n-gram 标记为无效。

另一种使 n-grams 失效的方法是采用逆文档频率统计量[8]。我们将文档定义为属于特定文件格式的 n-grams 集合。逆文档频率度量(idf)定义为:

$$
\text{idf}(t, D) = \log \frac{N}{1 + |{t \in D : t \in d}|}
$$

其中,D 是集合中所有 n-grams 的集合,N 是集合中不同文件格式的数量。$|{t \in D : t \in d}|$ 表示包含 t 这个 n-gram 的文件格式的数量。

为了存储无效的 n-grams,我们提出了两种数据结构:位图或有序数组。当无效 n-grams 的数量低于 1000 时,应使用有序数组;否则,位图是更优的选择。当使用有序数组时,对于总共 M 个无效 n-grams,检查某个 n-gram 是否有效的时间复杂度为 O(M)。另一方面,当使用位图时,相同操作的时间复杂度为 O(1)。

算法1. 用于 n-gram 的提取算法

输入 :二进制文件的路径 P 及其大小 s
输出 :来自文件的有效 n-grams 集合

  1. 内容 ← 读取文件(P)
  2. n-grams ← ∅
  3. InvalidNgrams ← 加载无效 n-grams()
  4. n-gram 位图 ← CreateNewBitmap(2³²)
  5. 对于 i ← 0 到 s−4 执行
  6. N ← 获取 n-gram(内容, i)
  7. if ¬ IsBitSet(NgramBitmap, N) ∧ ¬ IsBitSet(InvalidNgrams, N) then
  8. n-grams ← n-grams ∪ {N}
  9. 设置位(n-gram 位图, N)
  10. end
  11. 结束
  12. 返回 n-grams

算法1,用于从大小为 s 的文件中提取 n-grams,当 n = 4 时,其描述如下。过程 ReadFile(P) 读取由路径 P 标识的文件内容。然后,LoadInvalidNgrams 用于加载无效的 n-grams 位图。此外,过程 CreateNewBitmap(B) 创建一个具有 B 位的新位图。GetNgram(Buffer, Offset) 用于从缓冲区内指定偏移处获取一个 n-gram。接着,过程 IsBitSet(Bitmap, BitIndex) 和 SetBit(Bitmap, BitIndex) 分别用于检查位图中的某一位是否已设置以及设置某一位。该算法的时间复杂度为 O(s)。

3.3 倒排索引

在恶意软件集合中搜索二进制内容时,可以使用朴素方法。这意味着在每个文件的数据中搜索给定的字节序列。给定序列长度 m 和文件大小 s,我们在该文件内搜索该序列的时间复杂度为 O(m·s)。例如,在一个包含一百万文件、平均大小 1MB 的集合中搜索一个 16 字节的序列,磁盘平均读取速度为 100MB/s(即大约每秒 80 个文件的扫描能力),我们可以在 3 个半小时内完成对该序列的搜索。

本文提出的倒排索引与 Jin 等人[7]创建的倒排索引类似,同样包含一个指向倒排列表的引用表。我们建议使用变长引用,而不是[7]中的 8 字节固定长度。此外,我们的解决方案旨在针对长度为 4 字节 (n = 4) 的 n-gram 实现全文索引的最大性能。该值在倒排列表大小和引用表大小之间提供了最佳比率。在包含 100,000 个随机文件格式的文件集合中,我们发现了 99.93%(4.29·10⁹ 个唯一的 4 字节 n-gram)长度为 4 字节的 n-gram。在同一集合中,我们发现了 2.35·10⁻⁷%(43.6·10⁹ 个唯一的 8 字节 n-gram)长度为 8 字节的 n-gram。尽管

示意图1

当选择大小为 8 字节的 n-gram 时,倒排列表的平均大小是合理的,但引用表却难以管理。

标识符数量(4 字节) 标识符列表

如上所述,在引用表中,条目可以具有可变长度。当使用 n-gram 的大小为 4 字节时,最大引用计数为 2³²。倒排列表将采用图2所示的格式,其中每个文件标识符也占用 4 字节。这意味着倒排列表的总大小或倒排列表偏移量是 4 字节的倍数,从而可以使用 X/4 个位置来引用 X 字节的数据。假设有 Y 个位置,定义表示 Y−1(最后一个位置)所需的位数为 Z = ⌊log₂Y⌋。计算参考大小的公式为 RefSize = Z/8,如果 Z 是 8 的倍数,否则为 RefSize = Z/8 + 1。

为了将引用表存储到磁盘上,可以使用两种格式,如图3所示。第一种方法是将一个包含 2³² 个条目、每个条目为 RefSize 字节的表存储到磁盘,这会导致获取一个 n-gram 的倒排列表的时间复杂度为 O(1)。另一种方法则是按 n-gram 对有序的 对进行存储,针对集合中每个具有倒排列表关联的 n-gram。该方法提供 O(log₂ M) 的时间复杂度,其中 M 是集合中出现的唯一 n-grams 的数量。为了确定应使用的格式,我们将完整引用表的总大小 2³² · RefSize 与 对的总大小(由 (4 + RefCount)·M 给出)进行比较。

引用#0
引用#1
引用#2
reference #2³²−1

(a) 完整引用表

n-gram #0 引用#0
n-gram #1 引用#1
n-gram #2 引用#2
n-gram #M−1 参考 #M−1

(b) 成对引用表

倒排索引应由多个部分索引组成,这些部分索引可以使用可配置数量的文件来构建。这使得较旧的索引数据能够快速删除。倒排列表必须保持唯一性。在仅使用 321 个二进制文件进行的一次简短测试中,我们发现了 19,117,531 个倒排列表,其中 959,565 个(5.015%)是唯一的。为了保持倒排列表的唯一性,可以使用哈希表

可以使用。建议使用结果大小为 4 字节的哈希函数,例如 CRC-32[1]。在这种情况下,在哈希表中,对于倒排列表数据上的每个 CRC-32,可关联指向该倒排列表的参考,或关联具有相同 CRC-32 的其他倒排列表的参考列表。通过 O(1),实现设置或检索倒排列表参考的时间复杂度。

4 二进制 n-grams 作为大数据

使用现成的技术(如 Redis、Mongo DB 或 PostgreSQL[7])构建倒排索引需要耗费大量时间。为了提供合理的倒排索引构建性能,我们开发了一种定制的解决方案。在对二进制文件进行索引时,需要执行以下操作:

  • n-gram 的提取,在大多数情况下,速度合理;
  • 获取已提取的 n-gram 的现有倒排列表;
  • 更新倒排列表;
  • 更新引用表条目。

例如,给定一个 1 MB 的文件,在提取 n-grams 后,大约需要进行 300 万次操作。

常见的数据索引结构,如 B+ 树或红黑树,在最佳情况下提供 O(log₂ N) 的时间复杂度。为了存储这些数据结构,需要大量的主内存:每个键需要两个内存引用,在 64 位系统上占用 16 字节。

我们之所以在二进制 n-grams 的背景下讨论大数据,原因如下:

  • 在一个包含 M 个二进制文件的集合中,可能需要 2M − 1 个不同的倒排列表;
  • 每 MB 数据需要执行 300 万次操作来对该数据进行索引;
  • 通常,自动化恶意软件检测系统每天处理超过 50 万个样本,平均每个样本大小为 900KB。

鉴于索引单个文件所需的大量操作,我们将部分索引构建在主内存中,最后将其刷新到磁盘。所呈现的倒排索引[7]采用单线程方法构建。根据预计对单个二进制文件进行索引时存在大量的操作,我们的方法使用并行处理以提高性能。算法2用于将二进制文件的 n-grams 添加到倒排索引中,其过程很直接:对于提取出的每个 n-gram,将其文件标识符添加到对应的倒排列表中。为了从二进制文件中提取 n-grams,可以使用多个线程。每个线程应从队列中获取一个文件路径,并提取其 n-grams。当 n = 4 时,每个线程需要 512 兆字节的主内存,用于存储提取出的 n-grams 唯一性判断所用的位图。存储无效 n-grams 的位图应由所有线程共享。

尽管 n-gram 的提取过程已得到显著改进,但由于更新与 n-gram 相关联的倒排列表时需要对数据结构进行独占访问,该过程仍然难以优化。当 n = 4 时,我们使用了以下数据结构来构建倒排索引:

  • 一个包含 2³² 个条目的数组,每个条目为 4 字节。该数组的索引对应于 n-grams,条目则对应其倒排列表的引用标识符;
  • 一个引用表,用于存储对倒排列表的引用。随着新的倒排列表被创建,此表会不断增长。该表的索引是上述数组中的条目;
  • 一个包含 2³² 个条目、每个条目为 4 字节的数组,用于维护倒排列表数据上的哈希值(CRC-32)与其引用标识符之间的关联,或具有相同哈希值的一组引用标识符。该数组确保了倒排列表的唯一性;

算法2. 将文件标识符关联到 n-gram 的算法

输入 :一个 n-gram N 和一个文件标识符 F

  1. 现有倒排列表 ← 获取倒排列表(N)
  2. 如果 ExistingPostingList = ∅ 那么
  3. 新倒排列表 ← {F}
  4. 否则
  5. 新倒排列表 ← {F} ∪ 现有倒排列表
  6. 结束
  7. 哈希值 ← 哈希函数(新倒排列表)
  8. 倒排列表 ID ← 获取倒排列表 ID(哈希值)
  9. 如果 PostingListId = NULL 那么
  10. 倒排列表 ID ← 添加新倒排列表(哈希值, 新倒排列表)
  11. 结束
  12. 设置倒排列表 ID(N, 倒排列表 ID)

在算法2中,过程 GetPostingList(Ngram) 用于检索一个 n-gram 的倒排列表。然后,HashFunction(Data) 表示应用于数据缓冲区的通用哈希函数。此外,GetPostingListId(HashValue) 用于获取哈希值的引用标识符。最后,过程 AddNewPostingList(HashValue, NewPostingList) 和 SetPostingListId(Ngram, PostingListId) 分别用于将哈希值与新倒排列表相关联,以及将倒排列表引用标识符与一个 n-gram 相关联。算法2的时间复杂度为 O(K),其中 K 是倒排列表的最大长度。因此,对大小为 s 的文件进行数据索引的时间复杂度为 O(s·K)。最后但同样重要的是,当倒排列表被写入磁盘时,主存位置的引用将被替换为磁盘文件中的偏移量。

5 实验结果

为了评估构建倒排索引的性能,选取了若干组随机格式的二进制文件,其大小从 1,000 到 30,000 不等。我们使用朴素方法进行 n-gram 无效化,以实现索引构建算法的最大性能。同时,限制每个二进制文件最多索引 32MB 字节。图4展示了索引构建的性能。尽管该算法的复杂度在此情况下为 O(N·K),但图4中的蓝色曲线可近似为二次函数。这是由于使用了上述变长引用表所致。当没有空余条目用于存储倒排列表引用时,系统会重新分配该表,并将其大小增加 X 个空槽。为解决性能问题,提出的解决方案是增大 X。因此,图4中的红色曲线显示出倒排索引构建的线性特征。将部分索引导出至磁盘的性能始终低于 312 秒,无论索引了多少文件。图5a 表明,这主要取决于当时的磁盘负载。此外,还可以观察到在索引文件数量介于 8,000 到 9,000 个之间时,索引导出性能出现下降。这一点正是完整引用表开始替代成对引用表使用的时刻。

性能与 n-gram 的获取以及倒排列表的大小相关。如果倒排列表大小被限制为 K,那么对于包含 n-gram 且具有 N 个元素的文件标识符列表的获取复杂度为 O(K · N)。否则,如果倒排列表大小不受限制,则相同操作的复杂度为 O(M · N)。在当前设计中,由于使用了朴素方法进行 n-gram 的失效处理,倒排列表的大小受到了限制。因此,图5b 中所示的曲线可近似为一个线性函数。在恶意软件集合中查找包含 49,900 字节序列的文件时,所需时间为 5.302 秒,这显著优于朴素方法的搜索性能。

图6 显示,对于小于 10,000 个文件的小批量,索引大小是批次大小的两倍。当总批处理大小增加到 30,000 个文件时,索引大小往往小于批处理大小。这就是为什么必须一次性索引大量二进制文件的原因。

我们在一个包含 978,668 个二进制文件的集合上进行了一次测试。使用单台机器并占用 65GB 主内存,对该集合的数据进行索引处理耗时 57 小时。我们构建的每个部分倒排索引最多包含 25,000 个文件的数据。该文件集合生成的部分索引数量为 50 个。对单个文件的数据进行索引的平均时间为 0.211 秒。该集合在磁盘上的总大小为 817GB,所有部分索引在磁盘上的大小为 1116GB,即输出/输入比率为 1.36。该结果表明我们的解决方案具备每天处理 412,000 个二进制文件的索引能力。使用单台机器即可。如果需要在一天内索引更多的二进制文件,可以使用更多机器,且不会产生性能损失。

6 结论和未来工作

6.1 未来工作

应开发一种索引压缩方法,以减少磁盘上的索引大小。此外,还可以提出新的 n-gram 失效机制。通过使用独立的表来维护 n-gram 与倒排列表的关联关系,可以进一步提高 n-gram 插入的并行性。这意味着在从二进制文件中提取 n-grams 时必须对其进行排序。在搜索包含特定 n-gram 序列的文件时,也应使用并行化技术。

6.2 结论

本文提出了一种二进制内容索引系统,可用于恶意软件集合中的全文搜索。该系统利用内存构建部分倒排索引,以尽可能优化插入二进制文件数据的过程。在每批次 1,000 至 30,000 个二进制文件的测试中,插入单个二进制文件的 n-grams 的平均时间为 0.202 秒。此外,在大多数情况下,所提出的解决方案能够应对恶意软件使用的混淆技术,其方式是索引它们的内存转储。对于获取与某个 n-gram 相关联的文件标识符倒排列表,实现了常数搜索时间。如果系统用于搜索具有特定 n-gram 序列的文件,则获取结果所需的时间与序列长度成线性关系。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值