fix: 六引擎误报根因修复 — 解包递归、YARA 恒真规则、签名分级、real_type 溢出
检测范围与证据链修复,不改裁决口径(口径改动在下一条提交)。 - extractor:zip/tar 成员在超限分支外漏加回扫描队列,解包后内容永远不参与检测; zip 目录条目被当成内嵌对象;tar 超限成员先落盘不删。递归真正生效后深扫耗时可测。 - clamav:_HIGH/_MEDIUM/_UNWANTED 关键词是小写,而签名名是 "Win.Trojan..." 大小写混排, 分级判定恒不命中全走默认档;改统一大写匹配。超过 clamd MaxFileSize 的裸奔区间 (1G~1.5G) 新增 ClamAVSkippedSizeLimit(30) — 按"不可扫描不放行"进复核。 - yara:pe.yar C1 系列改用导入表判定,不再靠裸子串命中字符串表; linux.yar C7 多条规则条件恒真(裸 nc/fetch 之类)已收紧;C8/C3 相应收窄。 - structure:DangerousExecutableExtension 是载体信号而非恶意行为,critical 权重 45→35, 不再作为把中低置信度信号一起顶过 block_score 的底座。 - scanner/documents:real_type 拼进 libmagic 对 MSI/OLE 的元数据串实测 334+ 字符, 超列宽导致整条 scan_tasks INSERT 连 findings 一起丢(被拒文件不留痕)→ 列宽 256→512,migrate.sql 补 ALTER。
This commit is contained in:
@@ -23,6 +23,32 @@ BINARY_EXTS = {".exe", ".dll", ".sys", ".scr", ".com", ".cpl", ".ocx", ".msi", "
|
||||
_PE_SCN_MEM_EXECUTE = 0x20000000
|
||||
_PE_SCN_MEM_WRITE = 0x80000000
|
||||
|
||||
# 声明扩展名 → 该格式自己的魔数。这些容器本就不是 PE,用 pefile 解析必然抛错;
|
||||
# 把"格式完全正常的安装包/共享库"记成 PeParseFailed 会给每个 .msi/.jar/.so 白送 10 分。
|
||||
_NATIVE_MAGIC: Dict[str, Any] = {
|
||||
".msi": (b"\xd0\xcf\x11\xe0",),
|
||||
".msp": (b"\xd0\xcf\x11\xe0",),
|
||||
".mst": (b"\xd0\xcf\x11\xe0",),
|
||||
".jar": (b"PK\x03\x04", b"PK\x05\x06"),
|
||||
".war": (b"PK\x03\x04", b"PK\x05\x06"),
|
||||
".apk": (b"PK\x03\x04", b"PK\x05\x06"),
|
||||
".so": (b"\x7fELF",),
|
||||
".elf": (b"\x7fELF",),
|
||||
}
|
||||
|
||||
|
||||
def _is_native_container(path: str, ext: str) -> bool:
|
||||
"""文件内容是否就是其声明格式(非 PE)。"""
|
||||
expected = _NATIVE_MAGIC.get(ext)
|
||||
if not expected:
|
||||
return False
|
||||
try:
|
||||
with open(path, "rb") as fh:
|
||||
head = fh.read(4)
|
||||
except OSError:
|
||||
return False
|
||||
return any(head.startswith(m) for m in expected)
|
||||
|
||||
|
||||
def _manual_pe_analysis(path: str) -> List[Finding]:
|
||||
"""pefile 解析失败时,手工搜索 PE\\0\\0 签名并解析节表。
|
||||
@@ -172,6 +198,8 @@ class YaraEngine(Engine):
|
||||
return result
|
||||
|
||||
try:
|
||||
# 本环境的 yara-python 4.5.4 在 Match 对象上默认就带 meta,
|
||||
# 且 rules.match() 不接受 with_meta 关键字(传了会整引擎报错)。
|
||||
matches = rules.match(target.path)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
result.ok = False
|
||||
@@ -179,7 +207,8 @@ class YaraEngine(Engine):
|
||||
return result
|
||||
|
||||
for match in matches:
|
||||
severity, weight = _judge_yara_rule(match.rule, match.tags)
|
||||
meta = getattr(match, "meta", None) or {}
|
||||
severity, weight = _judge_yara_rule(match.rule, match.tags, meta)
|
||||
# 覆盖:如果 ctx 里的策略为该 YARA 规则定义了精细权重,用它
|
||||
try:
|
||||
policy = ctx.policy_engine.policy
|
||||
@@ -188,13 +217,18 @@ class YaraEngine(Engine):
|
||||
weight = float(yara_weights[match.rule])
|
||||
except Exception:
|
||||
pass # ctx 没有 policy 字段时保持默认 weight
|
||||
desc = str(meta.get("description", "")).strip()
|
||||
tags_text = ", ".join(match.tags)
|
||||
result.findings.append(
|
||||
Finding(
|
||||
engine=self.name,
|
||||
layer=self.layer,
|
||||
severity=severity,
|
||||
rule=match.rule,
|
||||
detail=f"YARA 规则 {match.rule} 命中(标签:{', '.join(match.tags) or '无'})",
|
||||
detail=(
|
||||
f"YARA 规则 {match.rule} 命中"
|
||||
+ (f"({desc})" if desc else f"(标签:{tags_text or '无'})")
|
||||
),
|
||||
weight=weight,
|
||||
meta={"tags": list(match.tags)},
|
||||
)
|
||||
@@ -232,6 +266,10 @@ class BinaryEngine(Engine):
|
||||
if ext not in BINARY_EXTS:
|
||||
return result
|
||||
|
||||
# 内容确实是声明的那个非 PE 容器(MSI=CFB、JAR/APK=ZIP、SO/ELF=ELF)→ 不做 PE 分析
|
||||
if _is_native_container(target.path, ext):
|
||||
return result
|
||||
|
||||
try:
|
||||
import pefile
|
||||
except Exception: # noqa: BLE001
|
||||
@@ -290,17 +328,54 @@ class BinaryEngine(Engine):
|
||||
)
|
||||
)
|
||||
|
||||
# 2) 可疑导入表
|
||||
suspicious_imports = self._suspicious_imports(pe)
|
||||
if suspicious_imports:
|
||||
# 2) 导入表能力信号:只做佐证,不做独立定罪
|
||||
# Windows 自身的内核/外壳组件(kernel32、shell32、wer)必然导入
|
||||
# VirtualAllocEx/WriteProcessMemory 这类 API——它们就是这些 API 的实现方。
|
||||
# 真正的注入判定由 YARA C1_PE_ProcessInjection(导入名邻接窗口)负责,
|
||||
# 这里按「命中个数」而不是「命中即高危」计分,避免正常软件被判恶意。
|
||||
tier1, tier2, tier3 = self._suspicious_imports(pe)
|
||||
if len(tier1) >= 2:
|
||||
findings.append(
|
||||
Finding(
|
||||
engine=self.name,
|
||||
layer=self.layer,
|
||||
severity=Severity.HIGH.value,
|
||||
rule="SuspiciousImports",
|
||||
detail=f"导入表包含高危 API:{', '.join(suspicious_imports[:8])}",
|
||||
weight=25.0,
|
||||
detail=f"导入表同时包含多个注入/后门级 API:{', '.join(tier1[:8])}",
|
||||
weight=20.0,
|
||||
)
|
||||
)
|
||||
elif len(tier1) == 1:
|
||||
findings.append(
|
||||
Finding(
|
||||
engine=self.name,
|
||||
layer=self.layer,
|
||||
severity=Severity.LOW.value,
|
||||
rule="SuspiciousImports",
|
||||
detail=f"导入表含单个注入/后门级 API(系统组件常态,需其他信号佐证):{tier1[0]}",
|
||||
weight=5.0,
|
||||
)
|
||||
)
|
||||
if len(tier2) >= 3:
|
||||
findings.append(
|
||||
Finding(
|
||||
engine=self.name,
|
||||
layer=self.layer,
|
||||
severity=Severity.LOW.value,
|
||||
rule="CapabilityImports",
|
||||
detail=f"导入表同时具备多项危险能力:{', '.join(tier2[:8])}",
|
||||
weight=10.0,
|
||||
)
|
||||
)
|
||||
elif tier3:
|
||||
findings.append(
|
||||
Finding(
|
||||
engine=self.name,
|
||||
layer=self.layer,
|
||||
severity=Severity.INFO.value,
|
||||
rule="AntiDebugImports",
|
||||
detail=f"导入表含反调试/环境探测 API(正常软件普遍使用):{', '.join(tier3[:8])}",
|
||||
weight=0.0,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -312,8 +387,8 @@ class BinaryEngine(Engine):
|
||||
layer=self.layer,
|
||||
severity=Severity.MEDIUM.value,
|
||||
rule="UnsignedExecutable",
|
||||
detail="可执行文件未携带数字签名",
|
||||
weight=20.0,
|
||||
detail="可执行文件未携带数字签名(内网自研工具常态,仅辅助人工判断)",
|
||||
weight=10.0,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -330,7 +405,7 @@ class BinaryEngine(Engine):
|
||||
severity=Severity.LOW.value,
|
||||
rule="SuspiciousTimestamp",
|
||||
detail="PE 编译时间戳异常,可能被伪造",
|
||||
weight=10.0,
|
||||
weight=0.0,
|
||||
)
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
@@ -433,32 +508,62 @@ class BinaryEngine(Engine):
|
||||
severity=Severity.LOW.value,
|
||||
rule="DebugInformationPresent",
|
||||
detail="PE 包含调试信息(PDB 路径或 .debug 节),可用于逆向定位",
|
||||
weight=10.0,
|
||||
weight=0.0,
|
||||
)
|
||||
)
|
||||
|
||||
return findings
|
||||
|
||||
def _suspicious_imports(self, pe: Any) -> List[str]:
|
||||
"""检测高危 API 导入。"""
|
||||
suspicious = {
|
||||
def _suspicious_imports(self, pe: Any) -> "tuple[List[str], List[str], List[str]]":
|
||||
"""导入 API 按恶意指向分档。
|
||||
|
||||
返回 (tier1, tier2, tier3):
|
||||
tier1 注入/后门级 API —— 单独出现即可疑
|
||||
tier2 危险能力 API —— 正常软件也常用,需多项共存才计分
|
||||
tier3 反调试/探测 API —— 信息项,不计分(大量合法商业软件带加壳自保会用)
|
||||
"""
|
||||
tier1_set = {
|
||||
"VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread",
|
||||
"NtUnmapViewOfSection", "SetWindowsHookEx", "OpenProcess",
|
||||
"WinHttpOpen", "InternetOpenUrl", "URLDownloadToFile",
|
||||
"CryptEncrypt", "AdjustTokenPrivileges", "CreateService",
|
||||
"ShellExecute", "WinExec", "IsDebuggerPresent", "CheckRemoteDebuggerPresent",
|
||||
"NtUnmapViewOfSection", "SetWindowsHookEx", "SetWindowsHookExA",
|
||||
"SetWindowsHookExW", "URLDownloadToFileA", "URLDownloadToFileW",
|
||||
"QueueUserAPC", "NtQueueApcThread", "RtlCreateUserThread",
|
||||
}
|
||||
found: List[str] = []
|
||||
tier2_set = {
|
||||
"OpenProcess", "WinHttpOpen", "WinHttpOpenRequest", "WinHttpSendRequest",
|
||||
"CryptEncrypt", "CryptDeriveKey", "AdjustTokenPrivileges",
|
||||
"LookupPrivilegeValueA", "LookupPrivilegeValueW", "OpenProcessToken",
|
||||
"VirtualProtectEx", "ReadProcessMemory", "SuspendThread", "GetAsyncKeyState",
|
||||
"SetWinEventHook", "HttpSendRequestA", "InternetWriteFile",
|
||||
"RegSetValueExA", "RegSetValueExW",
|
||||
# 以下 API 恶意软件常用,但任何"要装服务/要启动进程/要联网"的正常
|
||||
# 软件都必然导入它们,不足以单独定罪
|
||||
"WinExec", "ShellExecuteExA", "ShellExecuteExW", "CreateServiceA",
|
||||
"CreateServiceW", "InternetOpenUrlA", "InternetOpenUrlW",
|
||||
"CreateNamedPipeA", "DefineDosDeviceW",
|
||||
}
|
||||
tier3_set = {
|
||||
"IsDebuggerPresent", "CheckRemoteDebuggerPresent", "OutputDebugStringA",
|
||||
"GetTickCount", "GetTickCount64", "QueryPerformanceCounter",
|
||||
"GetProcessHeap", "FindWindowA", "FindWindowW",
|
||||
}
|
||||
found1: List[str] = []
|
||||
found2: List[str] = []
|
||||
found3: 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)
|
||||
if not imp.name:
|
||||
continue
|
||||
name = imp.name.decode("utf-8", errors="replace")
|
||||
if name in tier1_set:
|
||||
found1.append(name)
|
||||
elif name in tier2_set:
|
||||
found2.append(name)
|
||||
elif name in tier3_set:
|
||||
found3.append(name)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return sorted(set(found))
|
||||
return sorted(set(found1)), sorted(set(found2)), sorted(set(found3))
|
||||
|
||||
def _has_signature(self, pe: Any) -> bool:
|
||||
"""检查 PE 是否包含 Authenticode 签名目录。"""
|
||||
@@ -469,8 +574,25 @@ class BinaryEngine(Engine):
|
||||
return False
|
||||
|
||||
|
||||
def _judge_yara_rule(rule: str, tags: List[str]) -> tuple[str, float]:
|
||||
"""依据 YARA 规则名与标签判定严重度。"""
|
||||
_YARA_RISK_SEVERITY = {
|
||||
"critical": Severity.CRITICAL.value,
|
||||
"high": Severity.HIGH.value,
|
||||
"medium": Severity.MEDIUM.value,
|
||||
"low": Severity.LOW.value,
|
||||
}
|
||||
_YARA_RISK_WEIGHT = {"critical": 85.0, "high": 60.0, "medium": 30.0, "low": 10.0}
|
||||
|
||||
|
||||
def _judge_yara_rule(rule: str, tags: List[str], meta: Dict[str, Any] | None = None) -> tuple[str, float]:
|
||||
"""依据规则 meta.risk_level 判定严重度(其次才是标签/规则名启发式)。
|
||||
|
||||
v0.2.3 只看标签与规则名,规则库里的 risk_level 被完全忽略,
|
||||
结果 65 分的 C1_PE_RAT 显示成"低危",权重与严重度两套口径。
|
||||
"""
|
||||
risk = str((meta or {}).get("risk_level", "")).strip().lower()
|
||||
if risk in _YARA_RISK_SEVERITY:
|
||||
return _YARA_RISK_SEVERITY[risk], _YARA_RISK_WEIGHT[risk]
|
||||
|
||||
tag_set = {t.lower() for t in tags}
|
||||
text = f"{rule} {' '.join(tags)}".lower()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user