关于 writing-yara-rules
面向恶意软件分析与检测人员的 YARA 规则编写指南,涵盖耐久特征选择、条件设计及验证,侧重降低误报并兼顾扫描性能。
主要功能
- 01选择稳定的家族特征
- 02组合文本、十六进制与正则字符串
- 03使用 PE、ELF 等模块
- 04控制误报与扫描性能
- 05测试文件和内存规则
- 06组织并共享规则集
适用场景
- 01为恶意软件家族编写检测规则
- 02将样本或 IOC 转为可测试签名
- 03在样本库中搜寻相关恶意软件
- 04审查规则的误报与性能
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trilwu/ secskills --skill writing-yara-rules安装后在 Claude Code 中输入/writing-yara-rules确认已加载。
详细介绍
编写 YARA 规则
好的 YARA 规则匹配的是恶意软件的本质——它的代码、结构和不可避免的常量,而不是它的伪装,例如文件名或下次构建时就会变化的可变字符串。匹配本质,规则就能经受重新编译并捕获整个家族;匹配伪装,你就只能捕获某个样本一次。整个过程的关键在于选择字符串和条件:既要足够具体以避免误报,也要足够持久以便泛化。
适用场景
- 编写 YARA 规则以检测恶意软件家族、工具或能力
- 为已知样本中的文件或内存伪影创建特征码
- 将 IOC 或捕获的样本转化为可移植、可测试的检测规则
- 在语料库中搜寻某个家族(VirusTotal Retrohunt、LOKI/THOR、主机扫描)
- 根据共享代码、常量或结构对相关样本进行聚类
- 审查或调整现有规则,以减少误报并提升扫描性能
不适用场景
- 检测目标属于日志或 SIEM 遥测,而非文件或内存——请使用
writing-sigma-rules - 选择检测目标及其应归属的位置,属于高于规则语法的策略问题——请使用
engineering-detections - 你还不够了解样本,无法选择持久的锚点——请先使用
analyzing-malware - 执行威胁搜寻,而非编写特征码——请使用
hunting-threats - 整理 IOC、归因信息和完整报告——请使用
producing-threat-intelligence
规则结构
包含三个部分:meta(文档信息,不参与匹配)、strings(模式)和 condition(决定是否命中的布尔条件)。
import "pe"
rule Family_Loader_v1
{
meta:
author = "analyst"
date = "2026-07-26"
description = "ExampleLoader stage-1, config-decode stub + family constants"
hash = "a1b2c3...<sha256 of the analyzed sample>"
reference = "https://internal/case/1234"
version = "1"
tlp = "amber"
strings:
$decode = { 8A 04 0? 32 0? 88 0? 4? 3B ?? 72 }
$marker = "cfg::begin" ascii
$mutex = "Global\\ExL-%08x" ascii
condition:
uint16(0) == 0x5A4D and filesize < 2MB and 2 of them
}
重要的元数据字段。 author 和 date 用于记录来源;hash 记录规则所基于样本的确切哈希,以便未来的分析人员重新验证;reference 链接到能佐证规则的案例或报告;description 说明检测对象和原因。元数据是自由格式文本——YARA 不会对其进行求值——因此应将其视为规则的变更日志。添加工具会读取的 version/tlp/score 字段。
字符串与修饰符
文本字符串是默认类型。十六进制字符串匹配原始字节,并支持通配符和跳转。正则表达式写在斜杠之间。
strings:
$text = "Mozilla/5.0 (Custom-UA)" ascii wide
// Hex: ?? = any byte, 0? = any low nibble, [4-6] = 4 to 6 arbitrary bytes
$hex = { 55 8B EC 83 EC ?? 53 56 57 [4-6] E8 ?? ?? ?? ?? }
$re = /\/gate\/[a-f0-9]{16}\.php/ nocase
修饰符会改变字符串的匹配方式。可以组合使用能够共存的修饰符。
| 修饰符 | 效果 |
|---|---|
nocase | 不区分大小写地匹配文本/正则表达式 |
wide | 匹配 UTF-16LE(每个字符两个字节)——适用于大多数 Windows 字符串 |
ascii | 匹配单字节形式;与 wide 配合使用可匹配两种形式 |
fullword | 仅在两侧为非字母数字字节时匹配 |
xor | 匹配全部 256 种单字节 XOR 编码变体(密钥为 0x00-0xFF) |
xor(0x01-0xff) | 在限定的密钥范围内进行 XOR 匹配 |
base64 / base64wide | 匹配三种 base64 编码对齐方式 |
private | 匹配结果不会显示在输出中(-s/回调);仍会计入 of them |
xor 无需知道密钥即可查找由单字节密钥隐藏的字符串;base64 可捕获嵌入编码数据块中的配置值。两者都会增加扫描耗时——只有在有证据表明存在相应编码时才使用,不要默认启用。
构建持久的条件
条件决定了规则的具体程度。除了 all of them / N of them 之外,还可以使用:
condition:
// Anchor on file type before touching strings — cheap and decisive
uint16(0) == 0x5A4D and // MZ (PE)
uint32(0) == 0x464C457F and // 0x7F 'E' 'L' 'F' (ELF)
// Offset constraints
$magic at 0 and // string must sit at offset 0
$tag in (0..1024) and // within the first 1 KB
#beacon >= 3 and // the $beacon string appears 3+ times
// Bound the search — a 40-byte dropper is not a 50 MB installer
filesize < 500KB and
// Quorum across a candidate set, resilient to any one string changing
3 of ($cfg*) and
// for..of: k of a set, with a per-match predicate
for any i in (1..#url) : ( @url[i] < 0x2000 )
#string 表示匹配次数,@string[i] 表示第 i 次匹配的偏移量,!string[i] 表示其长度。for..of 可表达“这些字符串中任意/所有/N 个各自满足某个条件”,适用于编写“已知桩代码 100 字节范围内存在 RWX 标记”之类的规则。先使用 uint* 魔数进行锚定,可让引擎在扫描字符串前跳过不匹配的文件,从而同时提高准确性和速度。
特异性与持久性
每条规则都处于一条光谱上:太宽泛会产生误报,太狭窄则只能捕获一个构建版本。目标是一组特定于家族的常量,加上一个结构锚点。
不好——单个常见字符串,会匹配一半的良性软件:
rule Bad_TooBroad {
strings:
$ = "GetProcAddress"
condition:
$
}
不好——文件名只是伪装的定义;该家族会在每次活动中更改它:
rule Bad_Filename {
strings:
$ = "svch0st_updater.exe" nocase
condition:
$
}
好——多个作者不会轻易更改的常量,并通过文件类型和大小进行限制,同时要求达到一定数量的匹配项,避免一个被修改的字符串导致规则失效:
rule Good_FamilyCluster {
meta:
description = "ExampleRAT: config key schedule + mutex fmt + UA"
strings:
$keysched = { 66 0F 6F 05 ?? ?? ?? ?? 66 0F EF C8 66 0F 7F 4? }
$mutexfmt = "ExR-{%04x-%04x}" ascii
$ua = "X-ExR-Build:" ascii wide
$err = "cfg decrypt failed @stage%d" ascii
condition:
uint16(0) == 0x5A4D and filesize < 3MB and 3 of them
}
好的规则之所以能泛化,是因为它的锚点是恶意软件必须具备的东西——它自己的密钥调度、自有互斥体格式、自有诊断字符串——而不是操作人员会在不同部署之间切换的内容。
使用模块
模块会公开解析后的结构。在文件顶部使用 import。
PE——对 Windows 样本最有价值的模块:
import "pe"
rule PE_Structural {
condition:
pe.is_pe and
pe.imphash() == "d2a6...<import hash>" and // API import fingerprint
pe.number_of_sections == 5 and
pe.imports("wininet.dll", "InternetOpenA") and
pe.exports("PluginInit") and
for any s in pe.sections : ( s.name == ".xdata" and s.raw_data_size == 0 ) and
pe.rich_signature.length > 0 and // Rich header (compiler fingerprint)
pe.timestamp > 1577836800
}
imphash 会将使用相同导入表构建的样本聚类;Rich 标头可为构建工具链生成指纹,且很难伪造;pe.imports/pe.exports 锚定的是能力,而非偶然出现的字节。
ELF 为 Linux 提供与 PE 类似的功能(elf.number_of_sections、elf.type、elf.symtab)。math 可为区域评分——标记经过压缩或加密的数据块:
import "math"
rule Packed_HighEntropy {
condition:
uint16(0) == 0x5A4D and
math.entropy(0x1000, filesize - 0x1000) >= 7.2 and // near-random tail
math.mean(0, filesize) > 100
}
hash 可在条件中计算摘要——适合固定特定的嵌入式资源,而非整个文件(整个文件的哈希无需使用 YARA):
import "hash"
rule EmbeddedResource {
condition:
hash.sha256(0x400, 0x200) == "e3b0c4...<digest of the fixed config blob>"
}
定位与位置无关的特征
锚定会随代码移动的特征,而非随活动变化的特征。按持久性大致排序:加密常量(S 盒、MD5/SHA 初始值、自定义密钥调度)→ 该家族自己的互斥体/管道/注册表名称模板 → 配置标记和分隔符 → C2 URI 模板(/gate/、/panel/upload.php)→ 开发者留下的独特错误和调试字符串 → 使用 floss 反混淆后得到的栈字符串。文件名、编译时间戳、活动使用的 C2 IP,以及单个常见的 Win32 API 名称都不持久——它们只是伪装。
消除误报
规则命中样本并不代表完成;不命中良性软件才算完成。以下三个手段要结合使用:
- 匹配数量——要求匹配
N of M个字符串,绝不要只依赖一个常见字符串。任何单个字符串都可能出现在良性软件中;三个家族常量同时出现则不会。 - 文件类型锚点——在条件开头使用
uint16(0) == 0x5A4D或 ELF 魔数,这样规则就不会扫描无关文件类型。 - 大小限制——
filesize < N。20 KB 的加载程序不是 200 MB 的游戏客户端;大小限制可以轻松排除整类偶然匹配。
测试与验证
验证不是可选项。未经测试的规则迟早会引发误报风暴。
yara -w rule.yar sample # -w suppresses warnings; does it fire?
yara -s -w rule.yar sample # -s prints which strings matched, and where
yara -w -r rule.yar ./samples/family/ # -r recurse: must hit every known-true sample
yara -w -r rule.yar ./corpus/goodware/ # must produce ZERO hits — the load-bearing step
yara -C rules.yarc ... # compile once, scan many with the compiled form
原子及 4+ 字节锚点为何重要。 YARA 会通过提取每个字符串中的短定长字节子串——原子——来进行扫描,并先搜索这些子串;原子匹配后,才会对整个字符串进行完整验证。如果一个字符串唯一的定长片段只有 1–2 个字节(通配符过多的十六进制模式、两个字母的文本字符串),就会产生较弱的原子,导致其频繁匹配,几乎对每个文件都要进行验证,从而使性能大幅下降。确保每个字符串至少包含一段连续的 4+ 个非通配符字节。YARA 会在编译时针对速度较慢的字符串和较短的原子发出警告——去掉 -w(该选项会抑制这些警告),并将这些警告视为错误。避免使用无界和灾难性正则表达式(.*、嵌套量词);优先使用有界字符类和跳转。规则集运行缓慢时,可使用 yara -S(--print-stats,按规则统计信息)进行性能分析。
内存与进程扫描
文件规则和内存规则不可互换。以
uint16(0) == 0x5A4D 和 PE 节为锚点的规则,不会在载荷已解包、已重定位且没有磁盘头的进程上触发。
yara -w rule.yar mem.dmp # scan a memory dump / minidump
yara -w rule.yar 4242 # scan a live process — the PID is the final argument
yara -w -s rule.yar 4242 # -s prints which strings matched in the process, and where
对于内存规则,应去掉文件魔数锚点和 filesize 限制(进程没有有意义的文件大小);改为以仅在解包后才存在的解码后字符串和代码为锚点——明文配置、C2 模板、运行时拼接的栈字符串。将文件扫描规则和内存扫描规则分开,或者为仅适用于文件的条件设置门控,让同一条规则可以用于两者。
YARA-X:现代引擎
YARA-X 是 Rust 重写版本,也是发展方向:速度更快、更安全、诊断更完善,并且在很大程度上与源代码兼容。条件允许时,编写可在两者下运行的新规则。需要了解以下差异:
- CLI 为
yr(例如,使用yr scan rule.yar sample扫描、使用yr compile编译、使用yr fmt格式化)。 - 更严格的解析会拒绝一些有歧义的旧规则;负索引和少数已弃用的结构已被移除;
include和模块语义也有所收紧。 - 正则表达式引擎有所改进,性能警告也更清晰;
yr fmt和语言服务器已内置。 - 经典 YARA 的部分第三方模块尚未全部移植——依赖冷门模块前请先确认。
如果部署环境中混用不同引擎,请使用两种引擎测试同一条规则(LOKI/THOR 和较旧的扫描器运行经典 YARA;较新的流水线运行 YARA-X)。
组织和共享规则
- 规则集 — 每个家族或活动使用一个文件,通过
include "base.yar"共享辅助规则;将整个目录编译成单个.yarc以供分发。 - 私有规则 —
private rule不会单独报告;在公共规则中引用它,将其用作可复用的谓词(例如共享的“是否为 Windows PE”门控、共享的加壳器检测器)。这样可以让条件保持 DRY 且易于阅读。 - 标签 —
rule Foo : loader family_x { ... };扫描时使用yara --tag loader筛选。按角色和家族添加标签,便于在搜寻时选择子集。 - 外部变量 —
yara -d ext=value(或-x module=path);在条件中引用ext,让同一条规则可根据不同环境采用不同表现(例如由扫描主机传入的文件名/文件路径)。 - 元数据规范 — 统一的
author/date/reference/hash/score 字段,可让下游工具(THOR、VT、你的 CI)进行排序、评分和归属。
private rule is_pe {
condition: uint16(0) == 0x5A4D and filesize < 10MB
}
rule Family_X_Loader : loader family_x {
meta:
author = "analyst"
score = 75
strings:
$a = "ExL-%08x" ascii
$b = { 8A 04 0? 32 0? 88 0? 4? 3B ?? 72 }
condition:
is_pe and all of them
}
应当摒弃的借口
- “我对样本做了哈希,这就是一条规则。” 哈希只能精确匹配一个文件,除此之外什么也匹配不到。下一个构建版本的哈希就会不同。这是一个 IOC,不是检测规则。
- “它匹配了样本,可以发布了。” 未经良性文件语料库测试的规则,会引发一场尚未爆发的误报风暴。扫描样本并不能证明它不会匹配其他内容。
- “有一个好字符串就够了。” 一个字符串只需经过一次变异就会失效,而且常见字符串无处不在。应要求达到家族特征常量的多数门槛。
- “为求稳妥,我会给整个函数加通配符。” 过度使用通配符的十六进制规则不会留下 4 字节原子,因此规则只能依赖薄弱锚点匹配,严重拖慢扫描性能。保留真实的字节序列。
- “文件名很有辨识度,就用它吧。” 文件名只是伪装——操作者每次活动都可以零成本更名。应以代码和结构为锚点。
- “它在文件上有效,所以在内存中也有效。” 解包后的进程映像中不存在文件魔数和节锚点。内存扫描需要自己的规则。
- “先写宽泛规则,以后再调优。” 生产环境中的每次嘈杂命中都会损害分析师的信任,并掩盖真正的匹配。应在部署前调优,而不是部署后再调。
阅读外部来源
以 Markdown 格式获取公开公告、规范和供应商报告:
curl -sL "https://defuddle.md/<url>" # scheme in the path is optional
这会剥除页面中的冗余内容——对于以文字为主的页面,令令牌数减少约 78%——并返回全文而非摘要,因此你可以对其进行 grep,并相信未找到结果的判断。
以下三种情况不适用。获取 JSON 和 API 响应时应使用原始内容,因为可读性提取会破坏结构化数据。获取需要身份验证或由 JavaScript 渲染的页面时,应直接访问,因为它会以匿名方式检索页面。切勿通过它访问对手的基础设施(钓鱼链接、C2、恶意软件托管站点)、客户端拥有的主机或交战 URL——请求会离开你的机器并发送给第三方;对于正在运作的对手基础设施,这也会向运营者暴露你的行动。
有些网站会阻止提取器,并返回错误内容而非页面——例如 freedesktop.org 返回的 {"error":"Failed to fetch: 418 I'm a teapot"}。这表示获取请求遭到拒绝,并不表示来源称该内容不存在。在据此得出任何结论之前,请直接重新获取该 URL。
参考资料
writing-sigma-rules— 将同样的技巧用于日志和 SIEM 遥测数据engineering-detections— 选择检测目标及其适用位置analyzing-malware— 了解构建持久规则所依据的样本hunting-threats— 执行这些签名所驱动的威胁狩猎producing-threat-intelligence— 整理 IOC、归因和报告reporting-security-findings— 编写检测结果报告- YARA(VirusTotal)和 YARA-X — 检测引擎及其文档
- yarGen — 根据样本自动生成候选规则和字符串(仅作起点,绝非可直接使用的成品规则)
- yara-validator /
yr fmt— 在 CI 中检查并规范化规则 - LOKI 和 THOR(Nextron)— 用于主机端狩猎的 IOC 和 YARA 扫描器
- VirusTotal Retrohunt — 在 VT 的样本库中运行规则,以查找相关样本