简单介绍和实践
介绍
官方文档:https://yara.readthedocs.io/
YARA 是一个旨在(但不限于)帮助恶意软件研究人员识别和分类恶意软件样本的开源工具。Yara 可以同时运行在 Windows、Linux 和 macOS X 多种操作系统的命令行工具,并且 Python 提供的 python-yara 第三方库能够调用 Yara 检测恶意程序。
规则说明
每个规则由一组字符串和一个确定其逻辑的布尔表达式组成。通常YARA规则包含三部分:
元部分:这部分包含未处理但帮助用户了解内容的一般或特定的信息。
字符串部分:这部分包含需要在文件中搜索的所有字符串。
条件部分:这部分定义匹配的条件。
规则标识符
规则标识符就是规则的命名,有以下要求:
1、是由英文字母或数字组成的字符串
2、可以使用下划线字符
3、第一个字符不能是数字
4、对大小写敏感
5、不能超出 128 个字符串长度
注释:
// 单行注释
/*
多行注释
*/
元部分
元部分可用标记如下:
meta:用于添加元数据,例如作者、日期和描述等。
meta:
author = "Your Name"
date = "2023-06-12"
description = "Detects malware with specific string"
import:用于从其他Yara规则文件中导入公共函数和变量,以便在当前规则中使用。
import "common_functions.yar"
import <pe>
// 如果在 YARA 规则中出现了重复的函数名或变量名,会导致规则集编译失败并报错。
// 可以通过修改名称或建立命名空间来解决问题。
namespcae:namespace 是一个用于定义命名空间的关键字。命名空间是一种机制,用于将变量和函数进行逻辑上的分组和隔离,以避免命名冲突。
namespace <namespace_name> {
// 命名空间中的规则、变量和函数
}
举例:
// file1.yar规则如下:
namespace file1_ns {
rule my_rule {
strings:
$s = "hello"
condition:
$s
}
}
// file2.yar规则如下:
namespace file2_ns {
rule my_rule {
strings:
$s = "world"
condition:
$s
}
}
// 另一个文件中引用这两个规则文件时,需要使用命名空间来指定具体的规则:
import "file1.yar"
import "file2.yar"
rule combined_rule {
condition:
file1_ns::my_rule or
file2_ns::my_rule
}
include:用于在当前规则中包含其他Yara规则文件,以便扩展或重用现有规则。
include "common_rules.yar"
include <peid>
// include 用于将一个或多个规则文件合并为一个文件。在编译时,
// YARA 编译器会首先处理其中的所有规则,并将它们合并到一个规则集中,
// 以便进行更高效的匹配。import 用于从其他模块中引入特定的函数、变量
// 或规则,并将其与当前模块中的内容合并。在编译时,YARA 编译器会对每
// 个 import 进行独立处理,确保导入的代码被正确处理。
global:用于定义全局变量,可在当前规则文件及其导入的规则文件中使用。
global $max_filesize = 10MB
global $malware_family = "APT32"
private:用于定义仅在当前规则文件中使用的变量。
private $string_count = 0
rule:用于定义单个规则,并指定与该规则相关联的条件。
rule detect_malware {
strings:
$string1 = "malware_string" fullword ascii
$string2 = { 5B 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5D } // 匹配 [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
condition:
$string1 or ($string2 and size > 10KB)
}
include "binary" as binary:用于将二进制文件包含到规则中以执行某些操作。
// 用法:
include "payload.bin" as binary
// 举例:
include "file.bin" as binary
rule search_sequence {
condition:
$binary at 0x100 contains { 01 02 03 }
}
// 该规则使用 include "file.bin" as binary 将 "file.bin" 文件引入到规则中,
// 并赋值给变量 $binary。然后,在 condition 中,我们使用 $binary 来检查
// 二进制文件中偏移为 0x100 的位置是否包含字节序列 { 01 02 03 }。
function:用于定义自定义函数,可以在Yara规则中重复使用。
// 用法:
function get_file_md5(filename) {
md5 = hash.md5(filename)
return md5
}
// 举例:
// 计算MD5哈希值
rule example_rule {
strings:
$str = "hello world"
condition:
md5($str) == "5eb63bbbe01eeed093cb22bb8f5acdc3"
}
function md5(str)
{
var md5 = new ActiveXObject("System.Security.Cryptography.MD5CryptoServiceProvider")
var utf8 = new ActiveXObject("System.Text.UTF8Encoding")
var data = utf8.GetBytes_4(str)
var hash = md5.ComputeHash_2((data))
var hex = ""
for (var i = 0; i < hash.Length; i++) {
hex += hash(i).ToString("x2")
}
return hex
}
// 该函数定义了一个名为 md5 的函数,它接受一个字符串参数,
// 并返回该字符串的MD5哈希值。在规则的条件部分中调用了这个函数,
// 判断哈希值是否等于特定值。
// 检查文件路径
rule example_rule {
condition:
is_in_folder("C:\\Windows\\System32", file_path)
}
function is_in_folder(folder_path, file_path)
{
var fso = new ActiveXObject("Scripting.FileSystemObject")
var folder = fso.GetFolder(folder_path)
var files = new Enumerator(folder.Files)
while (!files.atEnd()) {
var file = files.item()
if (file.Path.toLowerCase() == file_path.toLowerCase())
return true
files.moveNext()
}
return false
}
// 该函数定义了一个名为 is_in_folder 的函数,它接受两个参数:
// 一个文件夹路径和一个文件路径。该函数检查给定的文件路径是否
// 存在于指定的文件夹中,并返回布尔值。在规则的条件部分中调用了
// 这个函数,判断给定路径是否位于 C:\\Windows\\System32 文件夹中。
// 解密字符串
rule example_rule {
strings:
$str = "UGFzc3dvcmQxMjM="
condition:
decrypt_base64($str, "Password123") == "Password123"
}
function decrypt_base64(base64_str, key)
{
var aes = new ActiveXObject("System.Security.Cryptography.AesCryptoServiceProvider")
aes.Mode = 1
aes.Padding = 2
var utf8 = new ActiveXObject("System.Text.UTF8Encoding")
var bytes = System.Convert.FromBase64String(base64_str)
var iv = new Array(aes.IV.Length)
for (var i = 0; i < aes.IV.Length; i++)
iv[i] = bytes[i]
aes.IV = iv
var key_bytes = utf8.GetBytes_4(key)
var decryptor = aes.CreateDecryptor(key_bytes, iv)
var decrypted = decryptor.TransformFinalBlock(bytes, aes.IV.Length, bytes.Length - aes.IV.Length)
return utf8.GetString(decrypted)
}
// 该函数定义了一个名为 decrypt_base64 的函数,它接受两个参数:
// 一个base64编码的字符串和一个解密密钥。该函数使用AES算法进行解密,
// 并返回解密后的字符串。在规则的条件部分中调用了这个函数,判断
// 给定的base64编码字符串是否等于解密后的值。
字符串部分
字符串可以分成三种:文本字符串、十六进制字符串、正则表达式:
文本字符串
//转义符:
\" 双引号
\\ 反斜杠
\t 制表符
\n 换行符
\xdd 十六进制的任何字节
//修饰符:
nocase: 不区分大小写
icase: 同上,但仅在 ASCII 编码中使用。
wide: 匹配2字节的宽字符。 只是将 ASCII 和 \x00 组合起来组成宽字符,
不支持包含非英文的 UTF-16 字符串。
wide ascii: 匹配 ASCII 字符集和宽字符的字符串
ascii: 匹配1字节的ascii字符,字符串默认是ASCII编码
xor: 匹配异或后的字符串
base64: 指示字符串是 base64 编码的字符串,并在匹配时解码它。
fullword: 匹配完整单词,匹配那些前后没有附加其他字符的单词(全词匹配)
注意:匹配的全词前后可以附加特殊字符(比如小数点), 不能是普通字符
private: 定义私有字符串,可以匹配出来,但是不会在结果中输出
base64wide: 同上,但使用 Unicode 编码。
nospace: 表示字符串中不能包含空格。常用于匹配 URL 和文件路径等特定格式的字符串。
not: 表示逆向匹配,即匹配字符串不存在于目标中。
rule CaseInsensitiveTextExample
{
strings:
//不区分大小写
$text_string = "foobar" nocase
//匹配宽字符串
$wide_string = "Borland" wide
//同时匹配2种类型的字符串
$wide_and_ascii_string = "Borland" wide ascii
//匹配所有可能的异或后字符串
$xor_string = "This program cannot" xor
//匹配所有可能的异或后wide ascii字符串
$xor_string = "This program cannot" xor wide ascii
//限定异或范围
$xor_string = "This program cannot" xor(0x01-0xff)
/*
全词匹配
匹配:www.domain.com
匹配:www.my-domain.com
不匹配:www.mydomain.com
*/
$wide_string = "domain" fullword
//私有字符串可以正常匹配规则,但是永远不会在输出中显示
$text_string = "foobar" private
condition:
$text_string
}
十六进制字符串
//通配符:可以代替某些未知字节,与任何内容匹配
rule WildcardExample
{
strings:
//使用‘?’作为通配符
$hex_string = { 00 11 ?? 33 4? 55 }
condition:
$hex_string
}
//不定长通配符:可以匹配长度可变的字符串
rule JumpExample
{
strings:
//使用‘[]’作为跳转,与任何长度为 2 字节的内容匹配
$hex_string1 = { 00 11 [2] 44 55 }
$hex_string2 = { 00 11 [0-2] 44 55 } // 表示 0 到 2 个通配符
//该写法与string1作用完全相同
$hex_string3 = { 00 11 ?? ?? 44 55 }
$hex_string6 = { 00 11 ~?0 62 B4} // 从 4.3.0 开始,可以用非修饰了
$hex_string4 = { 00 11 [-] 62 B4} // 这个可以匹配无限长的字符串
$hex_string5 = { 00 11 [10-] 62 B4} // 标识 10 到任意长度
condition:
$hex_string1 and $hex_string2
}
//也可以使用类似于正则表达式的语法
rule AlternativesExample1
{
strings:
$hex_string = { 00 11 ( 22 | 33 44 ) 55 }
/*
可以匹配以下内容:
00 11 22 55
00 11 33 44 55
*/
condition:
$hex_string
}
//还可以将上面介绍的方法整合在一起用
rule AlternativesExample2
{
strings:
$hex_string = { 00 11 ( 33 44 | 55 | 66 ?? 88 ) 99 }
condition:
$hex_string
}
正则表达式
有关正则表达式,后面专门出一篇文章介绍
正则表达式部分写法:/正则表达式内容/
符号 | 含义 |
---|---|
\ | 匹配一个字符。\?\*等 |
^ | 匹配开头 |
$ | 匹配结尾 |
. | 匹配任意单个字符 |
() | 匹配括号里的内容 |
[] | 匹配【】里的任意内容 |
* | 匹配0或多次 |
+ | 至少匹配一次 |
? | 匹配0或1次 |
{n} | 匹配n次 |
{n,} | 至少匹配n次 |
{,m} | 最多匹配m次 |
{n,m} | 匹配n到m次 |
\t | tab |
\n | 换行 |
\r | 回车 |
\xNN | 某个字符 |
\w | 匹配一个单词(数字,字母,下划线) |
\W | 匹配非单词 |
\s | 匹配一个空白字符 |
\S | 匹配非空白字符 |
\d | 匹配数字 |
\D | 匹配非数字 |
\b | 单词边界 |
\B | 非单词边界 |
条件部分
常见的条件表达式如下:
all of them // 匹配规则中的所有字符串,表示 strings 中的所有变量, 你可以使用关键字 them
any of them // 匹配规则中的任意字符串
all of ($a*) // 匹配标识符以 $a 开头的所有字符串
any of ($a,$b,$c) // 匹配 a, b,c 中的任意一个字符串
1 of ($*) // 匹配规则中的任意一个字符串
2 of ($m1,$m2,$m3) // 匹配三个字符串中任意存在两个
2 of ($m) // 用通配符 标识任意字符,从字符串 m* 集中匹配任意两个
如没有专门引用字符串的事件, 则可以仅使用 $ 来将它们全部引用,举例如下
rule AnonymousStrings
{
strings:
$ = "dummy1"
$ = "dummy2"
condition:
1 of them
}
Yara规则允许通过and, or, 和not等相关运算符来表示布尔表达式, 算术运算符(+,-,*,%)和位运算符(&, |, <<, >>, ~, ^)也可用于数值表达式中。举例:($a or $b) and ($c or $d)
计数字符串:想知道字符串在文件或进程内存中出现的次数,变量名是用 # 代替 $ 的字符串标识符。举例:#a == 2 and #b > 2
字符串偏移(虚拟地址)举例:$a at 100 and $b at 200
如果在文件的偏移 100 处(或者在一个正在运行的进程中, 位于虚拟地址 100 位置)发现了字符串 $a, 我们的规则就能捕获到该字符串,当然 $b 也要满足条件。也可以使用十六进制表示:$a at 0x64 and $b at 0xC8
at 操作符指定到一个具体的偏移量, 而你可以使用操作符 in 来指定字符串的位置范围,举例:
$a in (0..100) and $b in (100..filesize)
字符串 $a 必须在偏移 0-100 之间才能找到, 而 $b 则必须是在偏移 100 到文件末尾位置找到。
匹配长度:正则表达式 /fo*/, 可以匹配字符串 fo, foo 和 fooo 等。在字符串标识符前加一个 ! 得到匹配长度, 你就可以将匹配长度作为你条件的一部分。!a[1] 是第一个匹配到的字符串 $a 的长度, 而 !a[2] 就是第二个匹配到的字符串的长度, 依此类推. !a 是 !a[1] 的缩写。举例:
rule Hak5
{
strings:
$re1 = /hack*/ // 可以匹配 hacker, hacked, hack, hack*
condition:
!re1[1] == 4 and !re1[2] > 6
}
文件大小:filesize 保存着正在扫描的文件的大小,大小以字节为单位。举例:filesize > 200KB 。说明:filesize仅在规则应用于文件的时候生效. 如果应用于正在运行的进程, 那么它会永远都匹配不了。
可执行程序入口点:如果扫描的文件是一个 PE 或 ELF 文件, 那么 entry_point 会存有可执行文件的入口点偏移值。而如果扫描一个运行的进程, entry_point 会存有可执行文件入口点的虚拟地址。 entry_point 的经典用法是用于搜索入口点的一些 pattern, 以检测壳或简单的感染病毒. 目前使用 entry_point 的方式是通过导入 PE 和 / 或 ELF 的库并使用它们各自的功能. Yara 的 entrypoint 函数自第 3 版开始就已经过时了。不要使用 yara 的 entrypoint, 请在导入 PE 或 ELF 文件后使用对应的 pe.entry_point 和 elf.entry_point。
访问指定位置的数据:如想从特定偏移位置读取数据, 并将其存为一个变量。可以使用以下任意方式
// 数据存储默认以小端序, 如想要读取大端序的整形数, 请使用下面几个以be结尾的对应函数
int8(<offset or virtual address>)
int16(<offset or virtual address>)
int32(<offset or virtual address>)
uint8(<offset or virtual address>)
uint16(<offset or virtual address>)
uint32(<offset or virtual address>)
int8be(<offset or virtual address>)
int16be(<offset or virtual address>)
int32be(<offset or virtual address>)
uint8be(<offset or virtual address>)
uint16be(<offset or virtual address>)
uint32be(<offset or virtual address>)
// 参数<offset or virtual address>可以是任何一个返回无符号整数的
// 表达式, 包括可以是uintXX函数的返回值
uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550
for…of: 对许多字符串应用同一个条件,样式如下:
for num of string_set : ( boolean_expression )
对每个 string_set 的字符串, 都会计算 boolean_expression 的值, 并且这些值必须至少有 1 个为真。举例:
for any of ($a,$b,$c) : ( $ at elf.entry_point )
for all of them : ( # > 3 )
for all of ($a*) : ( @ > @b ) // 这里指的是偏移量
迭代字符串出现次数
rule Three_Peat
{
strings:
$a = "dummy1"
$b = "dummy2"
condition:
for all i in (1,2,3) : ( @a[i] + 10 == @b[i] )
}
这个规则说的是, $b 出现前三个的字符串应当分别隔 $a 出现的前三个的字符串 10 个字节远. 另外一种写法如下:
for all i in (1..3) : ( @a[i] + 10 == @b[i] )
// 每一次$a都应当出现在文件的前100个字节内,# 表示次数
for all i in (1..#a) : ( @a[i] < 100 )
// 也可以指定字符串的某一次出现需要满足条件(而非全部)。
for any i in (1..#a) : ( @a[i] < 100 )
for 2 i in (1..#a) : ( @a[i] < 100 )
引用其它规则
就像C语言中引用函数那样。函数, 或是这里说的规则, 都必须在使用前进行定义
rule Rule1
{
strings:
$a = "dummy1"
condition:
$a
}
rule Rule2
{
strings:
$a = "dummy2"
condition:
$a and Rule1
}
其它说明
全局规则:Yara 规则允许用户在所有规则中进行约束。如果希望所有规则都忽略掉那些超出特定大小限制的文件, 那么可对规则进行修改, 或是编写一条全局规则:
global rule SizeLimit
{
condition:
filesize < 2MB
}
私有规则:私有规则在匹配时没有任何输出。和其它规则成对引用时,可以使输出更为清楚。比如为了判断文件是否恶意,有这样一条私有规则,要求文件必须是 ELF 文件。一旦满足这个要求,随后就会执行下一条规则。但我们在输出里想看的并不是该文件它是不是 ELF,我们只想知道文件是否恶意,那么私有规则就派上用场了。要想创建一条私有规则,只需要在 rule 前添加一个 private 即可。
private rule PrivateRule
{
...
}
规则标签:如果只想查看 ruleName 类型的规则输出,可以对规则打上标签。
rule TagsExample1 : Foo Bar Baz
{
...
}
rule TagsExample2 : Bar
{
...
}
使用模块:一些模块由 YARA 官方发布, 比如 PE 和 Cukoo 模块. 这些模块就如 python 那样导入即可, 不过在导入时模块名需要添加双引号
import "pe"
import "cuckoo"
// 一旦模块成功导入, 就可以在函数前加模块名, 来使用这些功能。
pe.entry_point == 0x1000
cuckoo.http_request(/someregexp/)
未定义的值:一些值在运行时保留为 undefined。如果以下规则在 ELF 文件上执行并找到对应的字符串, 那么它的结果相当于 TRUE & Undefined。结果返回要注意!
import "pe"
rule Test
{
strings:
$a = "some string"
condition:
$a and pe.entry_point == 0x1000
}
外部变量:外部变量允许你定义一些, 依赖于第三方提供值的规则。该数据可以是整数、字符串、布尔变量
//外部变量允许你在使用 YARA -d 命令时指定一个自定义数据,
//使用布尔变量和一个整数变量作为判断条件
rule ExternalVariableExample2
{
condition:
bool_ext_var or filesize < int_ext_var
}
//字符串变量可以与以下运算符一起使用:
contains:如果字符串包含指定的子字符串,返回 True
matches: 如果字符串匹配给定的正则表达式时,返回 True
rule ExternalVariableExample3
{
condition:
string_ext_var contains "text"
}
rule ExternalVariableExample4
{
condition:
string_ext_var matches /[a-z]+/
}
文件包含:yara 规则可以使用类似 C 语言的导入方式 (#include, 不过 yara 中并不使用 # 号来包含所需的文件, 而是用双引号引起来)来包含其他文件. 你可以在包含时使用相对路径或绝对路径。如果是 windows 系统, 甚至还可以是驱动设备的路径
include "Migos.yar"
include "../CardiB.yar"
include "/home/user/yara/IsRapper.yar"
include "c:\\yara\\includes\\oldRappers.yar"
include "c:/yara/includes/oldRappers.yar"
Yara 使用方法
参数参考 | 功能 |
---|---|
-h 或 --help | 查看 Yara 使用帮助。 |
-C 或 --complied-rules | 规则文件中包含已经使用 yarac 编译的规则。规则分为文本规则和已编译规则,3.9 及以上需要加这个,防止用户无意中使用来自第三方的已编译规则。来自不受信任源的编译规则可能包含。 |
-c 或 --count | 仅打印匹配项数 |
-d < identifier>=< value> 或 --define=identifier=value | 定义外部变量。此选项可以多次使用。 |
-r | 对文件夹进行递归扫描,默认不会递归。 |
--fail-on-warnings | 将警告视为错误。如果与 --no-warnings 一起使用,则不起作用。 |
-f 或 --fast-scan | 快速匹配模式。 |
-i < identifier> 或 --identifier=< identifier> | 打印名为< identifier>的规则,并忽略其余规则。 |
--max-process-memory-chunk=< size> | 扫描进程内存时,以给定大小的块形式读取数据。 |
-l < number> 或 --max-rules=< number> | 匹配多个规则后中止扫描。 |
--max-strings-per-rule=< number> | 设置每个规则的最大字符串数(默认值 = 10000)。如果规则包含更多 然后指定数量的字符串将发生错误。 |
-x < module>=< file> 或 --module-data=< module>=< file> | 将< file>的内容作为数据传递给< module>。示例:-x cuckoo=/cuckoo_report.json。 |
-n 或 --negate | 仅打印不满足规则(否定)。 |
-N 或 --no-follow-symlinks | 扫描时不要遵循符号链接。 |
-w 或 --no-warnings | 禁用警告。 |
-m 或 --print-meta | 打印元数据。 |
-D 或 --print-module-data | 打印模块数据。 |
-e 或 --print-namespace | 打印规则的命名空间。 |
-S 或 --print-stats | 打印规则的统计信息。 |
-s 或 --print-strings | 打印匹配的字符串。 |
-L 或 --print-string-length | 匹配字符串的打印长度。 |
-g 或 --print-tags | 打印标签。 |
--scan-list | 扫描 FILE 中列出的文件,每行一个。 |
-v 或 --version | 显示 Yara 版本信息 |
-z < size> 或 --skip-larger=< size> | 扫描目录时,跳过大于给定< size>(以字节为单位)的文件。 |
-k < slots> 或 --stack-size=< slots> | 分配 < slots> 数的堆栈大小。默认值:16384。允许使用更大的规则,但内存开销更大。 |
-t < tag> 或 --tag=< tag> | 打印标记为 < tag> 的规则,并忽略其余规则。 |
-p < number> 或 --threads=< number> | 使用 < number> 数量指定的线程扫描目录。 |
-a < seconds> 或 --timeout=< seconds> | 经过几秒钟后中止扫描。 |
Yara 使用举例:
// 用法:
yara [OPTIONS] RULES_FILE TARGET
// 将 /foo/bar/rules 中的规则应用于当前目录中的所有文件。 不扫描子目录:
yara /foo/bar/rules .
// 将 /foo/bar/rules 中的规则应用于 bazfile。仅报告标记为 Packer 或 Compiler 的规则:
yara -t Packer -t Compiler /foo/bar/rules bazfile
// 扫描 /foo 目录及其子目录中的所有文件:
yara /foo/bar/rules -r /foo
// 定义三个外部变量 mybool、myint 和 mystring:
yara -d mybool=true -d myint=5 -d mystring="my string" /foo/bar/rules bazfile
// 将 /foo/bar/rules 中的规则应用于 bazfile,
// 同时将 cuckoo_json_report 的内容传递给 cuckoo 模块:
yara -x cuckoo=cuckoo_json_report /foo/bar/rules bazfile
// 扫描多个文件
yara [OPTIONS] RULES_FILE_1 RULES_FILE_2 RULES_FILE_3 TARGET
// 扫描进程:
yara [OPTIONS] RULES_FILE_1 pid
yarac
规则文件可以直接以源代码形式传递,也可以预先使用 yarac 工具编译。如果要使用相同的规则多次调用YARA,建议编译的形式使用规则。这样可以节省时间,因为对于 YARA 来说,加载已编译的规则比反复编译相同的规则更快。这些规则将应用于指定为 YARA 最后一个参数的目标,如果它是目录的路径,则将扫描其中包含的所有文件。
// 用法:
yarac [OPTION]... [NAMESPACE:]SOURCE_FILE... OUTPUT_FILE
参数 | 功能 |
---|---|
-d | 设置外部变量 |
-h | 获取帮助 |
-w | 忽略警告 |
-v | 显示版本 |