
빠르고 메모리 효율적인 YARA 규칙 작성에 대한 가이드
효율적인 YARA 규칙을 작성하는 것은 빠르고 정확한 스캔 성능을 유지하는 데 필수적입니다. 이 가이드는 규칙을 최적화하고, 불필요한 계산을 줄이며, 일반적인 함정을 피하는 데 도움이 되는 핵심 원칙과 모범 사례를 제공합니다. Victor M. Alvarez, WXS 등 업계 전문가들의 통찰력과 YARA 커뮤니티의 기여가 포함되어 있습니다.
이 섹션에서는 YARA 성능 모범 사례에 대한 간결한 요약을 제공합니다. 자세한 설명과 예제는 아래 전체 가이드를 참조하세요.
"YARA를 두 단계 프로세스로 생각하세요: 첫째, 문자열에 나열된 모든 패턴을 검색하고, 둘째, 조건을 평가합니다. 잘 구성된 조건으로 잘못 선택된 문자열을 보완할 수는 없습니다."
— Wesley Shields
YARA는 파일을 스캔할 때 네 가지 주요 단계를 따릅니다:
YARA는 문자열을 먼저 검색하므로 문자열 선택이 규칙 효율성의 가장 중요한 단일 요소입니다.
✅ 문자열 모범 사례:
\x00\x00\x00\x00은 너무 자주 나타납니다.nocase는 신중하게 사용 – 검색 변형이 기하급수적으로 생성됩니다.YARA는 조건을 순차적으로 평가하며 첫 번째 실패 지점에서 중단합니다.
✅ 조건 모범 사례:
filesize < X)for all i in (1..filesize)는 비효율적)@) 사용⚠ 참고: 정규식 조건은 단락 평가되지 않으며 항상 마지막에 평가됩니다.
pe, elf 또는 magic 같은 모듈은 평가 전에 전체 파일을 파싱해야 하므로 스캔 시간이 증가합니다.
✅ 대안:
pe.is_pe 대신 uint16(0) == 0x5A4D를 사용하세요.과도한 매치는 스캔 속도를 저하시키고 "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 오토마톤에 공급하기 위해 검색 문자열에서 소위 원자(atom)를 찾습니다. 자세한 내용은 원자 장에서 설명하지만, 지금은 원자가 최대 4바이트 길이이며 YARA가 너무 많은 매치를 피하기 위해 원자를 매우 영리하게 선택한다는 것만 알면 충분합니다. 예제에서 YARA는 다음 4개의 원자를 선택할 수 있습니다:
<?phGETPOSTsser(assert에서 추출)여기서 스캔이 시작됩니다. 2~4단계는 모든 파일에 대해 실행됩니다. YARA는 Aho-Corasick 오토마톤이라는 프리픽스 트리를 사용하여 각 파일에서 위에 정의된 4개의 원자를 검색합니다. 일치하는 항목은 바이트코드 엔진으로 전달됩니다.
예를 들어 sser에서 매치가 발생하면 YARA는 앞에 a가 접두사로 붙었는지 확인한 후 t로 이어지는지 확인합니다. 이것이 사실이면 정규식 [\t ]{0,100}\(을 이어서 확인합니다. 이 영리한 접근 방식을 통해 YARA는 느린 정규식 엔진으로 전체 파일을 검색하는 대신 특정 부분만 골라 자세히 살펴봅니다.
모든 패턴 매칭이 완료된 후 조건이 확인됩니다.
YARA에는 예제 규칙의 CPU 집약적인 math.entropy 검사를 앞선 4가지 조건이 모두 충족된 경우에만 수행하는 또 다른 최적화 메커니즘이 있습니다. 자세한 내용은 조건 및 단락 평가 장에서 설명합니다.
조건이 충족되면 매치가 보고됩니다. 스캔은 2단계에서 다음 파일로 계속됩니다.
YARA는 문자열에서 "원자(atom)"라고 하는 최대 4바이트 길이의 짧은 하위 문자열을 추출합니다. 이러한 원자는 문자열 내의 어느 위치에서든 추출할 수 있으며, YARA는 파일을 스캔하는 동안 해당 원자를 검색하고, 원자 중 하나를 찾으면 문자열이 실제로 일치하는지 확인합니다.
예를 들어 다음 문자열을 고려해 보세요:
/abc.*cde/
=> 가능한 원자는 abc와 cde이며 둘 중 하나를 사용할 수 있습니다. 현재 abc 원자가 선호되는데, 둘의 품질이 동일하고 두 개 중 첫 번째이기 때문입니다.
/(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 }
=> 여기서 YARA는 01 02 03 04 원자를 사용합니다. 00 00 00 00은 너무 흔하기 때문입니다.
{ 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/
이 정규식에는 원자로 사용할 수 있는 고정 하위 문자열이 없으므로, 파일의 모든 오프셋에서 일치 여부를 평가해야 합니다.
또 다른 좋은 권장 사항은 반복 횟수가 너무 많은 루프를 피하는 것입니다. 특히 루프 내부의 문장이 너무 복잡한 경우입니다. 예를 들어:
strings:
$a = {00 00}
condition:
for all i in (1..#a) : (@a[i] < 10000)
이 규칙에는 두 가지 문제가 있습니다. 첫 번째는 문자열 $a가 너무 흔하다는 것이고, 두 번째는 $a가 너무 흔하기 때문에 #a가 너무 높아 수천 번 평가될 수 있다는 것입니다.
다음 조건도 반복 횟수가 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된 파일에서 균일한 콘텐츠로 나타날 가능성이 높습니다.
일부 문자열은 충분히 길지만 균일성(uniformity)이라는 다른 이유로 사용하지 말아야 합니다. 다음은 파일에서 너무 많은 매치를 유발할 수 있어 사용하지 말아야 할 문자열의 예입니다.
$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"가 가정된다는 점을 기억하세요. 가능한 조합은 다음과 같습니다:
낮음 - 원자 하나만 생성됨
$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
높음 - YARA가 선택한 4바이트에 대한 모든 대문자/소문자 조합이 원자로 생성됨
$s5 = "cmd.exe" nocase (all different cases, e.g. "Cmd.", "cMd.", "cmD." ..)
스크립팅 명령을 매치하려면 nocase를 사용하기 전에 해당 언어가 대소문자를 구분하지 않는지 확인하세요(예: php, Windows 배치). 한두 글자만 대소문자를 다르게 해야 한다면 정규식을 사용하는 것이 더 좋습니다. 예:
$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진수 문자열로 문제를 해결할 수 있다면 사용하지 마세요.
정규 표현식을 사용해야 한다면 탐욕적(greedy) .*와 게으른(reluctant) 수량자 .*?를 피하세요. 대신 .{1,30} 또는 .{1,3000}과 같은 정확한 숫자를 사용하세요. 또한 상한을 잊지 마세요(예: .{2,} 피하기).
수량자를 사용할 때 두 가지 상황이 발생할 수 있습니다:
정규 표현식의 시작 부분이 한 위치에 고정되어 있고 접미사만 변할 수 있는 경우, 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가 하나의 주소를 여러 번 매치하게 되며, 이는 이상적이지 않습니다. 이 경우 분석에 충분한 정보를 제공하는 합리적으로 작은 주소 하위 집합을 찾는 것이 좋습니다.
사용
/[-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가 하나의 인스턴스를 여러 번 매치하기 때문에 발생합니다.
"slowing down scanning"은 너무 짧은 원자를 생성하거나 전혀 생성하지 않는 문자열로 인해 발생합니다. 그 결과 YARA는 순진한(naïve) 패턴 매칭 알고리즘을 사용하게 되어 속도 저하가 발생합니다.
이러한 두 문제는 경우에 따라 다음 단계로 해결할 수 있습니다:
.*, .+, .*? 확인x{14,}와 같이 상한이 없는 수량자 확인참고로, 다음 장 조건 및 단락 평가에서 조건에 대한 몇 가지 팁이 언급됩니다. 그러나 이러한 변경만으로는 "too many matches" 및 "slowing down scanning" 오류를 해결할 수 없습니다.
"False"일 가능성이 가장 높은 요소를 먼저 배치하는 조건문을 작성하세요. 조건은 왼쪽에서 오른쪽으로 평가됩니다. 엔진이 규칙이 충족되지 않았음을 더 빨리 식별할수록 현재 규칙을 건너뛰고 다음 규칙을 더 빨리 평가할 수 있습니다. 이러한 조건문 순서 지정 방식으로 인한 속도 향상은 각 문장을 처리하는 데 필요한 CPU 사이클 차이에 따라 달라집니다. 모든 문장의 비용이 거의 동일하다면 순서를 변경해도 눈에 띄는 개선 효과가 없습니다. 문장 중 하나를 매우 빠르게 처리할 수 있다면, 첫 번째 문장이 FALSE인 경우 값비싼 문장 평가를 건너뛸 수 있도록 해당 문장을 먼저 배치하는 것이 좋습니다.
다음 문장에서 순서를 변경해도 큰 개선 효과는 없습니다:
$string1 and $string2 and uint16(0) == 0x5A4D
그러나 문장의 실행 시간이 매우 다른 경우, 단락 평가가 발동하도록 순서를 변경하면 스캔 속도가 크게 향상됩니다:
느림
// EXPENSIVE and CHEAP
math.entropy(0, filesize) > 7.0 and uint16(0) == 0x5A4D
빠름
// CHEAP and EXPENSIVE
uint16(0) == 0x5A4D and math.entropy(0, filesize) > 7.0
단락 평가는 값비싼 문장, 특히 "for" 문장의 최적화를 돕기 위해 도입되었습니다. 일부 사용자는 다음 예제와 같은 조건을 사용하고 있었습니다:
strings:
$mz = "MZ"
...
condition:
$mz at 0 and for all i in (1..filesize) : ( whatever )
filesize는 매우 큰 숫자가 될 수 있으므로 "whatever"가 여러 번 실행되어 실행 속도가 느려질 수 있습니다. 이제 단락 평가 덕분에 "for" 문장은 조건의 첫 번째 부분이 충족된 경우에만 실행되므로, 이 규칙은 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이 부족한 경우 워크플로우에서 스캔 직전에 불필요한 일부 메타데이터를 제거할 수 있습니다.