- Add YARA guide, WAF integration docs, and webshell rules - Ignore *.lock files in .gitignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
984 lines
25 KiB
Markdown
984 lines
25 KiB
Markdown
# 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 // <? 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 사용법
|
|
|
|
### 기본 스캔
|
|
|
|
```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 = '<?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'])}개 위협")
|
|
```
|
|
|
|
### 콜백 사용 (대용량 스캔)
|
|
|
|
```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)
|