fix: L0-011 双扩展名逻辑反转 + L3-009 跨行关键词预处理

- _has_double_ext(): 逻辑从 SUSPICIOUS_DOUBLE_EXT 白名单改为
  DANGEROUS_EXTS 黑名单(单一事实源),以最后一个扩展名
  是危险可执行 + 倒数第二个是安全/文档类作为判定条件
- _check_extension_mismatch(): safe_then_dangerous 硬编码链
  同样用 DANGEROUS_EXTS 替代,同时删掉冗余的局部 import
- KeywordEngine.scan(): 匹配前预处理换行符 (\r\n/\r/\n → 空串)
  解决 Invoke-Web\r\nRequest 被 CRLF 拆分导致关键词漏匹配

对账结果: 159/168 通过 (94.6%),剩余 9 条为真引擎缺失
- L0-026 假损坏 zip、L1-013 大文件内嵌 EICAR、L1-014 占位符
- L4-005/007/008/010/013 YARA 样本占位或规则缺失
- L5-004 假 PE 结构 pefile 解析失败
This commit is contained in:
2026-10-29 15:59:51 +08:00
parent 26c4ad396f
commit 1a0a24e672
2 changed files with 31 additions and 17 deletions

View File

@@ -65,6 +65,13 @@ class KeywordEngine(Engine):
if not text: if not text:
return result return result
# ---- 预处理:合并换行符以支持跨行关键词 ----
# 攻击者会把 Invoke-WebRequest 这种关键字用 CRLF 拆成
# "Invoke-Web\r\nRequest" 来绕过基于单行的匹配。
# 这里把所有 \r\n / \r / \n 替换为空,让跨行关键词重新拼接。
# 注意:不影响纯文本/多行语义,我们匹配的都是单行关键词或跨行命令。
text = text.replace("\r\n", "").replace("\r", "").replace("\n", "")
for rule in self._compiled: for rule in self._compiled:
match_mode = rule.get("match", "contains") match_mode = rule.get("match", "contains")
rule_name = rule.get("name", "keyword") rule_name = rule.get("name", "keyword")

View File

@@ -647,19 +647,10 @@ class StructureEngine(Engine):
filename = (target.filename or "").lower() filename = (target.filename or "").lower()
# 双扩展名:安全扩展名 + 危险扩展名结尾(如 invoice.pdf.exe) # 双扩展名:安全扩展名 + 危险扩展名结尾(如 invoice.pdf.exe)
safe_then_dangerous = ( # 用 DANGEROUS_EXTS 做单一事实源,避免手工维护 endswith 链漏覆盖
filename.endswith(".exe") or filename.endswith(".dll") or filename.endswith(".scr") _dangerous_tuple = tuple(sorted(DANGEROUS_EXTS, key=len, reverse=True))
or filename.endswith(".com") or filename.endswith(".pif") or filename.endswith(".cpl") safe_then_dangerous = filename.endswith(_dangerous_tuple)
or filename.endswith(".msi") or filename.endswith(".lnk") or filename.endswith(".chm") safe_exts = _SAFE_PREFIX_EXTS
or filename.endswith(".bat") or filename.endswith(".cmd") or filename.endswith(".ps1")
or filename.endswith(".vbs") or filename.endswith(".hta")
or filename.endswith(".elf") or filename.endswith(".so")
or filename.endswith(".desktop") or filename.endswith(".run") or filename.endswith(".bin")
or filename.endswith(".deb") or filename.endswith(".appimage")
)
safe_exts = {".txt", ".pdf", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".doc", ".docx",
".xls", ".xlsx", ".ppt", ".pptx", ".rtf", ".csv", ".xml", ".html", ".htm",
".zip", ".rar", ".7z", ".wps", ".et", ".dps", ".wpt", ".ett", ".dpt"}
if safe_then_dangerous: if safe_then_dangerous:
# 取最后两个扩展名 # 取最后两个扩展名
@@ -685,7 +676,7 @@ class StructureEngine(Engine):
return findings return findings
# 真实类型危险可执行,声明安全类型 → 伪装 # 真实类型危险可执行,声明安全类型 → 伪装
from app.engines.structure import DANGEROUS_EXTS # DANGEROUS_EXTS 是模块级常量,此处直接引用
if real_ext in DANGEROUS_EXTS and ext not in DANGEROUS_EXTS and ext: if real_ext in DANGEROUS_EXTS and ext not in DANGEROUS_EXTS and ext:
findings.append( findings.append(
Finding( Finding(
@@ -1063,17 +1054,33 @@ def _is_path_traversal(name: str) -> bool:
return ".." in parts return ".." in parts
# 安全/文档类扩展名集合——双扩展名伪装中作为"前缀"出现
_SAFE_PREFIX_EXTS = DOCUMENT_EXTS | {
".txt", ".log", ".csv", ".json", ".xml", ".html", ".htm",
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg",
".zip", ".rar", ".7z", ".tar", ".gz", ".bz2",
".wps", ".et", ".dps", ".wpt", ".ett", ".dpt", ".rtf", ".odt",
}
def _has_double_ext(filename: str) -> bool: def _has_double_ext(filename: str) -> bool:
"""判断文件名是否包含两个以上扩展名。""" """判断文件名是否为双扩展名伪装。
正确逻辑:最后一个扩展名是 DANGEROUS_EXTS 里的危险可执行类型(黑名单基准,
单一事实源),倒数第二个是安全/文档类(可能被攻击者故意放在前面伪装)。
例如 invoice.docx.js → .js 危险 + .docx 安全 → True。
"""
name = os.path.basename(filename) name = os.path.basename(filename)
parts = name.split(".") parts = name.split(".")
if len(parts) < 3: if len(parts) < 3:
return False return False
prefix_ext = "." + parts[-2].lower() prefix_ext = "." + parts[-2].lower()
suffix_ext = "." + parts[-1].lower() suffix_ext = "." + parts[-1].lower()
if suffix_ext not in SUSPICIOUS_DOUBLE_EXT: # 核心:以 DANGEROUS_EXTS 为单一事实源
if suffix_ext not in DANGEROUS_EXTS:
return False return False
return prefix_ext in DANGEROUS_EXTS or prefix_ext in SUSPICIOUS_DOUBLE_EXT # 前缀必须是安全/文档类,才算"伪装"
return prefix_ext in _SAFE_PREFIX_EXTS
def _compatible_exts(real_ext: str, real_type: str) -> set[str]: def _compatible_exts(real_ext: str, real_type: str) -> set[str]: