在做恶意软件分析和应急响应时,安全工程师经常面临一个问题:手头有一个可疑样本,需要在成千上万个文件里找出同一家族的其他成员。逐个人工查壳、逆向显然不现实,这时候YARA就成了绕不开的工具。YARA由VirusTotal的创始人Victor Alvarez开发,本质上是一种基于描述的规则语言,你用它在样本中描述特征,YARA负责在目标文件集合里做模式匹配。本文会从规则结构讲到实战编写,帮你真正落地这项技能。

YARA规则的基本结构:三大模块缺一不可
一条完整的YARA规则由规则名、meta段、strings段和condition段组成。其中strings和condition是核心,meta只是存放描述信息的元数据区。先看一个最小可用的规则骨架:
rule Example_Malware_Family
{
meta:
author = "analyst"
description = "检测某家族样本"
date = "2025-01-01"
hash = "d41d8cd98f00b204e9800998ecf8427e"
strings:
$a = "evil.exe"
$b = { 6A 40 68 00 30 00 00 6A 14 }
$c = /cmd\.exe\s+\// nocase
condition:
all of them
}meta段里的字段不会参与匹配,纯粹是给人看的,但养成良好的注释习惯非常重要。建议至少写上作者、样本哈希、参考链接和日期,几个月后回看规则时你不会记得当时提取的是哪个样本的特征。
strings段定义了三类最常用的特征:文本字符串、十六进制字节序列和正则表达式。$a是普通字符串,$b是十六进制模式,适合描述机器码片段或文件头特征,$c则是正则表达式,配合nocase修饰符实现大小写不敏感匹配。变量名用$开头是硬性要求。
condition段是规则的灵魂,它决定了strings里定义的特征如何组合判断。上面的all of them表示所有特征都命中才报警,这种写法误报极低,但可能因为样本变种导致漏报。实际编写中需要在误报和漏报之间反复权衡。
字符串与十六进制匹配的进阶技巧
文本字符串除了基本匹配,还支持多种修饰符。nocase忽略大小写,wide匹配UTF-16编码的字符串,这在分析Windows样本时特别有用,因为Windows API内部大量使用宽字符。ascii是默认行为,wide ascii组合可以同时匹配两种编码。此外还有fullword要求完整单词匹配,避免"evil"命中"medieval"这类尴尬情况。
rule String_Modifiers_Demo
{
strings:
$regkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" wide ascii nocase
$url = "http://update" fullword
$hex_wild = { E8 ?? ?? ?? ?? 83 C4 04 }
$hex_jump = { 6A 00 [4-6] 68 00 30 00 00 }
condition:
2 of them
}十六进制模式是YARA的强项。上面代码里$hex_wild中的??是单字节通配符,常用于跳过call指令后的相对地址偏移。$hex_jump中的[4-6]表示跳过4到6个任意字节,这种跳跃语法在匹配编译型恶意代码时非常实用,因为编译器插桩和优化会导致指令间出现可变长度的填充。
一个常见的坑是正则表达式滥用。YARA的正则引擎没有回溯优化,复杂正则会导致扫描性能急剧下降,官方建议尽量避免嵌套量词和贪婪匹配。能用十六进制通配符解决的问题,就优先用十六进制,正则只留给必须的场景,比如匹配格式多变的水坑链接或C2域名。
condition条件表达式:让规则更聪明的关键
condition段支持完整的布尔逻辑和丰富的内置变量。filesize返回文件大小,pe模块能解析PE文件结构,entrypoint是入口点偏移。灵活组合这些变量,可以把规则限定在合理的目标范围内。
import "pe"
rule Ransomware_Loader
{
strings:
$opener = "vssadmin.exe delete shadows /all /quiet" nocase
$marker = { 4D 5A 90 00 }
condition:
filesize < 500KB and
uint16(0) == 0x5A4D and
all of them and
pe.number_of_sections >= 3 and
not pe.is_signed
}上面这条规则用filesize过滤掉超大文件,用uint16(0) == 0x5A4D确认目标是MZ开头的PE文件,再借助pe模块检查段数量并排除已签名文件。uint16(0)这类函数直接读文件头部字节,比定义十六进制字符串更高效,是判断文件类型的推荐做法。
除了pe模块,常用的还有hash模块计算哈希、math模块做熵值分析。加壳样本的某些区段熵值接近8,可以用math.entropy(offset, length) > 7.2作为辅助条件识别加壳行为。不过要注意,高熵本身不是恶意指标,加密文档、游戏资源文件同样熵值很高,只能作为组合条件之一使用。
实战流程:从样本提取到规则验证
拿到一个新样本后,推荐的工作流是:先在隔离环境里跑一遍基础分析,确认样本类型和行为,再用strings命令或者PE分析工具提取可疑字符串。重点关注的特征包括互斥体名、注册表持久化路径、C2地址、PDB路径、调试字符串以及独特的错误提示文案。
特征提取有个原则:优先选择攻击者不易修改的内容。比如硬编码的互斥体名和注册表路径,攻击者改动成本高,是优质特征;而C2域名经常轮换,单独作为强条件容易漏报。同时准备一套"白名单"测试集,把常见的系统文件、正常软件丢进去扫描,确认规则不会误伤。
验证环节建议使用yara命令行工具配合-w参数查看警告,用-s参数打印命中的字符串位置:
# 扫描单个文件并显示命中详情 yara -s my_rule.yar sample.exe # 递归扫描目录,忽略警告 yara -w -r my_rule.yar /samples/ # 统计规则性能(高版本支持) yara --fail-on-warnings my_rule.yar sample.exe
一条成熟的规则往往要经历多轮迭代:初版命中率太高就收紧condition,从any of them改成3 of them或加上文件类型限定;漏报了就补充新变种的特征。很多团队还会把规则接入CI流程,每次提交自动用样本库回归测试,防止规则更新破坏已有检测能力。
常见坑点与优化建议
第一个坑是规则里只有一个短字符串,比如_kernel32.dll这种四五个字符的特征,几乎必然误报。经验法则是文本特征至少8字节以上,或者多个短特征组合出现才触发。第二个坑是忽略了编码问题,很多样本里的字符串是宽字符的,只写普通字符串会漏掉。
性能方面,避免在遍历场景下使用重正则和无限跳跃的十六进制模式,[0-100000]这种大范围跳跃会让扫描慢到无法接受。如果规则要跑在EDR或网关设备上,性能和准确率同样重要。最后,给规则起一个有意义的名字并维护版本记录,团队协作时FamilyName_Behavior_Version这样的命名规范能省去大量沟通成本。