yara入门
YARA is a tool aimed at (but not limited to) helping malware researchers to identify and classify malware samples. With YARA you can create descriptions of malware families (or whatever you want to describe) based on textual or binary patterns.
yara(Yet Another Recursive Acronym)是一款强大的模式匹配工具(卧槽,老外的幽默是骨子里的),主要用于恶意软件分析、威胁检测和文件分类。它允许研究人员和安全分析师基于文本或二进制模式(如字符串、正则表达式、字节码等)创建规则,从而识别和分类文件或进程。
-
包管理工具安装
sudo apt-get install yara yara-doc libyara8 libyara-dev python3-yara python3-yaramod libyaramod0 libyaramod-dev -
源码安装
git clone https://github.com/VirusTotal/yara.git ./bootstrap.sh && ./configure --enable-cuckoo --enable-magic --enable-dotnet make && make install
1. 核心功能
- 基于规则的检测,使用自定义规则匹配文件、内存数据或网络流量中的特征
- 支持多种数据类型,可检测字符串、十六进制模式、正则表达式、文件哈希等
- 模块化设计,支持扩展模块,如PE、ELF、Mach-O、PDF、ZIP等文件格式解析
- 跨平台,支持Windows、Linux、macOS,并可以集成到多种安全工具中,如VirusTotal、Cuckoo沙箱
- 高性能,适用于大规模文件扫描和实时监控
2. 语法规则
yara语法规则十分简单,下面是入门文档中介绍的一则简单的样例。
rule silent_banker : banker // 分类空白
{
meta:
description = "This is just an example"
thread_level = 3 // 威胁等级3
in_the_wild = true // 这条规则是基于真实世界中捕获的样本写的,不是仅用于测试或概念验证
strings:
$a = {6A 40 68 00 30 00 00 6A 14 8D 91}
$b = {8D 4D B0 2B C1 83 C0 27 99 6A 4E 59 F7 F9}
$c = "UVODFRYSIHLNWPEJXQZAKCBGMT"
condition:
$a or $b or $c
}
规则定义使用rule关键字,和c语言变量名定义一样,可以使用子母下划线,但是不可以使用数字作为首子母,同时规则名称大小写子母敏感,并不允许超过128字节。

规则本身由规则元数据,字符串定义和匹配条件三部分组成,字符串定义可省略,但是匹配条件是规则核心部分。匹配条件部分是整个规则的逻辑核心,它包含一个boolean表达式,进而判断一个文件或者进程是否匹配规则,上面的匹配条件部分引用字符串$a,$b,$c变量作为boolean变量,如果在查找的进程或文件中存在对应部分,条件匹配
与c语言一样,yara规则使用/**/作为段注释,//作为行注释开始
字符串定义
在yara规则中存在三种类型的字符串定义
-
十六进制字符串
wild-cards, jumps, and alternatives三种形式使得十六进制字符串的定义更为灵活
rule WildcardExample { strings: $hex_string = { E2 34 ?? C8 A? FB } condition: $hex_string } rule JumpExample { strings: $hex_string = { F4 23 [4-6] 62 B4 } // FE 39 45 [10-] 89 00 匹配10到无穷字节 // FE 39 45 [-] 89 00 匹配0到无穷字节 condition: $hex_string } rule AlternativesExample { strings: $hex_string = { F4 23 ( 62 B4 | 56 | 45 ?? 67 ) 45 } condition: $hex_string } -
文本字符串
文本字符串可以在其定义后增加修饰符,修饰符会改变字符串的解释方式
rule TextExample { strings: $text_string = "foobar" nocase // wide,表示匹配宽字符 // nocase,大小写无关 // ascii,兼容ascii形式 // "Hello" ascii 48 65 6C 6C 6F // "Borland" wide ascii,会略微增加扫描时间(需检查两种编码) // ASCII: 42 6F 72 6C 61 6E 64 // 宽字符: 42 00 6F 00 72 00 6C 00 61 00 6E 00 64 00 // xor,匹配但字节异或加密的字符串 // "This program cannot" xor // YARA会自动尝试所有1字节的XOR key(也就是0x00到0xFF),将目标数据异或后,检查是否还原出字符串"This program cannot" // fullword,匹配的字符串是一个完整的词 // "test",不添加fullword的匹配test、testing、latest、mytestfile // "test" fullword,只匹配test condition: $text_string } -
正则表达式
正则表达式最为灵活,正则表达式不再使用双引号定义而是使用前向斜线,和文本字符串一样正则表达式也可使用nocase, ascii, wide, fullword修饰,两者语义相同 在yara2.0之前的版本,使用PCRE和RE2正则库,在新版本中yara内置正则表达式引擎,该引擎使用了PCRE大多数特性,但不包括组捕获,后向引用,POSIX字符集
rule RegExpExample1 { strings: $re1 = /md5: [0-9a-fA-F]{32}/ $re2 = /state: (on|off)/ condition: $re1 and $re2 }
条件语句
条件语句和所有的编程语言一样是一系列的boolean表达式,其中可以使用and,or,not连接,同时支持关系操作符>=, <=, <, >, == 和 !=,也支持将算数运算符(+, -, *, , %)和位运算符(&, |, «, », ~, ^)用作数值表达式,字符串标识符用作表示该字符串是否存在
-
计数字符
通过#标识符,返回字符串出现的次数
rule CountExample { strings: $a = "dummy1" $b = "dummy2" condition: #a == 6 and #b > 10 } -
位置判断
可以通过@a[i]来获取字符串$a的第i次出现的偏移(offset)或虚拟地址,判断是否符合规则 如果你提供了一个超过该字符串实际出现次数的索引,返回的将是一个NaN非数字值
rule AtExample { strings: $a = "dummy1" $b = "dummy2" condition: $a at 100 and $b at 200 // 表示字符串a出现在100的偏移处,并且字符串b出现在200的偏移处 $a in (0..100) and $b in (100..filesize) // 表示在0-100偏移中查找字符串a,在后续的内容中查找字符串b } -
长度匹配
对于很多包含跳跃(jumps)的正则表达式或十六进制字符串来说,匹配的长度是可变的
例如,正则表达式 /fo*/ 可以匹配以下字符串,“fo”,“foo”,“foooo”,它们都是有效匹配,但长度各不相同,你可以使用!a[1]表示$a的第1次匹配的长度
!a是!a[1]的简写形式,表示第1次匹配的长度 -
文件大小
在对文件进行扫描时,可以使用filesize关键字表示当前正在扫描的文件大小
-
可执行文件的入口
entrypoint关键字,如果正在扫描的是一个PE可移植可执行文件或ELF可执行与可链接格式文件,并且是在扫描文件的情况下,表示可执行文件入口点的原始偏移地址,如果是在扫描运行中的进程,那么 entrypoint表示的是主可执行文件入口点的虚拟地址。典型用法是检测加壳程序或简单的文件感染者
rule EntryPointExample1 { strings: $a = { E8 00 00 00 00 } condition: $a at entrypoint } rule EntryPointExample2 { strings: $a = { 9C 50 66 A1 ?? ?? ?? 00 66 A9 ?? ?? 58 0F 85 } condition: $a in (entrypoint..entrypoint + 10) } -
访问指定位置数据
有很多情况下,你可能希望根据文件中的某个偏移地址或虚拟内存地址中的数据来编写条件,uintx(offset)可从offset处读取x位数据
rule IsPE { condition: // MZ signature at offset 0 // PE signature at offset stored in MZ header at 0x3C uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 } -
集合条件
rule OfExample2 { strings: $foo1 = "foo1" $foo2 = "foo2" $foo3 = "foo3" condition: 2 of ($foo*) // equivalent to 2 of ($foo1,$foo2,$foo3) 1 of them // equivalent to 1 of ($*) all of them // all strings in the rule any of them // any string in the rule } rule ForAllWithPrefix { strings: $a1 = "evil" $a2 = "bad" $a3 = "danger" condition: for all of ($a*) : ( @ < 200 ) // 所有以$a开头的字符串都必须在偏移小于200的位置才匹配 } rule Occurrences { strings: $a = "dummy1" $b = "dummy2" condition: for all i in (1,2,3) : ( @a[i] + 10 == @b[i] ) // 在三次a字符串出现后的10个偏移处都是b字符串 for all i in (1..#a) : ( @a[i] < 100 ) // 所有匹配到的 $a 字符串,其偏移位置都在文件的前 100 个字节以内 } -
引用其他规则
rule Rule1 { strings: $a = "dummy1" condition: $a } rule Rule2 { strings: $a = "dummy2" condition: $a and Rule1 }
规则元数据
meta节中,使用key-value键值对定义规则元数据 虽然元数据对于yara扫描器没有直接作用,但是但在一些使用YARA的平台或工具中,它可能用于过滤结果(比如只显示in_the_wild=true的命中),区分测试规则与正式规则,自动化上报时,赋予不同优先级,安全团队内部根据它做tag/分级/审核
其他扩展
-
全局规则
使用global关键字修饰的规则,可以作用于所有规则之上,例如忽略文件超过2MB的文件
-
私有规则
使用private修饰的规则,在匹配文件时不会被YARA主动报告出来,这样但结合YARA的规则引用特性,把复杂的检测逻辑拆成多个小块,每块写成一个private rule,在一个主规则中引用这些private rule,最终只让主规则显示在输出中,可以避免YARA输出太杂乱
-
规则标签
标签是附加在规则上的“关键词”,可以帮助你在运行YARA时快速 筛选感兴趣的规则匹配结果
global rule SizeLimit { condition: filesize < 2MB } private rule common_strings { strings: $a = "dummy" condition: $a } rule master_rule { condition: common_strings } yara --tag=apt rules.yar sample.bin rule detect_something : malware apt groupx { strings: $a = "evil_string" condition: $a } -
外部变量
在yara规则中允许用户在规则中引用外部变量,外部变量可是是整形,字符串或者布尔类型。除了传统的操作类型,外部类型允许使用contains和matches操作。
rule ExternalVariableExample { condition: ext_var == 10 myvar contains "world" // yara -d myvar="hello world" myrules.yar file.bin // 如果外部变量定义myvar为"hello world",那么这个条件就为True string_ext_var matches /[a-z]+/is // 匹配perl语法的正则表达式,i忽略大小写,s单行模式 } -
include其他规则
使用include关键字,可以像c语言一样包含其他规则到当前规则中
3.模块扩展
外部模块扩展yara的特性,它使得在规则中可以使用更复杂的数据结构。yara官方随附提供,PE模块、Cuckoo模块、Magic模块、Hash模块、Math模块、Dotnet模块、Time模块,这些模块通常用于处理特定类型的文件或获取高级特征
使用案例
- PE模块:分析Windows可执行文件的结构(如导入表、节信息等)
- Cuckoo模块:结合沙箱分析结果进行检测
- Magic模块:允许根据文件的输出标准UNIX命令识别文件的类型
- Hash模块:提供计算文件全部内容或部分内容的MD5,SHA1,SHA256能力
- Math模块:执行基础的数学运算,主要用于规则中对提取数据进行数值处理或转换判断
- Dotnet模块:允许使用.NET文件格式的属性和功能为,NET文件创建更细粒度的规则
- Time模块:时间模块允许您在Yara规则中使用时间条件
import "pe"
rule pe_example
{
condition:
pe.number_of_sections == 1
pe.exports("CPlApplet")
// 判断目标 PE文件是否导出了名为CPlApplet的函数
// 这是一个Windows控制面板小程序必须导出的函数,Windows会调用这个函数来与.cpl文件进行交互
// 如果一个PE文件.dll/.cpl导出了这个函数,那么它极有可能是一个控制面板小程序
pe.characteristics & pe.DLL
// pe特性字段位与pe.DLL,判断当前文件是否是dll文件
}
import "elf"
rule single_section
{
condition:
elf.number_of_sections == 1
elf.machine == elf.EM_X86_64
}
# 传统的YARA规则主要依赖于文件中的字符串、结构、二进制特征等静态内容
# 传统规则基于静态字符串的检测如下所示
# 1.误报(合法程序可能只是把这个域名当作文档、示例或者调试信息写进了文件,并不会真正访问该域名)
# 2.漏报(真正的恶意程序可能根本不会以明文形式存储域名,而是加密、Base64 编码、拆分拼接等方式)
# cuckoo模块允许动态行为与静态内容联合分析, Cuckoo模块为执行文件生成行为报告
rule evil_domain
{
strings:
$domain = "someone.doingevil.com"
condition:
$domain
}
yara -x cuckoo=behavior.json rules.yar malware.exe
import "cuckoo"
rule evil_doer
{
strings:
$some_string = { 01 02 03 04 05 06 }
condition:
$some_string and
cuckoo.network.http_request(/http:\/\/someone\.doingevil\.com/)
// cuckoo模块会加载behavior.json文件,该文件又cuckoo生成,进而判断威胁
}
import "magic"
rule is_pdf
{
condition:
magic.mime_type == "application/pdf"
}
import "hash"
rule detect_known_hash
{
condition:
hash.sha256(0, filesize) == "a7a287d6ce872f9ab6fffc31db9f6369"
// 参数0表示文件的偏移
}
import "time"
rule created_after_2024
{
condition:
time.created > 1704067200 // 2024-01-01 00:00:00 UTC
// 判断文件是否是2024年后创建
}
编写模块
除了使用官方内置的上述模块外,开发者可以自己编写模块(使用c语言编写),丰富yara的检测能力,在检测规则中可以直接使用 模块源码置于libyara/modules目录,强烈建议模块文件和目录文件同名,比如开发foo模块,最好将模块文件命名为foo.c 模块中demo模块的demo.c是学习自定义模块编写的入门资料
#include <yara/modules.h>
#define MODULE_NAME demo
int my_hello_function(
YR_OBJECT* function_obj,
YR_SCAN_CONTEXT* context,
int argc,
YR_OBJECT_FUNCTION_ARGUMENT* args,
YR_OBJECT** result)
{
// 创建一个返回值对象并设置值
*result = yr_object_create(YR_OBJECT_STRING, NULL);
if (*result == NULL)
return ERROR_INSUFFICIENT_MEMORY;
return yr_object_set_string(*result, "Hello from YARA module", context);
}
int my_add_function(
YR_OBJECT* function_obj,
YR_SCAN_CONTEXT* context,
int argc,
YR_OBJECT_FUNCTION_ARGUMENT* args,
YR_OBJECT** result)
{
int a = args[0].value.i;
int b = args[1].value.i;
*result = yr_object_create(YR_OBJECT_INTEGER, NULL);
if (*result == NULL)
return ERROR_INSUFFICIENT_MEMORY;
return yr_object_set_integer(*result, a + b, context);
}
begin_declarations;
// 申明可以在规则中使用的函数和数据结构
declare_string("greeting");
// 更复杂的数据结构
begin_struct("some_structure");
declare_integer("foo");
begin_struct("nested_structure");
declare_integer("bar");
declare_string_array("bat");
declare_string("baq");
declare_float("tux");
end_struct("nested_structure");
end_struct("some_structure");
// "ii"表示接受两个整数参数, "i"表示返回整数
declare_function("add", "ii", "i", my_add_function);
declare_function("hello", "", "s", my_hello_function);
end_declarations;
int module_initialize(
YR_MODULE* module)
{
// 模块的构造函数,函数允许开发者初始化模块可能使用的全局化的数据结构
return ERROR_SUCCESS;
}
int module_finalize(
YR_MODULE* module)
{
// 模块的析构函数
return ERROR_SUCCESS;
}
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
{
// 这个函数会在每个被扫描的文件上调用一次,但前提是某个规则通过import指令导入了该模块
// module_load函数是模块获取扫描文件的机会,在这个阶段你可以以你喜欢的方式分析或解析这个文件,然后将分析结果填充到你在模块的declarations部分定义的数据结构中
// 下面操作将"Hello World!"赋值给前期定义的greeting变量
set_string("Hello World!", module_object, "greeting");
return ERROR_SUCCESS;
}
int module_unload(
YR_OBJECT* module_object)
{
// 允许开发者在此刻释放在module_load阶段申请的相关资源
return ERROR_SUCCESS;
}
#undef MODULE_NAME
import "demo"
rule use_hello
{
condition:
demo.hello() contains "Hello"
}
在编写完成实现代码后,需要在module_list文件中增加MODULE(demo)语句,最后修改libyara/Makefile.am将自身添加至编译过程
4.C-API使用
把大象塞进冰箱的三个步骤:打开冰箱门,把大象塞进去,关上冰箱门
同样,libyara库的使用也十分简单
#include <unistd.h>
#include "yara.h"
void compile_callback(int error, const char *file, int line, const YR_RULE *rule, const char *msg, void *data) {
fprintf(stderr, "[%s] %s:%d: %s\n", error == YARA_ERROR_LEVEL_WARNING ? "warning" : "error", file, line, msg);
}
// 函数必须返回CALLBACK_CONTINUE,CALLBACK_ABORT,CALLBACK_ERROR
int scan_callback(YR_SCAN_CONTEXT *ctx, int message, void *message_data, void *user_data) {
int result = CALLBACK_CONTINUE;
YR_RULES *rule = NULL;
YR_MODULE_IMPORT *module = NULL;
YR_OBJECT_STRUCTURE *obj = NULL;
switch (message) {
case CALLBACK_MSG_RULE_MATCHING:
case CALLBACK_MSG_RULE_NOT_MATCHING:
rule = (YR_RULES *)message_data;
fprintf(stdout, "threat %s\r\n", message == CALLBACK_MSG_RULE_MATCHING ? "found" : "not found");
break;
case CALLBACK_MSG_IMPORT_MODULE:
// 模块导入时调用,每扫描一个文件都会调用,只要规则中包含import语句
// 初始化module结构体中,data和size字段,data字段存放cuckoo的json报告,size内容大小
module = (YR_MODULE_IMPORT *)message_data;
break;
case CALLBACK_MSG_MODULE_IMPORTED:
// 模块导入完成时调用
// 可以根据加载结果输出日志或做统计分析
obj = (YR_OBJECT_STRUCTURE *)message_data;
break;
case CALLBACK_MSG_SCAN_FINISHED:
fprintf(stdout, "scan finish\r\n");
break;
default:
break;
}
return result;
};
YR_RULES *do_yara_rule_load(YR_COMPILER *compiler) {
YR_RULES *rules = NULL;
struct {
const char *name;
FILE *f;
} predef_rules[] = {
{"./rules/malware.yar", NULL},
};
yr_compiler_set_callback(compiler, compile_callback, NULL);
for (int i = 0; i < sizeof(predef_rules) / sizeof(predef_rules[0]); i++) {
predef_rules[i].f = fopen(predef_rules[i].name, "r");
if (!predef_rules[i].f) {
fprintf(stderr, "failed to open yara rule %s \r\n", predef_rules[i].name);
goto done;
}
int errors = yr_compiler_add_file(compiler, predef_rules[i].f, NULL, NULL);
if (errors) {
fclose(predef_rules[i].f);
fprintf(stderr, "failed to compile %s, total %d errors find\r\n", predef_rules[i].name, errors);
break;
}
fclose(predef_rules[i].f);
}
// yr_rules_save() and yr_rules_load() 可以保存和加载已编译的规则
// 一旦yr_compiler_get_rules调用,后面不可以再编译规则
yr_compiler_get_rules(compiler, &rules);
yr_rules_save(rules, "rules.compile");
done:
return rules;
}
int main(int argc, char** argv) {
int result = -1;
YR_COMPILER *compiler = NULL;
YR_RULES *rules = NULL;
// 分配库运行所需的资源,初始化内部的数据结构
// 在使用规则前必须使用yara编译器将规则编译成二进制形式
// 多线程环境只需要主线程调用初始化构造与析构函数
if (yr_initialize()) {
fprintf(stderr, "failed to inital yara\r\n");
return 0;
}
if (yr_compiler_create(&compiler)) {
fprintf(stderr, "failed to create yara compiler\r\n");
goto done;
}
// 如果你的规则中引用外部变量,必须在编译前定义,在编译后你可以改变该值,编译期间只做检查而已
// yr_compiler_define_integer_variable(compiler, "my_var", 0);
if (access("rules.compile", F_OK) != 0) {
if (!(rules = do_yara_rule_load(compiler))) goto done;
} else
yr_rules_load("rules.compile", &rules);
// 把大象塞进冰箱的最后一步,就是调用yr_rules_scan_xxx函数族扫描文件
// flag字段目前只支持SCAN_FLAGS_FAST_MODE和0,fastmod表示一旦某个字符串在文件中被找到,后续即使它再次出现,也不会再被匹配
// timeout字段0表示永不超时
// yr_scanner_xxx函数族也可以扫描文件内存等
yr_rules_scan_file(rules, "test.txt", SCAN_FLAGS_FAST_MODE, scan_callback, NULL, 0);
result = 0;
done:
if (rules) yr_rules_destroy(rules);
if (compiler) yr_compiler_destroy(compiler);
yr_finalize();
return result;
}
5.实际案例
最后分析一个来着威胁情报的实际yara规则,作为入门篇的结束
rule apt_UAC0063_Settings_xml_containing_VBE {
meta:
intrusion_set = "UAC-0063"
description = "Detects settings.xml file containing a VBE in hex"
source = "Sekoia.io"
creation_date = "2024-12-03"
classification = "TLP:GREEN"
hash = "e3f6d079d99eeb54566fc37fa24ff6f7"
strings:
$start = "<w:settings"
$vbe_head = "23407e5e"
$vbe_tail = "5e237e40"
$var = "w:val="
condition:
filesize < 50KB
and $start
and $vbe_head
and $vbe_tail
and #var > 100
}