YARA是一个流行的开源项目,广泛应用于恶意软件扫描、数据泄露检测等领域。YARA 的强大之处在于其规则灵活性和易用性以及支持自定义模块的集成。
本文通过介绍如何优化Yara检测规则来提升Yara的扫描效率,优化策略适用于Yara 3.7版本之上。
如果想要了解更多关于Yara的知识,可以参考以下链接文章:
- YARA:第一章-启动参数
- YARA:第二章-字符串之十六进制字符串(一)
- YARA:第三章-字符串之文本字符串(二)
- YARA:第四章-字符串之正则表达式(三)
- YARA:第五章-条件块
- YARA:第六章-更多关于规则
- YARA:第七章-模块使用之PE
- YARA:第八章-模块使用之ELF
- YARA:第九章-模块使用之Magic
- YARA:第十章-模块使用之Hash
- YARA:第十一章-模块使用之Math
- YARA:第十二章-模块使用之Time、Console和String
- YARA:第十三章-编写定制化模块
- YARA:第十四章-基于JSON文件的威胁分析
- YARA:第十五章-libyara使用(威胁检测)
- YARA:第十六章-libyara之C API手册(威胁检测)
文章目录
1. 了解Yara的扫描流程
如果想要研究如何优化Yara的性能,那么了解Yara的扫描过程是十分关键的。Yara的扫描过程主要分为四个步骤,分别是Compiling the rules(编译规则)、Aho-Corasick automaton(AC自动机)、Bytecode engine(字节码引擎)和Conditions(条件块)。
下面是一个Yara规则的示例:
import "math"
rule example_php_webshell_rule
{
// "meta:" 包含了规则的元数据,即描述信息,这些内容不参与规则匹配,
// 但是这些数据可以包含当前规则的作用、作者、日期以及参考链接等。
meta:
description = "Just an example php webshell rule"
date = "2021/02/16"
// "strings:" 包含了规则字符串内容,Yara支持正则表达式、字符串文本和
// 十六进制字符作为规则内容。
strings:
$php_tag = "<?php"
$input1 = "GET"
$input2 = "POST"
$payload = /assert[\t ]{0,100}\(/
// "condition:" 条件块,Yara支持丰富的关键字用于描述当前规则生效的条件。
condition:
filesize < 20KB and // 要求所扫描文件大小大于20KB
$php_tag and // 要求当前扫描内容中包含字符串"<?php"
$payload and // 要求当前扫描内容中包含正则表达式 "assert[\t ]{0,100}\("匹配的字符串
any of ( $input* ) and // 要求当前扫描内容中包含字符串"GET"和"POST"
math.entropy(500, filesize-500) >= 5 //要求当前文件中指定内容块的信息熵值大于等于5
}
1.1 编译规则
编译规则发生在执行扫描之前。Yara从规则字符串中提取原子串(atoms)输入到AC自动机模型中。下面有对原子串介绍的章节。原子串最大为四个字节长度,Yara从原有的规则字符串中提取原子串作为AC自动机匹配串,提取原子串时从长度、复杂度等方面进行了筛选,选择最优原子串。参照上面Yara规则示例,下面展示了每个规则串提取原子串的结果:
原始规则串 原子串
"<?php" -> "<?ph"
"GET" -> "GET"
"POST" -> "POST"
/assert[\t ]{0,100}\(/ -> "sser"
由于原子串最长为四个字节,因此规则字符串长度超过四个字节,则提取其中最复杂的四个字节作为原子串。如果小于四个字节,则直接将整个规则串作为原子串,输入到AC自动机模型中。
1.2 AC自动机匹配
AC自动机(Aho-Corasick自动机)是一种用于字符串搜索的高效算法。其主要目的是在一个文本内容中快速查找多个模式字符串。
在此阶段,Yara开始扫描文件内容。在前面编译规则阶段Yara将原子串全部输入到AC自动机模型中。在此阶段,Yara使用AC自动机模型快速查找当前文件内容中是否有与原子串一样的字符串。如果有,则需要将所匹配字符串信息提交给字节码引擎。
1.3 字节码引擎匹配
字节码引擎主要负责当AC自动机匹配阶段匹配中原子串时,继续匹配原子串的前缀和后缀。例如,当前扫描的文件内容中有字符串"sser",命中原子串,字节码引擎则检查文件中此字符串前缀是否包含字符"a",以及后缀中是否包含字符"t"。如果前后缀字符都匹配中,接着继续匹配字符串后面内容是否符合正则表达式"[\t ]{0,100}\("。通过这种方法,Yara避免了将整个正则表达式作为匹配项直接匹配文件内容,因为这样的效率很低。
1.4 条件匹配
执行AC自动机匹配和字节码引擎匹配后,将执行条件匹配来做最后判断是否有规则命中。
2. 优质的原子串
Yara从规则字符串中提取最大长度为四字节的子串作为atoms(本章提到的原子串即为atoms)。这些原子串可以是规则字符串中的任意部分,且Yara在扫描文件内容时匹配的是这些原子串。只有在原子串匹配后,才会使用字节码引擎继续匹配规则串中除原子串以外的字符是否一致。
例如,下面是一个规则字符串:
$rex = /abc.*cde/
上面这个规则字符串中的原子串可以是"abc",也可以是"cde"。这两者都可以作为原子串,因为他们的质量是相同的。Yara会将"abc"作为原子串,因为它是两个相同质量的原子串中的第一个。
例如,下面是一个规则字符串:
$rex = /(one|two)three/
上面这个规则字符串中的原子串可以是"one"、"two"、"thre"和"hree",Yara可以单独将"thre"或者"hree"作为原子串输入到AC自动机中,也可以将"one"和"two"同时输入到AC自动机中。但是原子串"thre"更加适合作为原子串输入到AC状态机,相比较"one"、"two"来说它的原子串"thre"长度较长,在文件中匹配命中的几率较低。"thre"相较于"hree"来说,后者中包含重复的字符"e",原子串中重复的字符越少越好。
Yara会尽最大的努力从规则串中提取出最优的原子串,例如下面这个规则串:
$hex_str = { 00 00 00 00 [1-4] 01 02 03 04 }
上面这个规则字符串中的原子串可以是"00 00 00 00",也可以是"01 02 03 04"。Yara会优先选择"01 02 03 04"作为原子串,因为"00 00 00 00"太常见了,很容易在文件中匹配,匹配命中次数过多也会降低检测效率。
下面是一个规则字符串:
$hex_str = { 01 02 [1-4] 01 02 03 04 }
上面这个规则字符串中原子串可以是"01 02",也可以是"01 02 03 04"。Yara会优先选择"01 02 03 04",因为它比"01 02"更长。
所以,提升规则串检测效率的方法之一就是包含高质量的原子串。下面示例列举了一些低效的字符串,因为从这些规则串中提取的原子串太短或者包含太多重复字符导致很容易在文件中匹配中。
$hex_str1 = {00 00 00 00 [1-2] FF FF [1-2] 00 00 00 00} // 原子串中包含重复字符
$hex_str2 = {AB [1-2] 03 21 [1-2] 01 02}
// 原子串过短
$rex_str1 = /a.*b/
// 原子串过短
$rex_str2 = /a(c|d)/ // 原子串过短
最糟糕的规则字符串是那些根本不包含任何原子串的字符串,如下所示:
$rex_str1 = /\w.*\d/ // 无法提取原子串
$rex_str2 = /[0-9]+\n/ // 无法提取原子串
3. 不要太多循环的条件
Yara规则的条件块中如果包含循环语句且需要循环很多次会降低Yara的扫描效率,尤其每次循环需要执行复杂且耗时的计算。例如:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
上面这个规则有两个问题,问题一是规则字符串"00 00"太过于普通,在扫描文件内容时很容易命中。问题二是因为规则字符串"00 00"太过于常见,会导致"#a"("#a"表示规则字符串"00 00"在当前文件中匹配中总数)的数值过高,从而是的循环执行上千次。
下面这条规则也是低效的,因为其循环次数取决于文件的大小,如果所扫描文件很大,会导致循环次数也很高:
strings:
$a = "example"
condition:
for all i in (1..filesize) : ($a at i)
4. 合理使用Magic模块
不要在Windows环境中使用Magic模块,该模块只支持Linux系统。虽然使用该模块能够比较精确的识别文件类型,但是会影响扫描速度,建议通过特征值匹配文件头的形式替代使用Magic模块。
下面是参考GIF文件格式头定义编写的规则,用于识别GIF格式文件:
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
下面是使用Magic模块识别文件类型:
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
5. 避免规律型字符串
在编写规则时尽量避免太短的规则字符串,因为规则字符串少于四个字节很容易在文件中匹配中。除此之外,规则字符串足够长,但是由于字符串内容过于规律性,也会导致匹配效率降低。下面符合规律型的字符串内容:
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // wide formatted spaces
虽然上面规则字符串长度够长,也可以提取原子串,但是很明显具有规律型,因此对文件内容进行原子串匹配时会匹配很多次,例如第一个规则字符串对应的原子串为"2222",那么依据Yara当前匹配机制,当文件内容中出现该规则字符串内容时,原子串会匹配中60次,匹配中60次即执行了60次AC自动机和字节码引擎,这明显是低效的。
6. 字符串建议
编写规则串时尽量少用修饰符,即降低规则串可变形数量。尽可能少用"nocase"修饰符,因为此修饰符导致规则字符串提取很多原子串。
下面示例中规则串中只需要提取少量原子串输入到AC自动机模型中:
$s1 = "cmd.exe"
$s2 = "cmd.exe" ascii // 此规则和$s1一样,ascii是默认修饰符,可省略
$s3 = "cmd.exe" wide // UTF-16格式,原子串也是此格式
$s4 = "cmd.exe" ascii wide // 生成两种原子串,分别是ascii和UTF-16格式
$s5 = { 63 6d 64 2e 65 78 65 }
下面示例中规则串会提取多个原子串输入到AC自动机中:
$s5 = "cmd.exe" nocase // 有nocase修饰符为大小写不敏感,因此原子串可以是"Cmd."、"cMd."、 "cmD." ..
因此尽量确认所扫描的文件内容是大小写不敏感的场景下,在规则串中使用"nocase"关键字修饰。如果说只是规则串中一两个字符串区分大小写,尽量使用正则表达式来代替"nocase"关键字,示例如下:
$re = /[Pp]assword/
需要注意的时,如果使用正则表达式替代"nocase"关键字导致原子串长度过短时,也会降低扫描效率。示例如下:
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
如果说规则串变形数量较少时,建议将变形后的字符串全部写在规则中,上面示例规则优化后为下面这段规则:
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
7. 正则表达式建议
尽量避免使用正则表达式,因为正则表达式比纯字符串匹配要慢得多,并且还会消耗大量的内存。此外,在编写十六进制规则串时尽量不要使用跳转符(即跳过指定数量字符)和通配符(即匹配任意值)。
如果不得不在规则串中写入正则表达式,尽量避免使用贪婪的语句例如".*"。尽量使用精确数量范围匹配语句例如".{1,30}"、".{1,3000}",不要忘记上限。
当正则表达式中使用数量条件时,两种情况将会发生:
如果说正则表达式的锚定串在前面,后面字符串是可变的,Yara会尽可能长的匹配此表达式。例如".*"、".+"或者".{2,}",这些会导致匹配中很长的字符串并且扫描效率降低。
如果说正则表达式的锚定在后面,前面字符串是可变的,Yara会匹配所有的可能性。示例如下:
$re1 = /Tom.{0,2}/ // 锚定串在前面,尽可能长的匹配,因此此规则匹配"Tomxx"
$re2 = /.{0,2}Tom/ // 锚定串在后面,匹配所有可能性,即"Tom"、"xTom"、"xxTom"都会匹配。
太短的匹配项可能因为很容易匹配中导致Yara产生"too many matches"告警。下面是一个正则表达式规则用于匹配邮箱地址。当使用"[-a-z0-9._%+]"加量词时,Yara将会在一个邮箱地址上多次命中此规则,这是低效的。例如:
/[-a-z0-9._%+]*@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
/[-a-z0-9._%+]+@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
/[-a-z0-9._%+]{x,y}@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
上面规则中由于量词为"*"、"+"和"{x,y}"可能导致一个邮箱地址前缀反复匹配中,为了避免这种情况我们可以提取一个有效的子集作为规则:
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/ // 不加量词可以避免上面重复匹配的情况。
OR
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/ // 以字符"@"作为开头也可以避免重复匹配,因为锚定串在前面。
如果你能确定字符串的规律,例如"exec"命令后面跟"/bin/sh",你可以使用关键字"@"来表示。如下所示:
$rex_str = /exec.*\/bin\/sh/
而下面这种写法可以提升匹配效率:
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
如果说正则表达式中包含一段字符串序列可以作为锚定点,那么这个字符换序列越长越好,例如下面这段正则表达式检测效率并不高,因为其中使用到"[.]*":
$s1 = /http:\/\/[.]*\.hta/
我们可以限定"[]"中可匹配字符类型以及"{}"中限定数量范围:
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/
最好的是添加更长的锚定字符串序列:
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
8. 规则串命中太多导致扫描效率低
当Yara在扫描过程中因为某个规则串在当前文件中命中太多并且超过设定的阈值(在Yara源码中limits.h中有对阈值进行设置)而导致报"Too many matches"告警信息。导致这种现象往往是这些字符串过于普遍或者规则有问题导致在某段字符串中命中太多次。例如原子串"aa"会在字符串"aaaaaaaaaaaaaaaa"中命中很多次。
遇到这种情况,可以通过以下方式解决:
(1)检查正则表达式中是否包含量词".*"、".+"、".*?"。
(2)检查量词是否设置了上限,例如"x{14,}"是尽量避免的。
(3)检查量词范围是否过大,例如"x{1,3000000}"。
(4)检查在十六进制字符串中是否包含较大的跳转,例如"00 01 [0-3000] 02 03"。
(5)检查是否包含通配符,能够更加细致的指定通配符标识的特征来降低通配符范围,或者能否将规则分割成两个来省略的通配符的使用。
(6)检查是否包含可选字符,此规则是否可以分解成两个或者多个规则,例如"(a|b)cd"。
(7)尝试添加单词匹配的规范,例如使用"\b"。
9. 条件块的短路机制
在编写条件语句时,把其中最有可能是false的条件放在靠前位置。条件块是从左到右进行判断,检测引擎越早识别出不满足的条件,就可以越早的跳过当前规则并进入下一个规则匹配。当然,条件语句的排序也应该考虑每个条件执行需要占用CPU的时间。如果有的条件占用CPU较长,有的较少。建议将占用较少的放在靠前的位置。如果所有的条件占用CPU时间相差不大,那么调换顺序的对于提升匹配效率效果并不大。
例如下面这个条件语句更改条件先后顺序并不能显著提升检测效率:
$string1 and $string2 and uint16(0) == 0x5A4D
下面这个条件语句中包含占用CPU时间较长的条件"math.entropy(0, filesize) > 7.0"。如果按照下面的顺序编写条件会导致降低扫描效率:
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
优化后的条件语句如下所示:
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
短路机制在一定条件下可以优化一些占用CPU较长的条件语句,例如下面示例:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
如果文件非常大,那么"whatever"将会执行很多次,从使得扫描效率大大降低。现在使用短路机制,只有文件大小小于指定大小时才会执行for语句:
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
10. 短路机制对正则表达式无效
短路机制并对于正则表达式规则无效,因为在规则编译阶段就已经将正则表达式输入到匹配引擎中了。下面这个例子中正则表达式规则串对于Yara扫描效率的降低不会因为文件大小的不同而改变:
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
11. 元数据
Yara规则中元数据会被读取到内存中,虽然不参与规则匹配,但是用于输出规则信息。因为元数据需要读入内存中,因此占用一定空间,如果说Yara运行的设备上内存空间并不是很足的情况下,可以删除掉一些非必要的元数据信息(元数据存储在内存的哈希表中)。
如果您觉得这篇文章对您有所帮助,或者在阅读过程中有所启发,我将非常感激您的支持。您的打赏不仅是对我努力的认可,也是对知识分享精神的一种鼓励。如果您愿意,可以通过以下方式给予支持: