writing-yara-rules について
マルウェア分析者や検知エンジニア向けの YARA ルール作成ガイドです。長く使える特徴と条件設計、検証を重視し、誤検知を抑えながらスキャン性能を考慮します。
主な機能
- 01耐久性のあるファミリー特徴の選定
- 02テキスト、ヘキサ、正規表現の組み合わせ
- 03PE、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 ルールがマッチするのは、マルウェアの 本質、つまりコード、構造、避けられない定数であり、ファイル名や次のビルドで変わる可変文字列といった 見せかけ ではありません。本質にマッチすれば、再コンパイル後もルールが機能し、ファミリー全体を検出できます。見せかけにマッチすれば、1 つのサンプルを一度検出するだけです。すべての要点は、誤検知を避けるだけの具体性と、汎用性を保つだけの耐久性を兼ね備えた文字列と条件を選ぶことです。
使用する場面
- マルウェアファミリー、ツール、または機能を検出する YARA ルールを作成する
- 既知のサンプルから、ファイルまたはメモリ内のアーティファクト用シグネチャを作成する
- IOC や取得した検体を、移植可能でテスト可能な検出ルールにする
- コーパス全体でファミリーを探索する(VirusTotal Retrohunt、LOKI/THOR、ホスト上でのスキャン)
- 共通するコード、定数、または構造で関連サンプルをクラスタリングする
- 誤検知やスキャン性能を考慮して既存ルールをレビューまたは調整する
使用しない場面
- 検出対象がファイルやメモリではなく、ログや SIEM テレメトリである場合 —
writing-sigma-rulesを使用してください - 検出対象や配置先の選定など、ルールの構文より上位の戦略を検討する場合 —
engineering-detectionsを使用してください - 耐久性のあるアンカーを選べるほど、まだサンプルを理解できていない場合 —
まず
analyzing-malwareを使用してください - シグネチャの作成ではなく、ハントを実行する場合 —
hunting-threatsを使用してください - IOC、帰属分析、完成したレポートをまとめる場合 —
producing-threat-intelligenceを使用してください
ルールの構成
セクションは 3 つです。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 項目も追加してください。
文字列と修飾子
テキスト文字列がデフォルトです。16 進文字列は生のバイト列にマッチし、ワイルドカードとジャンプに対応します。正規表現はスラッシュで囲んで記述します。
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(1 文字あたり 2 バイト)にマッチします。Windows の文字列の多くが該当します |
ascii | 1 バイト形式にマッチします。wide と組み合わせると、両方にマッチします |
fullword | 英数字以外のバイトで区切られている場合にのみマッチします |
xor | 1 バイトの XOR でエンコードされた 256 通りすべてのバリアント(キー 0x00-0xFF)にマッチします |
xor(0x01-0xff) | 指定範囲のキーを使った XOR にマッチします |
base64 / base64wide | 3 通りの base64 エンコード位置にマッチします |
private | マッチは出力(-s/コールバック)に表示されません。ただし、of them ではカウントされます |
xor はキーがわからなくても、1 バイトのキーで隠された文字列を検出します。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* のマジック値をアンカーにすれば、マッチしないファイルを対象に文字列をスキャンする前にエンジンが除外できます。これは正確さと速度の両方に役立ちます。
特異性と耐久性
各ルールは、広すぎる(誤検知が多い)状態と狭すぎる(1つのビルドしか検出できない)状態の間にあります。ファミリー固有の定数群に、構造的なアンカーを1つ加えることを目指します。
悪い例 — よくある文字列1つだけを使うと、無害なソフトウェアの半分に一致します。
rule Bad_TooBroad {
strings:
$ = "GetProcAddress"
condition:
$
}
悪い例 — ファイル名は偽装の定義そのものです。ファミリーはキャンペーンごとに名前を変えます。
rule Bad_Filename {
strings:
$ = "svch0st_updater.exe" nocase
condition:
$
}
良い例 — 作成者が簡単には変更できない複数の定数を使い、ファイルの種類とサイズで絞り込み、文字列が1つ変異してもルールが壊れないよう、一定数の一致を必須にします。
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-box、MD5/SHAの初期値、独自の鍵スケジュール)→ファミリー独自のミューテックス/パイプ/レジストリ名のテンプレート→設定マーカーと区切り文字→C2 URIテンプレート(/gate/、/panel/upload.php)→開発者が残した固有のエラー文字列やデバッグ文字列→flossで難読化を解除した後のスタック文字列です。ファイル名、コンパイル時のタイムスタンプ、キャンペーンのC2 IP、よくあるWin32 API名1つだけでは耐久性がありません。これらは偽装にすぎません。
誤検知をなくす
ルールはサンプルに一致しただけでは完成ではありません。無害なソフトウェアに一致しなくなって、初めて完成です。次の3つの手段を組み合わせます。
- 一致数 — ありふれた文字列1つだけではなく、
N of M個の文字列を必須にします。単独の文字列なら無害なソフトウェアに含まれることがありますが、ファミリー固有の定数が3つそろって現れることはありません。 - ファイル形式のアンカー — 条件の先頭に
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バイトしかない文字列(ワイルドカードが多すぎる16進パターン、2文字のテキスト文字列)は弱いアトムとなり、頻繁に一致するため、ほぼすべてのファイルで検証が発生し、パフォーマンスが低下します。すべての文字列に、ワイルドカードを含まない4バイト以上連続した部分を少なくとも1つ含めてください。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/スコアの各フィールドを統一すると、後続のツール(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 であり、検出ルールではありません。
- 「サンプルに一致した。これで出荷しよう。」 正常なファイルのコーパスでテストしていないルールは、爆発前の誤検知の嵐です。サンプルをスキャンしても、ほかに何がヒットするかは分かりません。
- 「特徴的な文字列がひとつあれば十分だ。」 文字列ひとつでは、変更ひとつで役に立たなくなります。また、一般的な文字列はあらゆる場所に出現します。ファミリー固有の定数を一定数要求してください。
- 「安全のため、関数全体にワイルドカードを使おう。」 16 進数にワイルドカードを多用すると 4 バイトのアトムが残らず、弱い基準でルールが一致し、スキャン性能が損なわれます。実際のバイト列は維持してください。
- 「ファイル名が特徴的だから、それを使おう。」 ファイル名は仮装のようなもので、攻撃者はコストをかけずにキャンペーンごとに変更します。コードと構造を基準にしてください。
- 「ファイルで動くなら、メモリでも動く。」 展開済みプロセスのイメージには、ファイルマジックやセクションを基準にする情報は存在しません。メモリには独自のルールが必要です。
- 「まず広く検出して、あとで調整しよう。」 本番環境でノイズの多い検出があるたびに、アナリストの信頼が損なわれ、本当の一致が埋もれます。導入後ではなく、導入前に調整してください。
外部ソースの読み込み
公開されているアドバイザリ、仕様書、ベンダーレポートを Markdown として取得します。
curl -sL "https://defuddle.md/<url>" # scheme in the path is optional
ページの定型部分を取り除くため、文章中心のページではトークン数を約 78%削減できます。また、要約ではなく全文を返すため、grep で検索でき、見つからなかった場合の結果も信頼できます。
用途に適さないケースが 3 つあります。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 のコーパス全体に適用し、関連サンプルを見つけます