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:
@@ -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")
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
Reference in New Issue
Block a user