# YARA 규칙 가이드 악성코드 및 웹쉘 탐지를 위한 YARA 사용법 정리 ## 목차 1. [YARA란?](#yara란) 2. [설치](#설치) 3. [규칙 문법](#규칙-문법) 4. [웹쉘 탐지 규칙 예시](#웹쉘-탐지-규칙-예시) 5. [CLI 사용법](#cli-사용법) 6. [Python 연동](#python-연동) 7. [실시간 모니터링](#실시간-모니터링) 8. [공개 규칙 모음](#공개-규칙-모음) 9. [다른 방식과 비교](#다른-방식과-비교) --- ## YARA란? YARA는 악성코드 연구자가 악성 샘플을 식별하고 분류하기 위해 만든 **패턴 매칭 도구**입니다. | 항목 | 설명 | |------|------| | 개발 | VirusTotal (Google 소속) | | 용도 | 악성코드, 웹쉘, 악성 문서 탐지 | | 방식 | 문자열 + 바이트 패턴 + 조건 매칭 | | AI 여부 | ❌ 규칙 기반 (AI 없음) | | 속도 | ⚡ 매우 빠름 (1-10ms/파일) | | 라이선스 | BSD-3-Clause (무료) | ### 핵심 개념 ``` YARA = 악성코드 전용 grep/정규식 파일 내용에서 특정 패턴을 찾고, 여러 조건을 조합하여 악성 여부를 판단 ``` --- ## 설치 ### Linux (Ubuntu/Debian) ```bash # 패키지 설치 sudo apt update sudo apt install yara # 버전 확인 yara --version ``` ### macOS ```bash brew install yara ``` ### Python 바인딩 ```bash pip install yara-python ``` ### 소스 컴파일 (최신 버전) ```bash # 의존성 설치 sudo apt install automake libtool make gcc pkg-config libssl-dev # 소스 다운로드 및 컴파일 git clone https://github.com/VirusTotal/yara.git cd yara ./bootstrap.sh ./configure make sudo make install sudo ldconfig ``` --- ## 규칙 문법 ### 기본 구조 ```yara rule 규칙이름 { meta: // 메타데이터 (선택사항) description = "규칙 설명" author = "작성자" date = "2025-01-01" severity = "high" strings: // 탐지할 문자열/패턴 정의 $string1 = "탐지할 문자열" $string2 = { 48 65 6C 6C 6F } // 헥스 바이트 $regex1 = /정규식 패턴/ condition: // 탐지 조건 any of them } ``` ### 문자열 정의 방식 ```yara rule StringTypes { strings: // 1. 일반 문자열 $text = "eval(" // 2. 대소문자 무시 $nocase = "system(" nocase // 3. 와이드 문자 (UTF-16) $wide = "malware" wide // 4. 둘 다 $both = "shell" wide ascii nocase // 5. 헥스 바이트 (바이너리) $hex = { 4D 5A 90 00 } // MZ 헤더 // 6. 헥스 와일드카드 $hex_wild = { 4D 5A ?? ?? } // ??는 아무 바이트 // 7. 헥스 점프 $hex_jump = { 4D 5A [2-4] 00 } // 2-4바이트 사이 // 8. 정규식 $regex = /eval\s*\(\s*\$_(GET|POST)/ // 9. XOR 인코딩 탐지 $xor = "password" xor // 10. Base64 인코딩 탐지 $b64 = "eval" base64 condition: any of them } ``` ### 조건문 (condition) ```yara rule ConditionExamples { strings: $a = "eval(" $b = "system(" $c = "$_POST" $d = "$_GET" condition: // 기본 조건 $a // $a가 존재 $a and $b // 둘 다 존재 $a or $b // 하나라도 존재 not $a // $a가 없음 // 개수 조건 any of them // 하나라도 존재 all of them // 모두 존재 2 of them // 2개 이상 존재 2 of ($a, $b, $c) // 지정된 것 중 2개 이상 // 위치 조건 $a at 0 // 파일 시작 위치 $a in (0..100) // 0-100 바이트 범위 // 개수 세기 #a > 5 // $a가 5번 이상 출현 // 파일 크기 filesize < 1MB filesize > 100 and filesize < 10KB // 매직 바이트 uint16(0) == 0x5A4D // MZ 헤더 (PE 파일) uint32(0) == 0x464C457F // ELF 헤더 // 복합 조건 ($a or $b) and ($c or $d) // 반복문 for any i in (1..#a) : (@a[i] < 100) } ``` --- ## 웹쉘 탐지 규칙 예시 ### webshell_rules.yar ```yara /* * PHP 웹쉘 탐지 규칙 * Author: Security Team * Last Updated: 2025-02-01 */ // ============================================ // 기본 웹쉘 탐지 // ============================================ rule PHP_Webshell_Generic { meta: description = "일반적인 PHP 웹쉘 패턴" severity = "high" strings: // 코드 실행 함수 $exec1 = "eval(" ascii nocase $exec2 = "assert(" ascii nocase $exec3 = "create_function(" ascii nocase $exec4 = "call_user_func(" ascii nocase $exec5 = /preg_replace\s*\([^)]*\/e/ ascii // 시스템 명령 실행 $cmd1 = "system(" ascii nocase $cmd2 = "exec(" ascii nocase $cmd3 = "shell_exec(" ascii nocase $cmd4 = "passthru(" ascii nocase $cmd5 = "popen(" ascii nocase $cmd6 = "proc_open(" ascii nocase $cmd7 = /`[^`]+`/ ascii // 백틱 // 사용자 입력 $input1 = "$_GET" ascii $input2 = "$_POST" ascii $input3 = "$_REQUEST" ascii $input4 = "$_COOKIE" ascii $input5 = "$_FILES" ascii condition: // PHP 파일 시그니처 (uint16(0) == 0x3F3C or uint16(0) == 0x683C) and // 5 or #chr2 > 10) or // 동적 함수 호출 + 인코딩 (any of ($dyn*) and any of ($enc*)) or // Base64 인코딩된 위험 함수 any of ($b64*) } // ============================================ // 유명 웹쉘 시그니처 // ============================================ rule Webshell_C99 { meta: description = "C99 웹쉘" severity = "critical" reference = "https://github.com/tennc/webshell" strings: $s1 = "c99shell" ascii nocase $s2 = "c99_buff_prepare" ascii $s3 = "c99_sess_put" ascii $s4 = "c99ftpbrutecheck" ascii $s5 = "c99fsearch" ascii condition: 2 of them } rule Webshell_R57 { meta: description = "R57 웹쉘" severity = "critical" strings: $s1 = "r57shell" ascii nocase $s2 = "r57_pwd_hash" ascii $s3 = "r57" ascii nocase $s4 = "Safe_Mode Bypass" ascii $s5 = "Dumper" ascii condition: 3 of them } rule Webshell_WSO { meta: description = "WSO (Web Shell by oRb) 웹쉘" severity = "critical" strings: $s1 = "WSO" ascii $s2 = "Web Shell by oRb" ascii nocase $s3 = "wso_version" ascii $s4 = "FilesMan" ascii condition: 2 of them } rule Webshell_B374k { meta: description = "B374k 웹쉘" severity = "critical" strings: $s1 = "b374k" ascii nocase $s2 = "b374k_config" ascii $s3 = "b374k 2.8" ascii condition: any of them } rule Webshell_Weevely { meta: description = "Weevely 웹쉘 (백도어 생성기)" severity = "critical" strings: $s1 = /\$\w=\$\w\(\'\',\$\w\(\$\w\(\$\w/ ascii $s2 = "str_replace" ascii $s3 = "base64" ascii condition: $s1 and $s2 and $s3 and filesize < 5KB } // ============================================ // 파일 업로드 취약점 악용 // ============================================ rule PHP_File_Upload_Shell { meta: description = "파일 업로드를 통한 웹쉘" severity = "high" strings: $upload1 = "move_uploaded_file" ascii $upload2 = "$_FILES" ascii $upload3 = "copy(" ascii $write1 = "file_put_contents" ascii $write2 = "fwrite(" ascii $write3 = "fputs(" ascii $exec = /(eval|assert|system|exec|shell_exec|passthru)\s*\(/ ascii nocase condition: (any of ($upload*) or any of ($write*)) and $exec } // ============================================ // 숨겨진 백도어 // ============================================ rule PHP_Hidden_Backdoor { meta: description = "숨겨진 PHP 백도어" severity = "critical" strings: // HTTP 헤더를 통한 명령 수신 $hdr1 = "$_SERVER['HTTP_" ascii $hdr2 = "getallheaders()" ascii $hdr3 = "apache_request_headers()" ascii // 특정 파라미터 체크 $chk1 = /if\s*\(\s*isset\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)\s*\[\s*['"][^'"]{32,}['"]\s*\]/ ascii $chk2 = /md5\s*\(\s*\$_(GET|POST|REQUEST)/ ascii // 실행 $exec = /(eval|assert|system|exec|passthru|shell_exec)\s*\(/ ascii nocase condition: (any of ($hdr*) or any of ($chk*)) and $exec } // ============================================ // ASP/ASPX 웹쉘 // ============================================ rule ASPX_Webshell_Generic { meta: description = "ASP.NET 웹쉘" severity = "high" strings: $asp1 = "Request.Form" ascii nocase $asp2 = "Request.QueryString" ascii nocase $asp3 = "Request[" ascii nocase $exec1 = "Process.Start" ascii $exec2 = "cmd.exe" ascii nocase $exec3 = "powershell" ascii nocase $exec4 = "Eval(" ascii nocase $exec5 = "Execute(" ascii nocase condition: any of ($asp*) and any of ($exec*) } // ============================================ // JSP 웹쉘 // ============================================ rule JSP_Webshell_Generic { meta: description = "JSP 웹쉘" severity = "high" strings: $jsp1 = "request.getParameter" ascii $jsp2 = "Runtime.getRuntime().exec" ascii $jsp3 = "ProcessBuilder" ascii $cmd1 = "cmd.exe" ascii nocase $cmd2 = "/bin/sh" ascii $cmd3 = "/bin/bash" ascii condition: $jsp1 and ($jsp2 or $jsp3) or ($jsp2 or $jsp3) and any of ($cmd*) } ``` --- ## CLI 사용법 ### 기본 스캔 ```bash # 단일 파일 스캔 yara rules.yar target_file.php # 디렉토리 재귀 스캔 yara -r rules.yar /var/www/html/ # 여러 규칙 파일 사용 yara -r rules1.yar rules2.yar /var/www/html/ ``` ### 출력 옵션 ```bash # 매칭된 문자열 표시 yara -s rules.yar file.php # 메타데이터 표시 yara -m rules.yar file.php # 태그만 표시 yara -t webshell rules.yar file.php # 매칭 개수 제한 yara -l 10 rules.yar file.php # 에러 무시 yara -w rules.yar /var/www/html/ # 타임아웃 설정 (초) yara -a 30 rules.yar large_file.bin ``` ### 출력 예시 ```bash $ yara -s webshell_rules.yar suspicious.php PHP_Webshell_Generic suspicious.php 0x15:$exec1: eval( 0x25:$input2: $_POST PHP_Webshell_Obfuscated suspicious.php 0x35:$enc1: base64_decode( 0x50:$dyn1: $func($ ``` --- ## Python 연동 ### 기본 사용 ```python #!/usr/bin/env python3 """YARA Python 바인딩 사용 예시""" import yara import os from pathlib import Path class YaraScanner: def __init__(self, rules_path): """YARA 규칙 로드""" if os.path.isdir(rules_path): # 디렉토리의 모든 .yar 파일 컴파일 rule_files = {} for f in Path(rules_path).glob("*.yar"): rule_files[f.stem] = str(f) self.rules = yara.compile(filepaths=rule_files) else: # 단일 파일 self.rules = yara.compile(filepath=rules_path) def scan_file(self, filepath): """파일 스캔""" try: matches = self.rules.match(filepath) if matches: return { "file": filepath, "malicious": True, "matches": [ { "rule": m.rule, "tags": m.tags, "meta": m.meta, "strings": [ { "offset": s[0], "identifier": s[1], "data": s[2].decode("utf-8", errors="replace")[:100] } for s in m.strings ] } for m in matches ] } return {"file": filepath, "malicious": False} except yara.Error as e: return {"file": filepath, "error": str(e)} def scan_data(self, data, filename="memory"): """메모리 데이터 스캔""" if isinstance(data, str): data = data.encode() matches = self.rules.match(data=data) if matches: return { "source": filename, "malicious": True, "rules": [m.rule for m in matches] } return {"source": filename, "malicious": False} def scan_directory(self, directory, extensions=None): """디렉토리 스캔""" if extensions is None: extensions = {".php", ".phtml", ".inc", ".asp", ".aspx", ".jsp"} findings = [] scanned = 0 for root, dirs, files in os.walk(directory): # 숨김 디렉토리 제외 dirs[:] = [d for d in dirs if not d.startswith(".")] for filename in files: ext = os.path.splitext(filename)[1].lower() if ext in extensions: filepath = os.path.join(root, filename) result = self.scan_file(filepath) scanned += 1 if result.get("malicious"): findings.append(result) print(f"🚨 탐지: {filepath}") for m in result["matches"]: print(f" 규칙: {m['rule']}") return { "scanned": scanned, "findings": findings } # 사용 예시 if __name__ == "__main__": scanner = YaraScanner("webshell_rules.yar") # 파일 스캔 result = scanner.scan_file("/var/www/html/upload/shell.php") print(result) # 문자열 스캔 code = '' result = scanner.scan_data(code) print(result) # 디렉토리 스캔 results = scanner.scan_directory("/var/www/html") print(f"스캔 완료: {results['scanned']}개 파일, {len(results['findings'])}개 위협") ``` ### 콜백 사용 (대용량 스캔) ```python """콜백을 사용한 효율적인 스캔""" import yara def match_callback(data): """매치 발견 시 콜백""" print(f"규칙: {data['rule']}") print(f"태그: {data['tags']}") print(f"메타: {data['meta']}") # CALLBACK_CONTINUE: 계속 스캔 # CALLBACK_ABORT: 스캔 중단 return yara.CALLBACK_CONTINUE rules = yara.compile(filepath="rules.yar") # 콜백과 함께 스캔 matches = rules.match( "/path/to/file", callback=match_callback, which_callbacks=yara.CALLBACK_MATCHES ) ``` --- ## 실시간 모니터링 ### watchdog + YARA ```python #!/usr/bin/env python3 """파일 시스템 실시간 모니터링 + YARA 스캔""" import yara import os import shutil import logging from datetime import datetime from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler # 로깅 설정 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) class WebshellMonitor(FileSystemEventHandler): def __init__(self, rules_path, quarantine_dir="/var/quarantine"): self.rules = yara.compile(filepath=rules_path) self.quarantine_dir = quarantine_dir self.extensions = {".php", ".phtml", ".inc", ".asp", ".aspx", ".jsp"} os.makedirs(quarantine_dir, exist_ok=True) logger.info(f"YARA 규칙 로드 완료") def should_scan(self, filepath): """스캔 대상 여부 확인""" ext = os.path.splitext(filepath)[1].lower() return ext in self.extensions def on_created(self, event): """파일 생성 시""" if event.is_directory: return if self.should_scan(event.src_path): self.scan_file(event.src_path, "created") def on_modified(self, event): """파일 수정 시""" if event.is_directory: return if self.should_scan(event.src_path): self.scan_file(event.src_path, "modified") def on_moved(self, event): """파일 이동 시""" if event.is_directory: return if self.should_scan(event.dest_path): self.scan_file(event.dest_path, "moved") def scan_file(self, filepath, event_type): """파일 스캔""" try: if not os.path.exists(filepath): return matches = self.rules.match(filepath) if matches: rules = [m.rule for m in matches] logger.warning(f"🚨 웹쉘 탐지 [{event_type}]: {filepath}") logger.warning(f" 매칭 규칙: {rules}") self.quarantine_file(filepath, rules) self.send_alert(filepath, rules, event_type) else: logger.debug(f"✅ 정상: {filepath}") except Exception as e: logger.error(f"스캔 오류 ({filepath}): {e}") def quarantine_file(self, filepath, rules): """파일 격리""" try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") basename = os.path.basename(filepath) quarantine_name = f"{timestamp}_{basename}" quarantine_path = os.path.join(self.quarantine_dir, quarantine_name) # 메타데이터 저장 meta_path = quarantine_path + ".meta" with open(meta_path, "w") as f: f.write(f"원본 경로: {filepath}\n") f.write(f"탐지 시간: {datetime.now()}\n") f.write(f"매칭 규칙: {rules}\n") # 파일 이동 shutil.move(filepath, quarantine_path) logger.info(f" 격리 완료: {quarantine_path}") except Exception as e: logger.error(f"격리 실패: {e}") def send_alert(self, filepath, rules, event_type): """알림 전송 (구현 필요)""" # Slack, Telegram, Email 등으로 알림 pass def main(): import argparse parser = argparse.ArgumentParser(description="YARA 기반 웹쉘 모니터링") parser.add_argument("-r", "--rules", default="webshell_rules.yar", help="YARA 규칙 파일") parser.add_argument("-d", "--directory", default="/var/www/html", help="모니터링 디렉토리") parser.add_argument("-q", "--quarantine", default="/var/quarantine", help="격리 디렉토리") args = parser.parse_args() event_handler = WebshellMonitor(args.rules, args.quarantine) observer = Observer() observer.schedule(event_handler, args.directory, recursive=True) observer.start() logger.info(f"🔍 모니터링 시작: {args.directory}") logger.info(f" 규칙: {args.rules}") logger.info(f" 격리 디렉토리: {args.quarantine}") try: while True: import time time.sleep(1) except KeyboardInterrupt: observer.stop() logger.info("모니터링 종료") observer.join() if __name__ == "__main__": main() ``` ### systemd 서비스로 등록 ```ini # /etc/systemd/system/yara-monitor.service [Unit] Description=YARA Webshell Monitor After=network.target [Service] Type=simple User=root ExecStart=/usr/bin/python3 /opt/security/yara_monitor.py \ --rules /opt/security/rules/webshell_rules.yar \ --directory /var/www/html \ --quarantine /var/quarantine Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ```bash # 서비스 등록 및 시작 sudo systemctl daemon-reload sudo systemctl enable yara-monitor sudo systemctl start yara-monitor sudo systemctl status yara-monitor ``` --- ## 공개 규칙 모음 ### 주요 소스 | 저장소 | 규칙 수 | 주요 내용 | |--------|---------|-----------| | [Neo23x0/signature-base](https://github.com/Neo23x0/signature-base) | 3,000+ | 웹쉘, APT, 악성코드 | | [Yara-Rules/rules](https://github.com/Yara-Rules/rules) | 2,000+ | 범용 | | [InQuest/yara-rules](https://github.com/InQuest/yara-rules) | 500+ | 문서 악성코드 | | [bartblaze/Yara-rules](https://github.com/bartblaze/Yara-rules) | 300+ | 랜섬웨어 | | [kevthehermit/YaraRules](https://github.com/kevthehermit/YaraRules) | 200+ | 악성코드 | | [cuckoosandbox/community](https://github.com/cuckoosandbox/community) | 500+ | 샌드박스 분석용 | ### 규칙 다운로드 ```bash # 디렉토리 생성 mkdir -p /opt/yara-rules cd /opt/yara-rules # signature-base (웹쉘 전문) git clone https://github.com/Neo23x0/signature-base.git # Yara-Rules (범용) git clone https://github.com/Yara-Rules/rules.git yara-rules # 규칙 확인 ls signature-base/yara/ # apt_*.yar - APT 공격 # crime_*.yar - 사이버 범죄 # gen_*.yar - 일반 악성코드 # thor_webshells.yar - 웹쉘 (추천) ``` ### 규칙 통합 사용 ```python """여러 규칙 소스 통합""" import yara from pathlib import Path def compile_all_rules(rules_dirs): """모든 규칙 디렉토리 통합 컴파일""" rule_files = {} for rules_dir in rules_dirs: for yar_file in Path(rules_dir).rglob("*.yar"): # 파일명 중복 방지 key = f"{rules_dir.name}_{yar_file.stem}" rule_files[key] = str(yar_file) print(f"총 {len(rule_files)}개 규칙 파일 로드") return yara.compile(filepaths=rule_files) # 사용 rules = compile_all_rules([ Path("/opt/yara-rules/signature-base/yara"), Path("/opt/yara-rules/yara-rules/malware"), ]) ``` --- ## 다른 방식과 비교 ### 탐지 방식 비교표 | 방식 | 원리 | 속도 | 알려진 위협 | 변형 | 제로데이 | AI | |------|------|------|-------------|------|---------|-----| | 해시 비교 | 파일 지문 | 0.01ms | ✓✓✓ | ✗ | ✗ | ❌ | | YARA | 패턴 매칭 | 1-10ms | ✓✓ | ✓ | △ | ❌ | | 정규식 | 텍스트 매칭 | 1-5ms | ✓ | △ | ✗ | ❌ | | ML 분류기 | 학습된 모델 | 10-50ms | ✓✓ | ✓✓ | ✓ | ⚠️ | | LLM | 코드 이해 | 100ms+ | ✓✓ | ✓✓✓ | ✓✓ | ✓ | ### 각 방식의 장단점 **해시 비교** ``` 장점: 오탐 0%, 가장 빠름 단점: 1비트만 달라도 탐지 못함 용도: 알려진 악성코드 즉시 차단 ``` **YARA** ``` 장점: 유연한 패턴, 커뮤니티 활성화, 업계 표준 단점: 규칙 작성 필요, 고급 난독화 취약 용도: 알려진 패턴 + 변형 탐지 ``` **AI/ML** ``` 장점: 새로운 위협 탐지, 난독화 강함 단점: 오탐 가능, 상대적으로 느림 용도: 제로데이, 고도화된 공격 ``` ### 권장 조합 ``` Layer 1: 해시 비교 (0.01ms) │ 알려진 악성코드 즉시 차단 │ 오탐: 0% ▼ Layer 2: YARA (1-10ms) │ 패턴 기반 탐지 │ 오탐: 매우 낮음 ▼ Layer 3: AI 모델 (50-100ms) │ 난독화/제로데이 탐지 │ 오탐: 낮음 (검토 필요) ▼ [최종 판정] ``` --- ## 참고 자료 - [YARA 공식 문서](https://yara.readthedocs.io/) - [YARA GitHub](https://github.com/VirusTotal/yara) - [YARA 규칙 작성 가이드](https://yara.readthedocs.io/en/stable/writingrules.html) - [Neo23x0 Signature Base](https://github.com/Neo23x0/signature-base) - [VirusTotal YARA](https://www.virustotal.com/gui/hunting/yaradetect)