効率的なYARAルールを作成することは、高速で正確なスキャンパフォーマンスを維持するために不可欠です。このガイドでは、ルールを最適化し、不要な計算を減らし、よくある落とし穴を回避するための主要な原則とベストプラクティスを提供します。Victor M. Alvarez氏、WXS氏など業界の専門家の知見と、YARAコミュニティからの貢献が組み込まれています。
このセクションでは、YARAパフォーマンスのベストプラクティスの簡潔なまとめを提供します。詳細な説明と例については、以下の完全なガイドを参照してください。
「YARAは2段階のプロセスとして考えてください。まず、stringsにリストされたすべてのパターンを検索し、次にconditionを評価します。適切に作成されたconditionで、不適切に選択された文字列を補うことはできません。」
— Wesley Shields
YARAはファイルをスキャンする際、4つの主要なステップを実行します:
YARAは最初に文字列を検索するため、文字列の選択はルールの効率にとって最も重要な単一の要素です。
✅ 文字列に関するベストプラクティス:
\x00\x00\x00\x00は出現頻度が高すぎます。nocaseは慎重に使用する – 検索バリエーションが指数関数的に増加します。YARAはconditionを順番に評価し、最初の失敗で停止します。
✅ conditionに関するベストプラクティス:
filesize < X)– 高コストなconditionの前に置きます。for all i in (1..filesize)は非効率です)。@)を使用する。⚠ 注記: 正規表現のconditionはショートサーキットされず、常に最後に評価されます。
pe、elf、magicなどのモジュールは、評価の前にファイル全体を解析する必要があり、スキャン時間が増加します。
✅ 代替案:
pe.is_peの代わりにuint16(0) == 0x5A4Dを使用してPEファイルを識別します。過剰な一致はスキャンを遅くし、「too many matches」エラーを引き起こす可能性があります。
✅ 非効率な一致の修正:
.*、.+、{x,}を避けます。@herrcore が、このパフォーマンスガイドで説明されているトピックをカバーする役立つビデオチュートリアルを作成しました。
YARAのパフォーマンスをどこでどのように最適化できるかをより深く理解するには、スキャンプロセスを理解することが役立ちます。これは基本的に4つのステップに分かれており、次のサンプルルールを使って非常に簡略化して説明します:
import "math"
rule example_php_webshell_rule
{
meta:
description = "Just an example php webshell rule"
date = "2021/02/16"
strings:
$php_tag = "<?php"
$input1 = "GET"
$input2 = "POST"
$payload = /assert[\t ]{0,100}\(/
condition:
filesize < 20KB and
$php_tag and
$payload and
any of ( $input* ) and
math.entropy(500, filesize-500) >= 5
}
このステップは実際のスキャンの前に行われます。YARAは、Aho-Corasickオートマトンに供給するために、検索文字列内のいわゆるatoms(アトム)を探します。詳細はアトムの章で説明されていますが、ここでは、それらが最大4バイト長であり、YARAが過剰な一致を避けるためにそれらを非常に賢く選択するということだけを知っていれば十分です。この例では、YARAは次の4つのアトムを選択する可能性があります:
<?phGETPOSTsser(assertから)ここでスキャンが開始されます。ステップ2〜4はすべてのファイルに対して実行されます。YARAは、Aho-Corasickオートマトンと呼ばれるプレフィックスツリーを使用して、上記で定義した4つのアトムを各ファイル内で探します。一致したものはすべてバイトコードエンジンに引き渡されます。
例えばsserに一致した場合、YARAはその前にaが付き、続いてtが続くかをチェックします。それが真であれば、正規表現[\t ]{0,100}\(に従って処理を続けます。この賢いアプローチにより、YARAは低速な正規表現エンジンでファイル全体を処理することを避け、特定の部分だけを選んで詳しく調べます。
すべてのパターンマッチングが完了した後、conditionがチェックされます。
YARAにはもう1つの最適化メカニズムがあり、サンプルルールのCPU負荷の高いmath.entropyチェックは、その前の4つのconditionが満たされた場合にのみ実行されます。詳細はconditionとショートサーキット評価の章で説明されています。
conditionが満たされると、一致が報告されます。スキャンはステップ2で次のファイルに続行されます。
YARAは、文字列から最大4バイト長の短い部分文字列を抽出します。これらは「アトム」と呼ばれます。これらのアトムは文字列内の任意の場所から抽出でき、YARAはファイルのスキャン中にそれらのアトムを検索します。アトムのいずれかが見つかった場合、YARAは文字列が実際に一致するかどうかを検証します。
例えば、次の文字列を考えてみましょう:
/abc.*cde/
=> 可能なアトムはabcとcdeで、どちらか一方を使用できます。現在はabcアトムが優先されます。これは、両者の品質が同じで、abcが2つのうち最初に現れるためです。
/(one|two)three/
=> 可能なアトムはone、two、thre、hreeで、thre(またはhree)だけを検索することも、oneとtwoの両方を検索することもできます。threアトムが優先されます。これは、oneやtwo(これらはより短い)よりも潜在的な一致が少なく、eの重複を含まないためです(より一意な文字ほど良い)。
YARAは各文字列から最良のアトムを選択するために最善を尽くします。例えば:
{ 00 00 00 00 [1-4] 01 02 03 04 }
=> ここでは、00 00 00 00はあまりに一般的なため、YARAはアトム01 02 03 04を使用します。
{ 01 02 [1-4] 01 02 03 04 }
=> 01 02 03 04は01 02よりも長いため、優先されます。
つまり、重要な点は、文字列に優れたアトムが含まれている必要があるということです。
次の文字列は、短すぎるアトムまたは一般的すぎるアトムを含むため、悪い文字列です:
{00 00 00 00 [1-2] FF FF [1-2] 00 00 00 00}
{AB [1-2] 03 21 [1-2] 01 02}
/a.*b/
/a(c|d)/
最悪の文字列は、アトムをまったく含まない文字列です。例:
/\w.*\d/
/[0-9]+\n/
これらの正規表現は、アトムとして使用できる固定部分文字列をまったく含まないため、ファイルのすべてのオフセットで一致するかどうかを評価する必要があります。
もう1つの重要な推奨事項は、反復回数が多すぎるループを避けることです。特に、ループ内のステートメントが複雑すぎる場合は避けてください。例:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
このルールには2つの問題があります。1つ目は、文字列$aが一般的すぎることです。2つ目は、$aが一般的すぎるために#aが非常に大きくなり、何千回も評価される可能性があることです。
次のconditionも非効率です。反復回数がfilesizeに依存し、filesizeも非常に大きくなる可能性があるためです:
for all i in (1..filesize) : ($a at i)
Windowsプラットフォームでは利用できない「magic」モジュールの使用は避けてください。「magic」モジュールを使用するとスキャンは遅くなりますが、正確な一致を提供します。
カスタムGIFマジックヘッダー定義:
rule gif_1 {
condition:
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3961) or
(uint32be(0) == 0x47494638 and uint16be(4) == 0x3761)
}
「magic」モジュールを使用する場合:
import "magic"
rule gif_2 {
condition:
magic.mime_type() == "image/gif"
}
短すぎる文字列の定義は避けてください。4バイト未満の文字列は、多くのファイルに出現するか、XOR処理されたファイル内の均一なコンテンツとして出現する可能性が高いです。
中には十分に長いのに、別の理由(均一性)により使用すべきでない文字列もあります。ファイル内で「too many matches」を引き起こす可能性があるため使用すべきでない文字列の例をいくつか示します。
$s1 = "22222222222222222222222222222222222222222222222222222222222222"
$s2 = "\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20\x00\x20" // wide formatted spaces
エラーメッセージは次のようになります:
error scanning yara-killer.dat: string "$mz" in rule "shitty_mz" caused too many matches
文字列定義はできるだけ狭く記述するようにしてください。可能であれば「nocase」属性は避けてください。多くのアトムが生成されて検索されるためです(メモリ使用量の増加、反復回数の増加)。修飾子がない場合、デフォルトで「ascii」が想定されることを覚えておいてください。可能な組み合わせは次のとおりです:
LOW(低) - アトムが1つだけ生成されます
$s1 = "cmd.exe" // (ascii only)
$s2 = "cmd.exe" ascii // (ascii only, same as $s1)
$s3 = "cmd.exe" wide // (UTF-16 only)
$s4 = "cmd.exe" ascii wide // (both ascii and UTF-16) two atoms will be generated
$s5 = { 63 6d 64 2e 65 78 65 } // ascii char code in hex
HIGH(高) - YARAが選択した4バイトについて、大文字と小文字のすべての組み合わせがアトムとして生成されます
$s5 = "cmd.exe" nocase (all different cases, e.g. "Cmd.", "cMd.", "cmD." ..)
スクリプトコマンドをマッチさせたい場合は、nocaseを使用する前に、その言語がそもそも大文字と小文字を区別しないかどうか(例:php、Windowsバッチ)を確認してください。1文字か2文字だけの大文字小文字の違いだけが必要な場合は、正規表現を使用する方がよいでしょう。例:
$re = /[Pp]assword/
次のような代替(alternation)を扱うときは注意してください:
$re = /(a|b)cde/
$hex = {C7 C3 00 (31 | 33)}
これらの文字列は短いアトムを生成するため、スキャンを遅くする可能性があります。 バリアントの数が少ない場合は、文字列を個別に記述することをお勧めします:
$re1 = /acde/
$re2 = /bcde/
$hex1 = {C7 C3 00 31}
$hex2 = {C7 C3 00 33}
必要な場合にのみ正規表現を使用してください。正規表現の評価は、プレーンな文字列マッチングよりも本質的に遅く、かなりの量のメモリを消費します。ジャンプとワイルドカードを持つ16進文字列で問題を解決できる場合は、正規表現を使用しないでください。
正規表現を使用しなければならない場合は、貪欲な.*や、控えめな量指定子.*?も避けてください。代わりに、.{1,30}や.{1,3000}のような正確な数値を使用してください。また、上限を忘れないでください(例:.{2,}を避ける)。
量指定子を使用する場合、2つの状況が発生する可能性があります:
正規表現の先頭が1つの位置に固定され、変化し得るのが接尾辞だけの場合、YARAは可能な限り長い一致をマッチします。.*、.+、.{2,}のような場合、これは大きな文字列につながり、スキャン速度の低下という問題を引き起こす可能性があります。
正規表現の先頭として可能な箇所が複数ある場合、YARAはそれらすべてをマッチします。
$re1 = /Tom.{0,2}/ // will find Tomxx in "Tomxx"
$re2 = /.{0,2}Tom/ // will find Tom, xTom, xxTom in "xxTom"
短い一致の数は簡単に上限を超え、「too many matches」エラーを引き起こす可能性があります。
次の例は、電子メールアドレスの正規表現です。[-a-z0-9._%+]を量指定子と共に使用すると、YARAは1つのアドレスに複数回マッチしますが、これは理想的ではありません。この場合は、分析に十分な情報を提供する、適度に小さなアドレスのサブセットを見つけることをお勧めします。
使用
/[-a-z0-9._%+]@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
OR
/@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
使用を避ける
/[-a-z0-9._%+]*@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
/[-a-z0-9._%+]+@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
/[-a-z0-9._%+]{x,y}@[-a-z0-9.]{2,10}\.[a-z]{2,4}/
例えばexecの後に/bin/shが続くことを確認したい場合は、@記号によって提供されるオフセットを使用できます。以下は遅い正規表現バージョンです:
$ = /exec.*\/bin\/sh/
以下はより高速なオフセットを使用する方法です:
strings:
$exec = "exec"
$sh = "/bin/sh"
conditions:
$exec and $sh and
@exec < @sh
また、マッチングの進行においてアンカーとして機能する長い文字列シーケンスを含めるようにしてください。繰り返しますが、長ければ長いほど良いです。
悪い例
$s1 = /http:\/\/[.]*\.hta/ // greedy [.]*
より良い例
$s1 = /http:\/\/[a-z0-9\.\/]{3,70}\.hta/ // better, with an the upper bound
最良の例
$s1 = /mshta\.exe http:\/\/[a-z0-9\.\/]{3,70}\.hta/
「Too many matches」エラーは、入力にあまりに頻繁に出現するあまりに一般的な文字列によって引き起こされるか、YARAが1つのインスタンスに複数回マッチすることによって引き起こされます。
スキャン速度の低下は、短すぎるアトムを生成する文字列、またはアトムをまったく生成しない文字列によって引き起こされます。その結果、YARAは単純な(naïve)パターンマッチングアルゴリズムを使用することになり、それが速度低下の原因となります。
これらの問題はどちらも、場合によっては次の手順で修正できます:
.*、.+、.*?がないか確認するx{14,}など)がないか確認する次の章conditionとショートサーキット評価では、conditionに関するいくつかのヒントが紹介されています。ただし、それらの変更は「too many matches」エラーやスキャン速度低下エラーを解決しません。
「False」になる可能性が最も高い要素が最初に配置されるように、conditionステートメントを記述してください。conditionは左から右に評価されます。エンジンがルールの不成立を識別するのが早いほど、現在のルールをスキップして次のルールを評価するのも早くなります。このようにconditionステートメントを並べることで得られる速度向上は、各ステートメントの処理に必要なCPUサイクルの差に依存します。すべてのステートメントのコストがほぼ同等であれば、ステートメントの並べ替えによる顕著な改善はありません。いずれかのステートメントを非常に高速に処理できる場合は、それを最初に配置することをお勧めします。最初のステートメントがFALSEの場合に、高コストなステートメントの評価をスキップできるためです。
次のステートメントの順序を変更しても、大きな改善にはなりません:
$string1 and $string2 and uint16(0) == 0x5A4D
ただし、ステートメントの実行時間が大きく異なる場合は、ショートサーキットをトリガーするように並べ替えることで、スキャン速度が大幅に向上します:
SLOW(遅い)
// EXPENSIVE and CHEAP
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
FAST(速い)
// CHEAP and EXPENSIVE
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
ショートサーキット評価は、高コストなステートメント、特に「for」ステートメントの最適化を支援するために導入されました。次の例のようなconditionを使用している人もいました:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
filesizeは非常に大きな数になり得るため、「whatever」が何度も実行され、実行が遅くなる可能性があります。ショートサーキット評価により、「for」ステートメントはconditionの最初の部分が満たされた場合にのみ実行されるため、このルールが遅くなるのはMZファイルのみです。さらなる改善としては次のようなものがあります:
$mz at 0 and filesize < 100KB and for all i in (1..filesize) : ( whatever )
これにより、反復回数に上限が設定されます。
バージョン3.10以降、整数範囲ループも最適化されました:
for all i in (0..100): (false)
for any i in (0..100): (true)
Both of these loops will stop iterating after the first time through.
残念ながら、これは正規表現には機能しません。正規表現はすべて最初に文字列マッチングエンジンに供給されるためです。次の例は、filesizeが200バイト未満のファイルだけでなく、すべてのファイルの検索を遅くします:
strings:
$expensive_regex = /\$[a-z0-9_]+\(/ nocase
conditions:
filesize < 200 and
$expensive_regex
この「ショートサーキット」評価は、YARAバージョン3.4以降で適用されます。
メタデータセクション内のデータは、すべてYARAによってRAMに読み込まれます。(ルールに100,000個のハッシュを挿入し、その前後のYARAスキャンのRAM使用量を確認することで、これを簡単にテストできます。)もちろん、ルールからメタデータを恒久的に削除したいわけではないでしょう。しかし、RAMが不足している場合は、スキャンの直前のワークフローで不要な部分をいくつか削除してもよいでしょう。