简单介绍和实践

介绍

  官方文档: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次
\ttab
\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显示版本