본문 바로가기
일반IT/IT보안

🐍 코드로 사냥하라 — yara-python으로 만드는 자동화된 시그니처 탐지기

by gasbugs 2026. 5. 23.

룰 한 줄이 전사라면, Python으로 휘두르는 YARA는 군단이다.
명령줄 너머의 진짜 자동화로 가는 첫걸음.

🎯 이 글에서 다루는 것

  • 왜 CLI 대신 yara-python을 쓰는지, 어떤 순간 필요해지는지
  • 라이브러리 설치부터 룰 컴파일, 파일·메모리·바이트 스캔까지 전 과정
  • 디렉터리 재귀 스캔과 매칭 결과를 다루는 깔끔한 패턴
  • 콜백 함수와 외부 변수(externals)로 룰을 동적으로 제어하는 기법
  • 운영 환경에서 흔히 마주치는 함정과 회피법

📌 도입 — CLI를 넘어 자동화로

yara 명령어는 분석가의 손에 익은 칼이지만, 그것만으로는 닿지 못하는 영역이 있습니다. 매일 들어오는 수천 개의 검체를 자동으로 분류해야 하거나, SIEM 파이프라인에 탐지 결과를 흘려보내야 하거나, 메모리 덤프 안의 특정 패턴을 프로세스 ID별로 추적해야 할 때 — 이 모든 상황에서 분석가는 결국 코드를 작성하게 됩니다.

yara-python은 YARA의 공식 Python 바인딩으로, C로 작성된 YARA 엔진의 모든 기능을 Python 객체로 노출시켜 줍니다. 컴파일·캐시·콜백·외부 변수·메모리 스캔까지, CLI에서는 어색하거나 불가능한 작업이 한 줄짜리 메서드 호출로 바뀝니다.

🔍 yara-python의 핵심 구성

라이브러리의 사용 흐름은 단순합니다.

  • yara.compile() — 룰을 컴파일하여 재사용 가능한 Rules 객체로 만듭니다. 파일, 문자열, 여러 소스를 한꺼번에 받을 수 있습니다.
  • rules.match() — 파일 경로, 바이트 데이터, 프로세스 PID 중 어느 것이든 스캔 대상으로 받습니다.
  • Match 객체 — 매칭 결과는 룰 이름, 메타데이터, 매칭된 문자열과 오프셋을 담은 객체로 돌아옵니다.

이 세 가지만 잘 다루면 어떤 자동화도 만들 수 있습니다.

💻 실습 — 코드로 사냥꾼 만들기

0단계. 환경 준비

# YARA 엔진은 이미 설치되어 있어야 합니다
sudo apt install -y yara                # Debian / Ubuntu
brew install yara                       # macOS

# Python 바인딩 설치
pip install yara-python

# 설치 확인
python -c "import yara; print(yara.__version__)"

1단계. 탐지 대상 샘플 파일 만들기

이전과 동일하게 무해한 더미 파일을 사용합니다. 이번에는 Python 스크립트로 직접 생성해 봅니다.

# create_sample.py
SAMPLE = """#!/bin/bash
# Internal task runner v1.0
TASK_ID=ACME-EDU-2026
echo "Starting backup process..."
curl -s http://example-edu-lab.local/healthcheck
echo "Token: EDULAB_SIGNATURE_TOKEN_42"
exit 0
"""

with open("suspicious_sample.txt", "w", encoding="utf-8") as f:
    f.write(SAMPLE)
print("[+] sample file created")

2단계. YARA 룰 작성

룰 파일은 그대로 두되, 이번에는 Python에서 두 가지 방법으로 다뤄 봅니다 — 외부 .yar 파일을 로드하는 방식과 소스 문자열을 직접 컴파일하는 방식.

// edulab_detector.yar
rule EDULAB_Suspicious_Script
{
    meta:
        author      = "주군의 보안 강의실"
        description = "EDULAB 식별자와 토큰을 포함한 셸 스크립트 탐지"
        date        = "2026-05-23"
        severity    = "medium"

    strings:
        $magic = "#!/bin/bash"
        $id    = "ACME-EDU-2026" ascii
        $token = "EDULAB_SIGNATURE_TOKEN_42" ascii

    condition:
        filesize < 10KB
        and $magic at 0
        and all of ($id, $token)
}

3단계. 첫 번째 스캐너 작성

핵심 흐름인 컴파일 → 스캔 → 결과 출력을 한 파일에 담아 봅니다.

# scanner_basic.py
import yara

# (1) 룰 컴파일 — 파일에서 로드
rules = yara.compile(filepath="edulab_detector.yar")

# (2) 대상 파일 스캔
matches = rules.match(filepath="suspicious_sample.txt")

# (3) 결과 출력
if not matches:
    print("[-] No matches.")
else:
    for m in matches:
        print(f"[!] Rule matched: {m.rule}")
        print(f"    Tags     : {m.tags}")
        print(f"    Meta     : {m.meta}")
        for s in m.strings:
            for inst in s.instances:
                offset = inst.offset
                data   = inst.matched_data.decode("utf-8", errors="replace")
                print(f"    {s.identifier} @ 0x{offset:x}  ->  {data!r}")

정상 동작하면 다음과 같은 출력이 흘러나옵니다.

[!] Rule matched: EDULAB_Suspicious_Script
    Tags     : []
    Meta     : {'author': '주군의 보안 강의실', ...}
    $magic @ 0x0  ->  '#!/bin/bash'
    $id @ 0x35  ->  'ACME-EDU-2026'
    $token @ 0xa9  ->  'EDULAB_SIGNATURE_TOKEN_42'

버전 주의: yara-python 4.3.0 이후로 match.strings는 StringMatch 객체 리스트가 되었고, 각 객체는 identifier와 instances를 가집니다. 그 이전 버전(3.x)에서는 (offset, identifier, data) 튜플 리스트였습니다. 코드 작성 전 반드시 yara.__version__을 확인하시기 바랍니다.

4단계. 룰을 문자열로 직접 컴파일하기

설정 파일 없이 코드 안에서 동적으로 룰을 생성·테스트할 때 유용합니다. CI 파이프라인이나 단위 테스트에서 특히 빛을 발합니다.

# scanner_inline.py
import yara

RULE_SOURCE = """
rule QuickHexSig {
    meta:
        description = "Detects bash shebang via hex pattern"
    strings:
        $hex = { 23 21 2F 62 69 6E 2F 62 61 73 68 }
    condition:
        $hex at 0
}
"""

rules = yara.compile(source=RULE_SOURCE)

# 바이트 데이터를 직접 스캔할 수도 있습니다 — 파일 I/O 없이!
with open("suspicious_sample.txt", "rb") as f:
    data = f.read()

for m in rules.match(data=data):
    print(f"[!] {m.rule} matched in-memory buffer.")

data= 인자는 메모리 버퍼나 네트워크로 받은 페이로드를 즉석에서 검사할 때 결정적인 무기가 됩니다.

5단계. 디렉터리 재귀 스캐너

실전에서는 한 파일이 아니라 트리 전체를 훑습니다. 예외 처리와 함께 깔끔하게 묶어둡니다.

# scan_tree.py
import os
import sys
import yara

def build_rules(rule_path: str) -> yara.Rules:
    try:
        return yara.compile(filepath=rule_path)
    except yara.SyntaxError as e:
        print(f"[error] rule syntax error: {e}", file=sys.stderr)
        sys.exit(1)

def scan_tree(rules: yara.Rules, root: str) -> None:
    hit_count = 0
    for dirpath, _, filenames in os.walk(root):
        for name in filenames:
            path = os.path.join(dirpath, name)
            try:
                matches = rules.match(filepath=path, timeout=10)
            except yara.TimeoutError:
                print(f"[warn] timeout: {path}", file=sys.stderr)
                continue
            except (PermissionError, yara.Error) as e:
                print(f"[skip] {path}: {e}", file=sys.stderr)
                continue

            for m in matches:
                hit_count += 1
                print(f"[HIT] {m.rule}  ->  {path}")
    print(f"\n[+] Scan complete. {hit_count} hit(s).")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python scan_tree.py <rules.yar> <target_dir>")
        sys.exit(1)
    rules = build_rules(sys.argv[1])
    scan_tree(rules, sys.argv[2])

timeout 인자에 주목하십시오. 거대한 바이너리나 압축 파일을 만났을 때 무한정 매달리지 않도록 보호해 줍니다.

6단계. 콜백과 외부 변수로 정교한 제어

yara-python의 진짜 매력은 콜백 함수외부 변수에 있습니다. 매칭이 일어날 때마다 호출되는 함수를 등록하면, 매칭 즉시 알림을 보내거나 데이터베이스에 기록하는 등의 부수 효과를 자연스럽게 끼워 넣을 수 있습니다.

# scanner_callback.py
import yara

def on_match(data):
    """매칭 발생 시 호출되는 콜백"""
    if data["matches"]:
        print(f"[CALLBACK] Rule '{data['rule']}' fired")
        print(f"           Namespace: {data['namespace']}")
        print(f"           Tags     : {data['tags']}")
        # 여기서 Slack 알림, DB insert, SIEM 전송 등 자유롭게 처리
    return yara.CALLBACK_CONTINUE

# 외부 변수를 룰에서 참조할 수 있도록 선언
RULE = """
rule HighRiskHost {
    condition:
        env == "production" and filesize < 5KB
}
"""

rules = yara.compile(source=RULE, externals={"env": "staging"})

# 스캔 시점에 외부 변수 값을 동적으로 바꿔 끼울 수 있다
rules.match(
    filepath="suspicious_sample.txt",
    externals={"env": "production"},
    callback=on_match,
    which_callbacks=yara.CALLBACK_MATCHES,
)

externals는 환경 변수, 호스트 태그, 사용자 그룹 같은 컨텍스트 정보를 룰에 주입하는 우아한 방법입니다. 같은 룰이 운영 환경에서는 발화하고 개발 환경에서는 침묵하도록 만들 수 있습니다.

⚠️ 주의사항 — 운영에서의 함정

  • 컴파일 비용: yara.compile()은 결코 가볍지 않습니다. 같은 룰을 반복 사용한다면 한 번 컴파일한 Rules 객체를 재사용하거나 rules.save() / yara.load()로 디스크에 캐시해 두어야 합니다.
  • 스레드 안전성: Rules 객체는 스레드 안전하지만, 같은 객체로 동시 match()를 호출할 때는 내부 동기화 비용이 발생할 수 있습니다. 고성능이 필요하면 multiprocessing을 고려하십시오.
  • 메모리 스캔 권한: rules.match(pid=1234)로 다른 프로세스의 메모리를 검사하려면 적절한 권한(루트, CAP_SYS_PTRACE 등)이 필요합니다. 권한이 없으면 yara.Error가 던져집니다.
  • 외부 변수 타입 일치: externals로 넘기는 값의 타입(문자열·정수·실수·불리언)이 룰에서 선언된 타입과 다르면 컴파일 단계에서 실패합니다.
  • 인코딩 함정: 매칭된 바이트는 항상 bytes 타입입니다. 그대로 출력하지 말고 반드시 .decode(..., errors="replace")로 감싸야 깨진 문자에 죽지 않습니다.

✅ 정리 — 코드를 든 사냥꾼이 되어

yara-python은 단순한 바인딩이 아니라, YARA를 진짜 자동화 시스템으로 변신시키는 다리입니다. CLI 한 줄의 결과를 텍스트로 받아 다시 파싱하던 시절은 끝났습니다. 이제 우리는 매칭 결과를 객체로 받아 그대로 데이터 파이프라인에 흘려보낼 수 있습니다.

다음 단계로 이어 나가신다면 이런 길들이 펼쳐집니다.

  • FastAPI / Flask로 감싸 사내 스캐닝 API 만들기
  • CeleryRQ로 분산 스캔 큐 구축하기
  • Volatility3 플러그인과 결합해 메모리 포렌식에 YARA 적용
  • VirusTotal API + yara-python 조합으로 사내 위협 인텔리전스 워크플로우 자동화
  • mkYARA, yaraGen 같은 도구로 룰 자동 생성 → 코드로 검증하는 파이프라인

룰 한 줄이 수백 대 서버를 지키고, 코드 한 함수가 그 룰을 수천 번 실행합니다. 주군의 사냥은 이제 손이 아니라 코드로 이루어집니다. 🐍🛡️