Stringless YARA Rules

在 InQuest,YARA 是我们用于执行深度文件检查的众多工具之一,具有相当广泛的规则集。InQuest 在流量非常大的网络中以线速运行,因此这些规则需要快速。
这篇博文是讨论 YARA 性能说明、技巧和技巧的系列文章中的第一篇。
YARA自称是“Pattern Matching Swiss Knife”。(但显然“Swiss Army Knife”是 Victorinox AG 的商标。谁知道?)
它用于确定给定的输入(通常是文件,但它也可以附加到正在运行的进程并分析其内存)是否匹配任何定义的规则。YARA 规则由三部分组成,其中两部分是可选的:

  • 一些规则元数据,它只是字符串到值的映射。这些名称-值对对规则本身没有影响,但对于在规则匹配时传达附加信息很有用。此部分是可选的。
  • 一些“字符串”。我把它放在引号中是因为它们不限于静态字符串;也允许使用正则表达式。这部分同样是可选的。
  • 一个条件。这正是任意复杂度的布尔表达式。如果给定文件的评估结果为真,则触发规则。此部分不是可选的。

规则中还可能发生其他一些事情,例如标签,但它们超出了本博文的范围。
条件是所有魔法都捆绑在一起的地方。它可以检查规则中定义的任何字符串/正则表达式的匹配,检查一些提供的外部变量的值,调用外部函数,甚至运行循环。

示例规则

举一个简单的例子,让我们编写一个规则来检测我们正在查看的文件是否是 Adobe Flash 文件。
Adobe Flash 文件以三个魔术字符串之一开头:“FWS”、“CWS”和“ZWS”。三个魔术字符串根据它们所包含的数据所使用的压缩机制来区分 Flash 文件。

第一次尝试

这是在 Yara 中写这个的“明显”方式:

  1. rule Flash
  2. {
  3. strings:
  4. $flash_magic = /[FCZ]WS/
  5. condition:
  6. $flash_magic at 0
  7. }

这定义了一个正则表达式(名为$flash_magic),区分大小写,它将匹配我们上面定义的三个字符串。然后我们说如果文件中偏移量 0(即文件的开头)有匹配项$flash_magic,则规则Flash将匹配。
让我们看看这有多快。我将在 InQuest 测试语料库中的 Flash 文件上运行它:

  1. yara -f test1.rule testfile : 0.006s

0.006s真的很不错吧?但是让我们尝试在更大(1GB)、更恶意的文件上运行它:

  1. yara -f test1.rule maliciousfile : 0.838s
  2. error scanning maliciousfile: string "$flash_magic" in rule "Flash" caused too many matches

嗯。那不好。我们根本无法对文件运行规则。

太多的比赛

这里发生的事情是,YARA 首先在文件中查找正则表达式的所有匹配项,然后检查规则条件以查看它们是否为真。恶意文件有大量字符串,最终包含字符“CWS”、“FWS”和/或“ZWS”。
显然,我们不想分析文件失败,所以我们需要尝试一些替代方案。

不要匹配太多

让我们尝试修改正则表达式,使其不匹配太多。由于魔术字节总是在文件的开头,让我们锚定正则表达式:

  1. rule Flash
  2. {
  3. strings:
  4. $flash_magic = /^[FCZ]WS/
  5. condition:
  6. $flash_magic
  7. }

这运行得如何?

  1. time yara -f test2.rule maliciousfile : 29.821s

成功!YARA 完成了对文件的分析。这是有道理的,因为正则表达式现在没有太多匹配项:它只能匹配文件的开头。
然而,运行大约需要三十秒。我们能更快地得到东西吗?

静态字符串会更好吗?

让我们尝试将正则表达式更改为三个字符串匹配并使用 YARA 的条件将它们绑定在一起:

  1. rule Flash
  2. {
  3. strings:
  4. $flash_magic1 = "FWS"
  5. $flash_magic3 = "CWS"
  6. $flash_magic2 = "ZWS"
  7. condition:
  8. $flash_magic1 at 0 or $flash_magic2 at 0 or $flash_magic3 at 0
  9. }

现在让我们尝试在恶意文件上运行此规则:

  1. yara -f test3.rule maliciousfile : 23.726s

非常好。我们加快了大约六秒钟。
现在的问题是,我们能走得更快吗?事实证明,我们可以……但它为什么会起作用需要一些解释。

字符串和原子

YARA 非常努力地使字符串和正则表达式匹配得非常快。它是在一个文件将有数百甚至数千条规则运行的假设下运行的,并且这些规则中的每一个通常都将包含一个(如果不是很多)字符串。
为了加快这个过程,YARA 尽量避免在整个文件上运行正则表达式。相反,在规则编译时,它会查看所有已定义的字符串和正则表达式,并从中提取出一组atom
原子是一个短字符串。例如,对于表达式

  1. $flash_magic = /[FCZ]WS/

YARA 可能会提取原子“FWS”、“CWS”和“ZWS”。然后将给定规则集的所有原子集输入Aho-Corasick 算法,这是一种快速字符串查找算法。
(“Aho-Corasick”中的“Aho”指的是 Alfred Aho,他也是 AWK 编程语言中的“A”。)
运行 Aho-Corasick 算法并记录每个原子的偏移量。多亏了这一点,各种正则表达式不需要在整个文件上运行:原子不可能与表达式对齐的任何位置都被消除了。
这可以带来巨大的加速,但它的主要缺点是需要大量预处理才能找到文件中的所有原子。对于具有大量原子匹配的大文件(指示大量潜在的正则表达式或字符串匹配),此预处理时间可能会很大。

消除字符串

为了加快速度,我们可以尝试消除字符串/正则表达式。^[FCZ]WS问题是,当找不到字符串时,如何匹配正则表达式?
YARA 有一组内置的“整数函数”,可以从文件中的给定偏移量读取各种大小和排序的整数。例如:

  1. uint16be(0x72) == 0x3829

将从文件中的偏移量读取一个 16 位大端整数0x72并查看它是否等于0x3829. 对于大端和小端格式的单字节和 16 位和 32 位整数存在类似的函数。
鉴于此,我们可以将正则表达式转换为这些调用的序列。为了不让你们都悬而未决,这就是它的样子:

  1. rule Flash
  2. {
  3. condition:
  4. /* 'CWS' = '43 57 53' */
  5. (uint16be(0x0) == 0x4357 and uint8(0x2) == 0x53)
  6. or
  7. /* 'FWS' = '46 57 53' */
  8. (uint16be(0x0) == 0x4657 and uint8(0x2) == 0x53)
  9. or
  10. /* 'ZWS' = '5a 57 53' */
  11. (uint16be(0x0) == 0x5a57 and uint8(0x2) == 0x53)
  12. }

请注意,此规则根本没有字符串。让我们尝试运行它:

  1. yara -f test4.rule maliciousfile : 15.665

大约快 25%!一点也不差。
请注意我们如何使用上面的 16 位和 8 位调用的组合来处理我们的字符串是三个字节长的事实。我们经常使用这种技巧来匹配任意长度的字符串。

我们能走得更快吗?

我们已经从根本无法处理文件到 29 秒、23 秒、15 秒。
在我们看是否可以更进一步之前,看看实际的下限是多少可能会很有用。为此,我们可以运行对文件不执行任何操作的规则:

  1. rule NullRule
  2. {
  3. condition:
  4. false
  5. }

让我们对文件运行它,看看它需要多长时间:

  1. yara -f test5.rule maliciousfile : 14.476s

运行对我们的文件不执行任何操作的规则比我们最快的规则花费的时间不到 1.2 秒。这 14 秒是启动 YARA、解析和编译规则、加载目标文件以及进行任何必要的预处理所需的时间。
我们可能可以通过在我们的条件下重新排序子句来减少几毫秒,但我认为这不会给我们带来太多好处。我认为我们真的以最快的速度获得了这条规则。

结论

YARA 已经相当快了,尤其是考虑到它的能力有多么广泛。但是,您如何编写规则可能会对它们的运行速度产生真正的影响,有时以不太明显的方式做事会导致一些真正的加速。