- Add YARA guide, WAF integration docs, and webshell rules - Ignore *.lock files in .gitignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
25 KiB
25 KiB
YARA 규칙 가이드
악성코드 및 웹쉘 탐지를 위한 YARA 사용법 정리
목차
YARA란?
YARA는 악성코드 연구자가 악성 샘플을 식별하고 분류하기 위해 만든 패턴 매칭 도구입니다.
| 항목 | 설명 |
|---|---|
| 개발 | VirusTotal (Google 소속) |
| 용도 | 악성코드, 웹쉘, 악성 문서 탐지 |
| 방식 | 문자열 + 바이트 패턴 + 조건 매칭 |
| AI 여부 | ❌ 규칙 기반 (AI 없음) |
| 속도 | ⚡ 매우 빠름 (1-10ms/파일) |
| 라이선스 | BSD-3-Clause (무료) |
핵심 개념
YARA = 악성코드 전용 grep/정규식
파일 내용에서 특정 패턴을 찾고,
여러 조건을 조합하여 악성 여부를 판단
설치
Linux (Ubuntu/Debian)
# 패키지 설치
sudo apt update
sudo apt install yara
# 버전 확인
yara --version
macOS
brew install yara
Python 바인딩
pip install yara-python
소스 컴파일 (최신 버전)
# 의존성 설치
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
규칙 문법
기본 구조
rule 규칙이름 {
meta:
// 메타데이터 (선택사항)
description = "규칙 설명"
author = "작성자"
date = "2025-01-01"
severity = "high"
strings:
// 탐지할 문자열/패턴 정의
$string1 = "탐지할 문자열"
$string2 = { 48 65 6C 6C 6F } // 헥스 바이트
$regex1 = /정규식 패턴/
condition:
// 탐지 조건
any of them
}
문자열 정의 방식
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)
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
/*
* 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 // <? or <h
// 실행 함수 + 사용자 입력
(any of ($exec*) or any of ($cmd*)) and
any of ($input*)
}
// ============================================
// 난독화 웹쉘 탐지
// ============================================
rule PHP_Webshell_Obfuscated {
meta:
description = "난독화된 PHP 웹쉘"
severity = "critical"
strings:
// 인코딩 함수
$enc1 = "base64_decode(" ascii nocase
$enc2 = "gzinflate(" ascii nocase
$enc3 = "gzuncompress(" ascii nocase
$enc4 = "gzdecode(" ascii nocase
$enc5 = "str_rot13(" ascii nocase
$enc6 = "convert_uudecode(" ascii nocase
// 문자 조합
$chr1 = /chr\s*\(\s*\d+\s*\)/ ascii
$chr2 = /\\x[0-9a-fA-F]{2}/ ascii
$chr3 = /\\[0-7]{3}/ ascii
// 동적 함수 호출
$dyn1 = /\$\w+\s*\(\s*\$/ ascii
$dyn2 = /\$\{\s*\$/ ascii
$dyn3 = /\$\w+\s*=\s*['"]\w+['"].*\$\w+\s*\(/ ascii
// Base64 인코딩된 위험 함수
$b64_eval = "ZXZhbC" ascii // base64("eval")
$b64_exec = "ZXhlYy" ascii // base64("exec")
$b64_system = "c3lzdGVt" ascii // base64("system")
$b64_assert = "YXNzZXJ0" ascii // base64("assert")
condition:
// 다중 인코딩 레이어
(2 of ($enc*)) or
// 문자 조합으로 함수 생성
(#chr1 > 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 사용법
기본 스캔
# 단일 파일 스캔
yara rules.yar target_file.php
# 디렉토리 재귀 스캔
yara -r rules.yar /var/www/html/
# 여러 규칙 파일 사용
yara -r rules1.yar rules2.yar /var/www/html/
출력 옵션
# 매칭된 문자열 표시
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
출력 예시
$ 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 연동
기본 사용
#!/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 = '<?php eval($_POST["cmd"]); ?>'
result = scanner.scan_data(code)
print(result)
# 디렉토리 스캔
results = scanner.scan_directory("/var/www/html")
print(f"스캔 완료: {results['scanned']}개 파일, {len(results['findings'])}개 위협")
콜백 사용 (대용량 스캔)
"""콜백을 사용한 효율적인 스캔"""
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
#!/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 서비스로 등록
# /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
# 서비스 등록 및 시작
sudo systemctl daemon-reload
sudo systemctl enable yara-monitor
sudo systemctl start yara-monitor
sudo systemctl status yara-monitor
공개 규칙 모음
주요 소스
| 저장소 | 규칙 수 | 주요 내용 |
|---|---|---|
| Neo23x0/signature-base | 3,000+ | 웹쉘, APT, 악성코드 |
| Yara-Rules/rules | 2,000+ | 범용 |
| InQuest/yara-rules | 500+ | 문서 악성코드 |
| bartblaze/Yara-rules | 300+ | 랜섬웨어 |
| kevthehermit/YaraRules | 200+ | 악성코드 |
| cuckoosandbox/community | 500+ | 샌드박스 분석용 |
규칙 다운로드
# 디렉토리 생성
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 - 웹쉘 (추천)
규칙 통합 사용
"""여러 규칙 소스 통합"""
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)
│ 난독화/제로데이 탐지
│ 오탐: 낮음 (검토 필요)
▼
[최종 판정]