Files
MeshSate/app/engines/binary.py
zhaolei 16f134e328 feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交
核心功能:
- 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制
- FastAPI + PostgreSQL + Redis + Docker Compose 架构
- 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性
- 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页)
- 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min)

并发与稳定性修复:
- 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行)
- JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写
- 移除 SQLite fallback, 统一 PostgreSQL
- API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
2026-10-28 19:07:11 +08:00

303 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""L2b YARA 规则与二进制分析引擎。
包含:
- YARA 规则匹配(规则库从 ``rules/`` 目录加载,支持热加载)
- PE 文件结构分析(pefile):节区异常、导入表、编译信息、签名
- 可执行文件能力识别(capa,若已安装)
"""
from __future__ import annotations
import os
import threading
from typing import Any, Dict, List, Optional
from app.engines.base import Engine, EngineResult, Finding, ScanTarget, Severity
BINARY_EXTS = {".exe", ".dll", ".sys", ".scr", ".com", ".cpl", ".ocx", ".msi", ".apk", ".jar", ".so", ".bin", ".elf"}
# ---------- 模块级全局 YARA 规则单例 ----------
# 所有 YaraEngine 实例共享同一个编译结果,避免重复 compile
# rules_dir 不同时(极少见)才分叉加载
_YARA_GLOBAL: Dict[str, Dict[str, Any]] = {}
_YARA_LOCK = threading.Lock()
def _get_global_yara(rules_dir: str) -> Optional[Any]:
"""获取全局共享的 YARA Rules 对象(自动按目录 mtime 重载)。"""
with _YARA_LOCK:
cached = _YARA_GLOBAL.get(rules_dir)
import yara as _yr
newest = 0.0
files: List[str] = []
if os.path.isdir(rules_dir):
for name in sorted(os.listdir(rules_dir)):
if name.endswith((".yar", ".yara")):
path = os.path.join(rules_dir, name)
files.append(path)
newest = max(newest, os.path.getmtime(path))
if cached and cached.get("mtime") == newest:
return cached.get("rules")
if not files:
_YARA_GLOBAL[rules_dir] = {"rules": None, "mtime": newest}
return None
try:
compiled = _yr.compile(filepaths={os.path.basename(p): p for p in files})
except Exception:
_YARA_GLOBAL[rules_dir] = {"rules": None, "mtime": newest}
return None
_YARA_GLOBAL[rules_dir] = {"rules": compiled, "mtime": newest}
return compiled
def _discover_rules_dir() -> str:
"""自动探测 YARA 规则目录(优先环境变量 > /app/rules > 项目根 rules/)。"""
env_dir = os.environ.get("MESHSAFE_RULES_DIR", "").strip()
if env_dir and os.path.isdir(env_dir):
return env_dir
for candidate in ("/app/rules", "rules", "./rules", "../rules"):
if os.path.isdir(candidate):
return os.path.abspath(candidate)
return env_dir or "/app/rules"
class YaraEngine(Engine):
"""YARA 规则匹配引擎(使用全局共享的编译结果)。"""
name = "yara"
layer = "L2B"
def __init__(self, rules_dir: str | None = None) -> None:
# 显式传入时才用传入值,否则走自动探测
self.rules_dir = rules_dir if rules_dir else _discover_rules_dir()
def _load_rules(self) -> Any:
"""委托给全局单例。"""
try:
import yara # noqa: F401 — 只是探测可用性
except Exception: # noqa: BLE001
return None
return _get_global_yara(self.rules_dir)
def scan(self, target: ScanTarget, ctx: Any) -> EngineResult:
result = EngineResult(engine=self.name)
rules = self._load_rules()
if rules is None:
result.ok = False
result.error = "YARA 规则库不可用或编译失败"
return result
try:
matches = rules.match(target.path)
except Exception as exc: # noqa: BLE001
result.ok = False
result.error = f"YARA 匹配失败:{exc}"
return result
for match in matches:
severity, weight = _judge_yara_rule(match.rule, match.tags)
# 覆盖:如果 ctx 里的策略为该 YARA 规则定义了精细权重,用它
try:
policy = ctx.policy_engine.policy
yara_weights = getattr(policy, "yara_weights", {}) or {}
if match.rule in yara_weights:
weight = float(yara_weights[match.rule])
except Exception:
pass # ctx 没有 policy 字段时保持默认 weight
result.findings.append(
Finding(
engine=self.name,
layer=self.layer,
severity=severity,
rule=match.rule,
detail=f"YARA 规则 {match.rule} 命中(标签:{', '.join(match.tags) or '无'})",
weight=weight,
meta={"tags": list(match.tags)},
)
)
# rule_set_version 用全局 compiled 对象的 id(每次 reload 变)
try:
_r = _get_global_yara(self.rules_dir)
result.meta["rule_set_version"] = str(id(_r)) if _r else "unavailable"
except Exception:
result.meta["rule_set_version"] = "unknown"
return result
def health(self) -> Dict[str, Any]:
rules = self._load_rules()
ok = rules is not None
return {
"engine": self.name,
"version": "rules" if ok else "none",
"ok": ok,
"status": "ok" if ok else "degraded",
"error": None if ok else "YARA 规则库未加载",
"info": f"rules_dir={self.rules_dir}" if not ok else "rules loaded",
}
class BinaryEngine(Engine):
"""PE/二进制文件结构分析引擎。"""
name = "binary"
layer = "L2B"
def scan(self, target: ScanTarget, ctx: Any) -> EngineResult:
result = EngineResult(engine=self.name)
ext = (target.declared_ext or "").lower()
if ext not in BINARY_EXTS:
return result
try:
import pefile
except Exception: # noqa: BLE001
result.ok = False
result.error = "pefile 未安装"
return result
try:
pe = pefile.PE(target.path, fast_load=False)
except Exception as exc: # noqa: BLE001
result.findings.append(
Finding(
engine=self.name,
layer=self.layer,
severity=Severity.MEDIUM.value,
rule="PeParseFailed",
detail=f"PE 结构解析失败:{exc}",
weight=10.0,
)
)
return result
try:
result.findings.extend(self._analyze(pe, target))
finally:
try:
pe.close()
except Exception: # noqa: BLE001
pass
return result
def _analyze(self, pe: Any, target: ScanTarget) -> List[Finding]:
findings: List[Finding] = []
# 1) 节区熵值:加密/加壳的节区熵接近 8
for section in getattr(pe, "sections", []) or []:
try:
name = section.Name.rstrip(b"\x00").decode("utf-8", errors="replace")
entropy = section.get_entropy()
except Exception: # noqa: BLE001
continue
if entropy >= 7.2:
findings.append(
Finding(
engine=self.name,
layer=self.layer,
severity=Severity.MEDIUM.value,
rule="HighEntropySection",
detail=f"节区 {name} 熵值 {entropy:.2f},可能经过加密或加壳",
weight=20.0,
)
)
# 2) 可疑导入表
suspicious_imports = self._suspicious_imports(pe)
if suspicious_imports:
findings.append(
Finding(
engine=self.name,
layer=self.layer,
severity=Severity.HIGH.value,
rule="SuspiciousImports",
detail=f"导入表包含高危 API:{', '.join(suspicious_imports[:8])}",
weight=45.0,
)
)
# 3) 无签名可执行文件
if not self._has_signature(pe):
findings.append(
Finding(
engine=self.name,
layer=self.layer,
severity=Severity.MEDIUM.value,
rule="UnsignedExecutable",
detail="可执行文件未携带数字签名",
weight=20.0,
)
)
# 4) 编译时间戳异常(明显晚于其他节区时间,或为 1970/未来时间)
timestamp = getattr(pe, "FILE_HEADER", None)
if timestamp is not None:
try:
ts = int(timestamp.TimeDateStamp)
if ts == 0 or ts > 4102444800: # 2100 年之后
findings.append(
Finding(
engine=self.name,
layer=self.layer,
severity=Severity.LOW.value,
rule="SuspiciousTimestamp",
detail="PE 编译时间戳异常,可能被伪造",
weight=10.0,
)
)
except Exception: # noqa: BLE001
pass
return findings
def _suspicious_imports(self, pe: Any) -> List[str]:
"""检测高危 API 导入。"""
suspicious = {
"VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread",
"NtUnmapViewOfSection", "SetWindowsHookEx", "OpenProcess",
"WinHttpOpen", "InternetOpenUrl", "URLDownloadToFile",
"CryptEncrypt", "AdjustTokenPrivileges", "CreateService",
"ShellExecute", "WinExec", "IsDebuggerPresent", "CheckRemoteDebuggerPresent",
}
found: List[str] = []
try:
for entry in getattr(pe, "DIRECTORY_ENTRY_IMPORT", []) or []:
for imp in entry.imports:
if imp.name:
name = imp.name.decode("utf-8", errors="replace")
if name in suspicious:
found.append(name)
except Exception: # noqa: BLE001
pass
return sorted(set(found))
def _has_signature(self, pe: Any) -> bool:
"""检查 PE 是否包含 Authenticode 签名目录。"""
try:
directory = pe.OPTIONAL_HEADER.DATA_DIRECTORY[4] # IMAGE_DIRECTORY_ENTRY_SECURITY
return bool(directory.VirtualAddress and directory.Size)
except Exception: # noqa: BLE001
return False
def _judge_yara_rule(rule: str, tags: List[str]) -> tuple[str, float]:
"""依据 YARA 规则名与标签判定严重度。"""
tag_set = {t.lower() for t in tags}
text = f"{rule} {' '.join(tags)}".lower()
if "critical" in tag_set or any(k in text for k in ("malware", "ransomware", "backdoor", "c2", "exploit")):
return Severity.CRITICAL.value, 85.0
if "high" in tag_set or any(k in text for k in ("trojan", "dropper", "obfusc", "shellcode")):
return Severity.HIGH.value, 60.0
if "medium" in tag_set or any(k in text for k in ("suspicious", "pua", "packer")):
return Severity.MEDIUM.value, 30.0
if "suspicious" in tag_set:
return Severity.MEDIUM.value, 30.0
return Severity.LOW.value, 10.0