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_EXECUTE = 0x20000000
|
||||||
_PE_SCN_MEM_WRITE = 0x80000000
|
_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]:
|
def _manual_pe_analysis(path: str) -> List[Finding]:
|
||||||
"""pefile 解析失败时,手工搜索 PE\\0\\0 签名并解析节表。
|
"""pefile 解析失败时,手工搜索 PE\\0\\0 签名并解析节表。
|
||||||
@@ -172,6 +198,8 @@ class YaraEngine(Engine):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 本环境的 yara-python 4.5.4 在 Match 对象上默认就带 meta,
|
||||||
|
# 且 rules.match() 不接受 with_meta 关键字(传了会整引擎报错)。
|
||||||
matches = rules.match(target.path)
|
matches = rules.match(target.path)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
result.ok = False
|
result.ok = False
|
||||||
@@ -179,7 +207,8 @@ class YaraEngine(Engine):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
for match in matches:
|
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 规则定义了精细权重,用它
|
# 覆盖:如果 ctx 里的策略为该 YARA 规则定义了精细权重,用它
|
||||||
try:
|
try:
|
||||||
policy = ctx.policy_engine.policy
|
policy = ctx.policy_engine.policy
|
||||||
@@ -188,13 +217,18 @@ class YaraEngine(Engine):
|
|||||||
weight = float(yara_weights[match.rule])
|
weight = float(yara_weights[match.rule])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # ctx 没有 policy 字段时保持默认 weight
|
pass # ctx 没有 policy 字段时保持默认 weight
|
||||||
|
desc = str(meta.get("description", "")).strip()
|
||||||
|
tags_text = ", ".join(match.tags)
|
||||||
result.findings.append(
|
result.findings.append(
|
||||||
Finding(
|
Finding(
|
||||||
engine=self.name,
|
engine=self.name,
|
||||||
layer=self.layer,
|
layer=self.layer,
|
||||||
severity=severity,
|
severity=severity,
|
||||||
rule=match.rule,
|
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,
|
weight=weight,
|
||||||
meta={"tags": list(match.tags)},
|
meta={"tags": list(match.tags)},
|
||||||
)
|
)
|
||||||
@@ -232,6 +266,10 @@ class BinaryEngine(Engine):
|
|||||||
if ext not in BINARY_EXTS:
|
if ext not in BINARY_EXTS:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# 内容确实是声明的那个非 PE 容器(MSI=CFB、JAR/APK=ZIP、SO/ELF=ELF)→ 不做 PE 分析
|
||||||
|
if _is_native_container(target.path, ext):
|
||||||
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pefile
|
import pefile
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
@@ -290,17 +328,54 @@ class BinaryEngine(Engine):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2) 可疑导入表
|
# 2) 导入表能力信号:只做佐证,不做独立定罪
|
||||||
suspicious_imports = self._suspicious_imports(pe)
|
# Windows 自身的内核/外壳组件(kernel32、shell32、wer)必然导入
|
||||||
if suspicious_imports:
|
# VirtualAllocEx/WriteProcessMemory 这类 API——它们就是这些 API 的实现方。
|
||||||
|
# 真正的注入判定由 YARA C1_PE_ProcessInjection(导入名邻接窗口)负责,
|
||||||
|
# 这里按「命中个数」而不是「命中即高危」计分,避免正常软件被判恶意。
|
||||||
|
tier1, tier2, tier3 = self._suspicious_imports(pe)
|
||||||
|
if len(tier1) >= 2:
|
||||||
findings.append(
|
findings.append(
|
||||||
Finding(
|
Finding(
|
||||||
engine=self.name,
|
engine=self.name,
|
||||||
layer=self.layer,
|
layer=self.layer,
|
||||||
severity=Severity.HIGH.value,
|
severity=Severity.HIGH.value,
|
||||||
rule="SuspiciousImports",
|
rule="SuspiciousImports",
|
||||||
detail=f"导入表包含高危 API:{', '.join(suspicious_imports[:8])}",
|
detail=f"导入表同时包含多个注入/后门级 API:{', '.join(tier1[:8])}",
|
||||||
weight=25.0,
|
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,
|
layer=self.layer,
|
||||||
severity=Severity.MEDIUM.value,
|
severity=Severity.MEDIUM.value,
|
||||||
rule="UnsignedExecutable",
|
rule="UnsignedExecutable",
|
||||||
detail="可执行文件未携带数字签名",
|
detail="可执行文件未携带数字签名(内网自研工具常态,仅辅助人工判断)",
|
||||||
weight=20.0,
|
weight=10.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -330,7 +405,7 @@ class BinaryEngine(Engine):
|
|||||||
severity=Severity.LOW.value,
|
severity=Severity.LOW.value,
|
||||||
rule="SuspiciousTimestamp",
|
rule="SuspiciousTimestamp",
|
||||||
detail="PE 编译时间戳异常,可能被伪造",
|
detail="PE 编译时间戳异常,可能被伪造",
|
||||||
weight=10.0,
|
weight=0.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
@@ -433,32 +508,62 @@ class BinaryEngine(Engine):
|
|||||||
severity=Severity.LOW.value,
|
severity=Severity.LOW.value,
|
||||||
rule="DebugInformationPresent",
|
rule="DebugInformationPresent",
|
||||||
detail="PE 包含调试信息(PDB 路径或 .debug 节),可用于逆向定位",
|
detail="PE 包含调试信息(PDB 路径或 .debug 节),可用于逆向定位",
|
||||||
weight=10.0,
|
weight=0.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return findings
|
return findings
|
||||||
|
|
||||||
def _suspicious_imports(self, pe: Any) -> List[str]:
|
def _suspicious_imports(self, pe: Any) -> "tuple[List[str], List[str], List[str]]":
|
||||||
"""检测高危 API 导入。"""
|
"""导入 API 按恶意指向分档。
|
||||||
suspicious = {
|
|
||||||
|
返回 (tier1, tier2, tier3):
|
||||||
|
tier1 注入/后门级 API —— 单独出现即可疑
|
||||||
|
tier2 危险能力 API —— 正常软件也常用,需多项共存才计分
|
||||||
|
tier3 反调试/探测 API —— 信息项,不计分(大量合法商业软件带加壳自保会用)
|
||||||
|
"""
|
||||||
|
tier1_set = {
|
||||||
"VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread",
|
"VirtualAllocEx", "WriteProcessMemory", "CreateRemoteThread",
|
||||||
"NtUnmapViewOfSection", "SetWindowsHookEx", "OpenProcess",
|
"NtUnmapViewOfSection", "SetWindowsHookEx", "SetWindowsHookExA",
|
||||||
"WinHttpOpen", "InternetOpenUrl", "URLDownloadToFile",
|
"SetWindowsHookExW", "URLDownloadToFileA", "URLDownloadToFileW",
|
||||||
"CryptEncrypt", "AdjustTokenPrivileges", "CreateService",
|
"QueueUserAPC", "NtQueueApcThread", "RtlCreateUserThread",
|
||||||
"ShellExecute", "WinExec", "IsDebuggerPresent", "CheckRemoteDebuggerPresent",
|
|
||||||
}
|
}
|
||||||
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:
|
try:
|
||||||
for entry in getattr(pe, "DIRECTORY_ENTRY_IMPORT", []) or []:
|
for entry in getattr(pe, "DIRECTORY_ENTRY_IMPORT", []) or []:
|
||||||
for imp in entry.imports:
|
for imp in entry.imports:
|
||||||
if imp.name:
|
if not imp.name:
|
||||||
|
continue
|
||||||
name = imp.name.decode("utf-8", errors="replace")
|
name = imp.name.decode("utf-8", errors="replace")
|
||||||
if name in suspicious:
|
if name in tier1_set:
|
||||||
found.append(name)
|
found1.append(name)
|
||||||
|
elif name in tier2_set:
|
||||||
|
found2.append(name)
|
||||||
|
elif name in tier3_set:
|
||||||
|
found3.append(name)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
return sorted(set(found))
|
return sorted(set(found1)), sorted(set(found2)), sorted(set(found3))
|
||||||
|
|
||||||
def _has_signature(self, pe: Any) -> bool:
|
def _has_signature(self, pe: Any) -> bool:
|
||||||
"""检查 PE 是否包含 Authenticode 签名目录。"""
|
"""检查 PE 是否包含 Authenticode 签名目录。"""
|
||||||
@@ -469,8 +574,25 @@ class BinaryEngine(Engine):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _judge_yara_rule(rule: str, tags: List[str]) -> tuple[str, float]:
|
_YARA_RISK_SEVERITY = {
|
||||||
"""依据 YARA 规则名与标签判定严重度。"""
|
"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}
|
tag_set = {t.lower() for t in tags}
|
||||||
text = f"{rule} {' '.join(tags)}".lower()
|
text = f"{rule} {' '.join(tags)}".lower()
|
||||||
|
|
||||||
|
|||||||
@@ -45,9 +45,21 @@ class ClamAVEngine(Engine):
|
|||||||
except _ClamdSizeLimit:
|
except _ClamdSizeLimit:
|
||||||
# 文件超过 ClamAV MaxFileSize(clamd.conf 可调,默认 1G)
|
# 文件超过 ClamAV MaxFileSize(clamd.conf 可调,默认 1G)
|
||||||
# → 引擎只是拒扫这个大文件,不当成引擎故障
|
# → 引擎只是拒扫这个大文件,不当成引擎故障
|
||||||
|
# 但"没查过"必须留下信号:策略里 max_file_size 是 1.5G,1G~1.5G 的文件
|
||||||
|
# 原本在 L1 完全裸奔却仍可能 clean/pass。按"不可扫描不放行"落人工复核。
|
||||||
result.ok = True
|
result.ok = True
|
||||||
result.meta["skipped"] = "size_limit"
|
result.meta["skipped"] = "size_limit"
|
||||||
result.meta["reason"] = "文件超过 ClamAV 大小限制已跳过"
|
result.meta["reason"] = "文件超过 ClamAV 大小限制已跳过"
|
||||||
|
result.findings.append(
|
||||||
|
Finding(
|
||||||
|
engine=self.name,
|
||||||
|
layer=self.layer,
|
||||||
|
severity=Severity.MEDIUM.value,
|
||||||
|
rule="ClamAVSkippedSizeLimit",
|
||||||
|
detail="文件超过 clamd MaxFileSize,L1 签名查杀未执行,按不可扫描内容处置",
|
||||||
|
weight=30.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
# 真·引擎不可用:连不上 / PING 超时 / daemon 挂了
|
# 真·引擎不可用:连不上 / PING 超时 / daemon 挂了
|
||||||
@@ -267,9 +279,9 @@ def _parse_clamd_response(text: str) -> Dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
# 签名名中的可信度关键词映射到严重度
|
# 签名名中的可信度关键词映射到严重度
|
||||||
_HIGH_KEYWORDS = ("ransom", "crypt", "trojan", "backdoor", "rootkit", "stealer", "keylog", "worm", "dropper", "ransomware")
|
_HIGH_KEYWORDS = ("RANSOM", "CRYPT", "TROJAN", "BACKDOOR", "ROOTKIT", "STEALER", "KEYLOG", "WORM", "DROPPER")
|
||||||
_MEDIUM_KEYWORDS = ("generic", "heur", "packed", "obfusc", "suspect", "malware", "riskware", "virus")
|
_MEDIUM_KEYWORDS = ("GENERIC", "HEUR", "PACKED", "OBFUSC", "SUSPECT", "MALWARE", "RISKWARE", "VIRUS")
|
||||||
_UNWANTED_KEYWORDS = ("PUA", "unwanted", "toolkit", "pup", "adware")
|
_UNWANTED_KEYWORDS = ("PUA", "UNWANTED", "TOOLKIT", "PUP", "ADWARE")
|
||||||
|
|
||||||
# EICAR 测试病毒签名 —— 明确标为 critical(等保要求:检测到即阻断)
|
# EICAR 测试病毒签名 —— 明确标为 critical(等保要求:检测到即阻断)
|
||||||
_EICAR_SIGNATURES = ("Eicar-Test-Signature", "EICAR-AV-Test", "Eicar-Test-Signature.UNOFFICIAL")
|
_EICAR_SIGNATURES = ("Eicar-Test-Signature", "EICAR-AV-Test", "Eicar-Test-Signature.UNOFFICIAL")
|
||||||
|
|||||||
@@ -56,6 +56,19 @@ EML_EXTS = {".eml", ".msg"}
|
|||||||
# 带 VBA 宏的 Office 扩展名(用于 VBA 纯文本扫描触发)
|
# 带 VBA 宏的 Office 扩展名(用于 VBA 纯文本扫描触发)
|
||||||
VBA_MACRO_OFFICE_EXTS = OLE_EXTS | OOXML_EXTS | {".bas", ".vba", ".cls", ".frm"}
|
VBA_MACRO_OFFICE_EXTS = OLE_EXTS | OOXML_EXTS | {".bas", ".vba", ".cls", ".frm"}
|
||||||
|
|
||||||
|
# OOXML 包内的内嵌 OLE 部件目录
|
||||||
|
EMBED_PREFIX_RE = re.compile(r"^(?:word|xl|ppt)/embeddings/.+")
|
||||||
|
|
||||||
|
# 内嵌对象中的办公载体:文档互嵌(子表格/图表/PDF)是正常用法,不等于藏可执行
|
||||||
|
OFFICE_PART_EXTS = {
|
||||||
|
".docx", ".docm", ".xlsx", ".xlsm", ".pptx", ".pptm",
|
||||||
|
".doc", ".xls", ".ppt", ".pdf", ".rtf", ".xml", ".txt", ".csv",
|
||||||
|
".htm", ".html", ".wmf", ".emf", ".bin",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 内嵌部件里直接判定为可执行对象的扩展名
|
||||||
|
EXEC_PART_EXTS = {".exe", ".dll", ".scr", ".js", ".jse", ".vbs", ".vbe", ".bat", ".cmd", ".ps1", ".hta", ".com", ".msi"}
|
||||||
|
|
||||||
# VBA 自动执行入口关键词
|
# VBA 自动执行入口关键词
|
||||||
VBA_AUTOEXEC_PATTERNS = [
|
VBA_AUTOEXEC_PATTERNS = [
|
||||||
r"Sub\s+AutoOpen\s*\(", r"Sub\s+AutoExec\s*\(", r"Sub\s+AutoNew\s*\(",
|
r"Sub\s+AutoOpen\s*\(", r"Sub\s+AutoExec\s*\(", r"Sub\s+AutoNew\s*\(",
|
||||||
@@ -211,14 +224,16 @@ class DocumentEngine(Engine):
|
|||||||
else:
|
else:
|
||||||
parser.close()
|
parser.close()
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
|
# 宏流解析不了 = 内容不可判定。这类畸形 OLE 常被用来探测解析器异常,
|
||||||
|
# 给 0 分等于"看不懂就放行",落人工复核。
|
||||||
findings.append(
|
findings.append(
|
||||||
Finding(
|
Finding(
|
||||||
engine=self.name,
|
engine=self.name,
|
||||||
layer=self.layer,
|
layer=self.layer,
|
||||||
severity=Severity.LOW.value,
|
severity=Severity.MEDIUM.value,
|
||||||
rule="OleToolsUnavailable",
|
rule="OleToolsUnavailable",
|
||||||
detail=f"VBA 宏解析失败:{exc}",
|
detail=f"VBA 宏解析失败:{exc}",
|
||||||
weight=0.0,
|
weight=30.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -407,6 +422,24 @@ class DocumentEngine(Engine):
|
|||||||
"excerpt": excerpt,
|
"excerpt": excerpt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _looks_like_executable(self, zf: Any, name: str) -> bool:
|
||||||
|
"""判断内嵌部件是否为可执行载荷(扩展名 + 文件头魔数)。
|
||||||
|
|
||||||
|
oleObject1.bin 之类的外壳里可能装着真正的 PE,光看扩展名会漏;
|
||||||
|
反之把每个 .bin 都判成可执行又会拦掉正常的图表/子文档。
|
||||||
|
"""
|
||||||
|
if os.path.splitext(name)[1].lower() in EXEC_PART_EXTS:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
with zf.open(name) as fp:
|
||||||
|
head = fp.read(1 << 20)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return False
|
||||||
|
if head[:2] == b"MZ" or head[:4] in (b"\x7fELF", b"\xca\xfe\xba\xbe"):
|
||||||
|
return True
|
||||||
|
# OLE 复合容器(Package/Ole10Native)内嵌 PE:找 MZ..PE 头
|
||||||
|
return bool(re.search(rb"MZ[\x00-\xff]{0,64}PE\x00\x00", head))
|
||||||
|
|
||||||
def _scan_ooxml(self, target: ScanTarget) -> List[Finding]:
|
def _scan_ooxml(self, target: ScanTarget) -> List[Finding]:
|
||||||
"""检测 OOXML(docx/xlsx/pptx)中的外部引用与外部链接。"""
|
"""检测 OOXML(docx/xlsx/pptx)中的外部引用与外部链接。"""
|
||||||
findings: List[Finding] = []
|
findings: List[Finding] = []
|
||||||
@@ -429,9 +462,11 @@ class DocumentEngine(Engine):
|
|||||||
weight=45.0,
|
weight=45.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if name.startswith("word/embeddings/") or name.startswith("xl/embeddings/") or name.startswith("ppt/embeddings/"):
|
# 内嵌 OLE 部件。zip 目录条目(以 / 结尾、长度为 0)不是部件——
|
||||||
|
# PowerPoint 导出的包常带空的 ppt/embeddings/ 目录,按文件处理会凭空报内嵌对象。
|
||||||
|
if EMBED_PREFIX_RE.match(name) and not name.endswith("/"):
|
||||||
embedded_ext = os.path.splitext(name)[1].lower()
|
embedded_ext = os.path.splitext(name)[1].lower()
|
||||||
if embedded_ext in {".exe", ".dll", ".scr", ".js", ".vbs", ".bat", ".bin"}:
|
if self._looks_like_executable(zf, name):
|
||||||
findings.append(
|
findings.append(
|
||||||
Finding(
|
Finding(
|
||||||
engine=self.name,
|
engine=self.name,
|
||||||
@@ -442,6 +477,17 @@ class DocumentEngine(Engine):
|
|||||||
weight=75.0,
|
weight=75.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif embedded_ext in OFFICE_PART_EXTS:
|
||||||
|
findings.append(
|
||||||
|
Finding(
|
||||||
|
engine=self.name,
|
||||||
|
layer=self.layer,
|
||||||
|
severity=Severity.LOW.value,
|
||||||
|
rule="EmbeddedObject",
|
||||||
|
detail=f"文档内嵌办公对象:{name}",
|
||||||
|
weight=5.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
findings.append(
|
findings.append(
|
||||||
Finding(
|
Finding(
|
||||||
|
|||||||
@@ -205,8 +205,9 @@ class SafeExtractor:
|
|||||||
safe_name = self._sanitize_name(name)
|
safe_name = self._sanitize_name(name)
|
||||||
dest = os.path.join(out_dir, safe_name)
|
dest = os.path.join(out_dir, safe_name)
|
||||||
try:
|
try:
|
||||||
with zf.open(info) as src, open(dest, "wb") as out:
|
|
||||||
written = 0
|
written = 0
|
||||||
|
overflow = False
|
||||||
|
with zf.open(info) as src, open(dest, "wb") as out:
|
||||||
while True:
|
while True:
|
||||||
chunk = src.read(1024 * 1024)
|
chunk = src.read(1024 * 1024)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
@@ -222,9 +223,11 @@ class SafeExtractor:
|
|||||||
f"成员 {name} 实际解压超限,已丢弃",
|
f"成员 {name} 实际解压超限,已丢弃",
|
||||||
10.0,
|
10.0,
|
||||||
)
|
)
|
||||||
|
overflow = True
|
||||||
break
|
break
|
||||||
out.write(chunk)
|
out.write(chunk)
|
||||||
else:
|
# 成员必须回到扫描队列,否则压缩包/OOXML 里的内容永远不参与检测
|
||||||
|
if not overflow:
|
||||||
report.total_bytes += written
|
report.total_bytes += written
|
||||||
report.files.append(
|
report.files.append(
|
||||||
ScanTarget(
|
ScanTarget(
|
||||||
@@ -323,14 +326,26 @@ class SafeExtractor:
|
|||||||
continue
|
continue
|
||||||
with src, open(dest, "wb") as out:
|
with src, open(dest, "wb") as out:
|
||||||
written = 0
|
written = 0
|
||||||
|
overflow = False
|
||||||
while True:
|
while True:
|
||||||
chunk = src.read(1024 * 1024)
|
chunk = src.read(1024 * 1024)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
written += len(chunk)
|
written += len(chunk)
|
||||||
if written > self.settings.max_upload_bytes:
|
if written > self.settings.max_upload_bytes:
|
||||||
|
out.close()
|
||||||
|
os.remove(dest)
|
||||||
|
self._add_finding(
|
||||||
|
report,
|
||||||
|
Severity.MEDIUM.value,
|
||||||
|
"MemberTooLarge",
|
||||||
|
f"成员 {name} 实际解压超限,已丢弃",
|
||||||
|
10.0,
|
||||||
|
)
|
||||||
|
overflow = True
|
||||||
break
|
break
|
||||||
out.write(chunk)
|
out.write(chunk)
|
||||||
|
if not overflow:
|
||||||
report.total_bytes += written
|
report.total_bytes += written
|
||||||
report.files.append(
|
report.files.append(
|
||||||
ScanTarget(
|
ScanTarget(
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ from app.engines.base import Engine, EngineResult, Finding, ScanTarget, Severity
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# cross_line 规则用的续行归一:换行(及其两侧空白)压成一个空格
|
||||||
|
NEWLINE_RUN_RE = re.compile(r"\s*[\r\n]+\s*")
|
||||||
|
|
||||||
|
|
||||||
class KeywordEngine(Engine):
|
class KeywordEngine(Engine):
|
||||||
"""内网特征词表匹配引擎。"""
|
"""内网特征词表匹配引擎。"""
|
||||||
@@ -94,12 +97,13 @@ class KeywordEngine(Engine):
|
|||||||
if not text:
|
if not text:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# ---- 预处理:合并换行符以支持跨行关键词 ----
|
# ---- 预处理 ----
|
||||||
# 攻击者会把 Invoke-WebRequest 这种关键字用 CRLF 拆成
|
# 默认按原始行匹配:把整篇文本的换行全部删除会让上一行行尾和下一行行首
|
||||||
# "Invoke-Web\r\nRequest" 来绕过基于单行的匹配。
|
# 黏成一句话(Ruby 源码 "…breed\n+fetch()…" 曾被当成 curl 特征命中)。
|
||||||
# 这里把所有 \r\n / \r / \n 替换为空,让跨行关键词重新拼接。
|
# cross_line: true 的规则再跑一遍"换行压成空格"的文本,用于兜住
|
||||||
# 注意:不影响纯文本/多行语义,我们匹配的都是单行关键词或跨行命令。
|
# wget \ + 换行 + -O payload 这类续行绕过;保留分隔符,不产生黏连。
|
||||||
text = text.replace("\r\n", "").replace("\r", "").replace("\n", "")
|
text_norm = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||||
|
text_joined = NEWLINE_RUN_RE.sub(" ", text_norm)
|
||||||
|
|
||||||
for rule in self._compiled:
|
for rule in self._compiled:
|
||||||
match_mode = rule.get("match", "contains")
|
match_mode = rule.get("match", "contains")
|
||||||
@@ -113,23 +117,29 @@ class KeywordEngine(Engine):
|
|||||||
if match_mode == "regex":
|
if match_mode == "regex":
|
||||||
compiled = rule.get("_re")
|
compiled = rule.get("_re")
|
||||||
if compiled:
|
if compiled:
|
||||||
for m in compiled.finditer(text):
|
scan_targets = [text_norm]
|
||||||
snippet = text[max(0, m.start() - 10): m.end() + 10]
|
if rule.get("cross_line"):
|
||||||
hits.append(snippet.strip())
|
scan_targets.append(text_joined)
|
||||||
|
for body in scan_targets:
|
||||||
|
for m in compiled.finditer(body):
|
||||||
|
snippet = body[max(0, m.start() - 10): m.end() + 10]
|
||||||
|
hits.append(" ".join(snippet.split()))
|
||||||
|
if len(hits) >= 3:
|
||||||
|
break
|
||||||
if len(hits) >= 3:
|
if len(hits) >= 3:
|
||||||
break
|
break
|
||||||
|
|
||||||
elif match_mode == "any_of":
|
elif match_mode == "any_of":
|
||||||
keywords = rule.get("keywords", [])
|
keywords = rule.get("keywords", [])
|
||||||
for kw in keywords:
|
for kw in keywords:
|
||||||
if kw.lower() in text.lower():
|
if kw.lower() in text_norm.lower():
|
||||||
hits.append(kw)
|
hits.append(kw)
|
||||||
if len(hits) >= 3:
|
if len(hits) >= 3:
|
||||||
break
|
break
|
||||||
|
|
||||||
elif match_mode == "contains":
|
elif match_mode == "contains":
|
||||||
pattern = rule.get("pattern", "")
|
pattern = rule.get("pattern", "")
|
||||||
if pattern and (pattern.lower() in text.lower()):
|
if pattern and (pattern.lower() in text_norm.lower()):
|
||||||
hits.append(pattern)
|
hits.append(pattern)
|
||||||
|
|
||||||
if hits:
|
if hits:
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ class StructureEngine(Engine):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 3) 危险可执行扩展名(顶层文件)
|
# 3) 危险可执行扩展名(顶层文件)
|
||||||
|
# 这是「载体」信号而不是恶意行为:单独命中应当进人工复核(≥review_score),
|
||||||
|
# 但不能成为把后续中/低置信度检测一起顶过 block_score 的底座,
|
||||||
|
# 否则正常签名的 exe/dll 会被判恶意。
|
||||||
if ext in DANGEROUS_EXTS and not target.is_extracted:
|
if ext in DANGEROUS_EXTS and not target.is_extracted:
|
||||||
severity = Severity.CRITICAL.value if ext in {".exe", ".scr", ".dll", ".com", ".pif"} else Severity.HIGH.value
|
severity = Severity.CRITICAL.value if ext in {".exe", ".scr", ".dll", ".com", ".pif"} else Severity.HIGH.value
|
||||||
findings.append(
|
findings.append(
|
||||||
@@ -117,7 +120,7 @@ class StructureEngine(Engine):
|
|||||||
severity=severity,
|
severity=severity,
|
||||||
rule="DangerousExecutableExtension",
|
rule="DangerousExecutableExtension",
|
||||||
detail=f"文件类型为可执行/脚本格式:{ext}",
|
detail=f"文件类型为可执行/脚本格式:{ext}",
|
||||||
weight=45.0 if severity == Severity.CRITICAL.value else 30.0,
|
weight=35.0 if severity == Severity.CRITICAL.value else 30.0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class ScanTask(Base):
|
|||||||
size: Mapped[int] = mapped_column(Integer, default=0)
|
size: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
sha256: Mapped[str] = mapped_column(String(64), index=True)
|
sha256: Mapped[str] = mapped_column(String(64), index=True)
|
||||||
md5: Mapped[str] = mapped_column(String(32), default="")
|
md5: Mapped[str] = mapped_column(String(32), default="")
|
||||||
real_type: Mapped[str] = mapped_column(String(256), default="")
|
real_type: Mapped[str] = mapped_column(String(512), default="")
|
||||||
|
|
||||||
status: Mapped[str] = mapped_column(String(16), default="pending", index=True)
|
status: Mapped[str] = mapped_column(String(16), default="pending", index=True)
|
||||||
mode: Mapped[str] = mapped_column(String(16), default="sync")
|
mode: Mapped[str] = mapped_column(String(16), default="sync")
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ _settings = get_settings()
|
|||||||
TEMP_ROOT = os.environ.get("MESHSAFE_TEMP_ROOT", "/tmp/meshsafe")
|
TEMP_ROOT = os.environ.get("MESHSAFE_TEMP_ROOT", "/tmp/meshsafe")
|
||||||
|
|
||||||
|
|
||||||
|
def _clip(value: Any, limit: int) -> str:
|
||||||
|
"""按列宽截断字符串。
|
||||||
|
|
||||||
|
外部可控的长度(libmagic 描述串、归档成员路径、用户文件名)一旦超出列宽,
|
||||||
|
PostgreSQL 会拒绝整条 INSERT,连带同事务里的 scan_findings 一起丢失——
|
||||||
|
被拒绝的文件反而查不到记录。宁可截断,也不能整条不落库。
|
||||||
|
"""
|
||||||
|
text = str(value or "")
|
||||||
|
return text[:limit]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ScanContext:
|
class ScanContext:
|
||||||
"""一次扫描任务的上下文。"""
|
"""一次扫描任务的上下文。"""
|
||||||
@@ -95,12 +106,12 @@ class ScanOutcome:
|
|||||||
id=self.task_id,
|
id=self.task_id,
|
||||||
channel=self.channel,
|
channel=self.channel,
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
filename=self.file_info.get("filename", ""),
|
filename=_clip(self.file_info.get("filename", ""), 512),
|
||||||
declared_ext=self.file_info.get("declared_ext", ""),
|
declared_ext=_clip(self.file_info.get("declared_ext", ""), 32),
|
||||||
size=self.file_info.get("size", 0),
|
size=self.file_info.get("size", 0),
|
||||||
sha256=self.file_info.get("sha256", ""),
|
sha256=self.file_info.get("sha256", ""),
|
||||||
md5=self.file_info.get("md5", ""),
|
md5=self.file_info.get("md5", ""),
|
||||||
real_type=self.file_info.get("real_type", ""),
|
real_type=_clip(self.file_info.get("real_type", ""), 512),
|
||||||
status="completed",
|
status="completed",
|
||||||
mode=self.mode,
|
mode=self.mode,
|
||||||
verdict=self.verdict,
|
verdict=self.verdict,
|
||||||
@@ -122,7 +133,7 @@ class ScanOutcome:
|
|||||||
severity=f.severity,
|
severity=f.severity,
|
||||||
rule=f.rule,
|
rule=f.rule,
|
||||||
detail=f.detail,
|
detail=f.detail,
|
||||||
extracted_path=f.extracted_path,
|
extracted_path=_clip(f.extracted_path, 1024),
|
||||||
weight=f.weight,
|
weight=f.weight,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -117,17 +117,23 @@ rule C7_Linux_ReverseShell_Misc {
|
|||||||
meta:
|
meta:
|
||||||
risk_level = "medium"
|
risk_level = "medium"
|
||||||
category = "C7"
|
category = "C7"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "多种语言反弹 Shell 通用特征"
|
reference = "多种语言反弹 Shell 通用特征"
|
||||||
description = "通用反弹 Shell 标记:/dev/tcp 或 /dev/udp 或 reverse shell 工具名"
|
description = "通用反弹 Shell 标记:/dev/udp 或 攻击工具名 + shell 参数"
|
||||||
strings:
|
strings:
|
||||||
$devudp = /\/dev\/udp\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{2,5}/ ascii
|
$devudp = /\/dev\/udp\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{2,5}/ ascii
|
||||||
$bashc = /bash\s+-c\s+['"].*\/dev\/(tcp|udp)/ ascii
|
$bashc = /bash\s+-c\s+['"].*\/dev\/(tcp|udp)/ ascii
|
||||||
$dash_e = /\/bin\/bash\s+-e\s+['"]/ ascii
|
$dash_e = /\/bin\/bash\s+-e\s+['"]/ ascii
|
||||||
$hydra = /Hydra|crackmap|weevely|meterpreter|msfconsole|msfvenom/ ascii nocase
|
// 裸词 Hydra 会命中 "dehydrated"、crack 会命中 "cracklib",必须带参数或成组出现
|
||||||
|
$hydra = /\bhydra\s+(-l|-P|-p|-s|-m|-t)\b/ ascii nocase
|
||||||
|
$tool = /\b(crackmapexec|weevely|meterpreter|msfconsole|msfvenom)\b/ ascii nocase
|
||||||
$reverseshell = /reverse[_\s]?shell|shell[_\s]?shock/ ascii nocase
|
$reverseshell = /reverse[_\s]?shell|shell[_\s]?shock/ ascii nocase
|
||||||
|
$netart = /(nc|ncat)\s+\S+\s+\d{2,5}\s+-[ec]\b|\/dev\/(tcp|udp)\/|socat\s+EXEC:|bash\s+-i\s*>&/ ascii
|
||||||
condition:
|
condition:
|
||||||
any of ($devudp, $bashc, $dash_e) or any of ($hydra, $reverseshell)
|
any of ($devudp, $bashc, $dash_e)
|
||||||
|
or $hydra
|
||||||
|
or ($tool and $netart)
|
||||||
|
or ($reverseshell and $netart)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -177,40 +183,43 @@ rule C7_Linux_Persistence_SystemD {
|
|||||||
meta:
|
meta:
|
||||||
risk_level = "high"
|
risk_level = "high"
|
||||||
category = "C7"
|
category = "C7"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1543.002 Systemd Service"
|
reference = "MITRE ATT&CK T1543.002 Systemd Service"
|
||||||
description = "恶意 systemd unit 文件:ExecStart 指向 /tmp/.hidden + multi-user.target + 反弹命令"
|
description = "恶意 systemd unit:ExecStart 指向临时目录隐藏文件 / 管道执行 / 反弹命令"
|
||||||
strings:
|
strings:
|
||||||
$unit = /\[Unit\]/ ascii
|
$unit = /\[Unit\]/ ascii
|
||||||
$service = /\[Service\]/ ascii
|
$service = /\[Service\]/ ascii
|
||||||
$install = /\[Install\]/ ascii
|
$install = /\[Install\]/ ascii
|
||||||
$execstart = /ExecStart=/ ascii
|
$execstart = /ExecStart=/ ascii
|
||||||
$multiuser = /multi-user\.target/ ascii
|
$multiuser = /multi-user\.target/ ascii
|
||||||
$hiddenpath = /\/tmp\/\.?\w+|\/var\/tmp\/\.?\w+/ ascii
|
// 隐藏文件(点开头)落在可写临时目录——正常 unit 只会在 /usr/bin、/opt 等路径
|
||||||
$bashsh = /\/bin\/(ba)?sh\s+-c/ ascii
|
$hiddenpath = /\/(tmp|var\/tmp|dev\/shm)\/\.[a-zA-Z0-9_-]{2,}/ ascii
|
||||||
|
$pipe = /(curl|wget)\s+\S+\s*\|\s*(ba)?sh/ ascii
|
||||||
|
$revshell = /((nc|ncat|socat)\s+.*(-e|--exec|EXEC:)\s*\/bin\/(ba)?sh|\/dev\/tcp\/\d{1,3}\.\d{1,3})/ ascii
|
||||||
condition:
|
condition:
|
||||||
// 完整 unit 片段 + 可疑路径
|
($unit and $service and $execstart and any of ($hiddenpath, $pipe, $revshell))
|
||||||
$unit and $service and $execstart and ($hiddenpath or $bashsh)
|
|
||||||
or
|
or
|
||||||
// Install multi-user.target + ExecStart 反弹
|
($install and $multiuser and $execstart and any of ($hiddenpath, $pipe, $revshell))
|
||||||
$install and $multiuser and any of ($execstart, $bashsh)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rule C7_Linux_Persistence_LD_PRELOAD {
|
rule C7_Linux_Persistence_LD_PRELOAD {
|
||||||
meta:
|
meta:
|
||||||
risk_level = "high"
|
risk_level = "high"
|
||||||
category = "C7"
|
category = "C7"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1574.006 LD_PRELOAD"
|
reference = "MITRE ATT&CK T1574.006 LD_PRELOAD"
|
||||||
description = "LD_PRELOAD 劫持:环境变量注入 + .so 文件 + dlopen"
|
description = "LD_PRELOAD 劫持:写 ld.so.preload 或预加载临时目录里的 .so"
|
||||||
strings:
|
strings:
|
||||||
$ldpreload = /LD_PRELOAD=/ ascii
|
$ldpreload = /LD_PRELOAD=/ ascii
|
||||||
$sofile = /\.so(\.\d+)?:?/ ascii
|
|
||||||
$dlopen = "dlopen" ascii
|
|
||||||
$dlsym = "dlsym" ascii
|
|
||||||
$exportenv = /export\s+LD_PRELOAD/ ascii
|
$exportenv = /export\s+LD_PRELOAD/ ascii
|
||||||
|
$ldso = /ld\.so\.preload/ ascii
|
||||||
|
$tmpso = /\/(tmp|var\/tmp|dev\/shm|proc\/\d+\/root)\/[a-zA-Z0-9_.\/-]*\.so(\.[0-9]+)?/ ascii
|
||||||
condition:
|
condition:
|
||||||
($ldpreload or $exportenv) and ($sofile or $dlopen or $dlsym)
|
// /etc/ld.so.preload 是内核级后门常用落点,单独出现即算证据
|
||||||
|
$ldso
|
||||||
|
or
|
||||||
|
// LD_PRELOAD 指向可写临时目录的 so;README 里 LD_PRELOAD=libasan.so 不算
|
||||||
|
(any of ($ldpreload, $exportenv) and $tmpso)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -267,41 +276,55 @@ rule C7_Misc_Ruby_Lua_ReverseShell {
|
|||||||
meta:
|
meta:
|
||||||
risk_level = "medium"
|
risk_level = "medium"
|
||||||
category = "C7"
|
category = "C7"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "多语言反弹 Shell"
|
reference = "多语言反弹 Shell"
|
||||||
description = "Ruby/Lua 反弹 Shell:TCPSocket + exec/system"
|
description = "Ruby/Lua 反弹 Shell:Socket 对象 + 紧邻的 shell 执行调用"
|
||||||
strings:
|
strings:
|
||||||
$ruby = "TCPSocket" ascii
|
$ruby = "TCPSocket" ascii
|
||||||
|
$ruby2 = "Socket.tcp" ascii
|
||||||
$lua = "loadstring" ascii
|
$lua = "loadstring" ascii
|
||||||
$lua2 = "luasocket" ascii
|
$lua2 = "luasocket" ascii
|
||||||
$exec = /(exec|system|IO\.popen)\s+.*\/bin\/(ba)?sh/ ascii
|
$exec = /(\bexec|\bsystem|IO\.popen|\bpopen|os\.execute|os\.runtime)\s*\(?\s*['"]((\/bin\/)?(ba)?sh|nc\s+-e|\/dev\/tcp)[^'"]{0,48}/ ascii
|
||||||
condition:
|
condition:
|
||||||
any of ($ruby, $lua, $lua2) and ($exec or any of ($ruby, $lua, $lua2))
|
// 只要出现 TCPSocket/loadstring 就命中 = 任何内嵌 Lua 的程序(Redis、nginx-lua、
|
||||||
|
// Wireshark 插件)必中。语言标记与 shell 执行必须挨在一起(256B 内)。
|
||||||
|
$exec and (
|
||||||
|
for any i in (1..#exec): (
|
||||||
|
any of ($ruby, $ruby2, $lua, $lua2) in (@exec[i] - 256..@exec[i] + 256)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C7-5 网络投毒 / MITM 工具(内网横向移动)
|
// C7-5 网络投毒 / MITM 工具(内网横向移动)
|
||||||
|
// 工具名必须整词匹配,且与攻击参数成组出现
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C7_Network_Tools_Malicious {
|
rule C7_Network_Tools_Malicious {
|
||||||
meta:
|
meta:
|
||||||
risk_level = "medium"
|
risk_level = "medium"
|
||||||
category = "C7"
|
category = "C7"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "内网攻击工具"
|
reference = "内网攻击工具"
|
||||||
description = "内网攻击工具:ettercap/responder/nmap 扫描 + SMB/DNS 投毒"
|
description = "内网攻击工具:ettercap/responder/nmap + 投毒参数成组出现"
|
||||||
strings:
|
strings:
|
||||||
$ettercap = "ettercap" ascii nocase
|
$tool1 = /\bettercap\b/ ascii nocase
|
||||||
$responder = /responder(\.py)?/ ascii nocase
|
$tool2 = /\bresponder(\.py)?\b/ ascii nocase
|
||||||
$nmap = "nmap" ascii nocase
|
$tool3 = /\bnmap\b/ ascii nocase
|
||||||
$smb = /--smb/ ascii
|
// 攻击参数 / 漏洞编号(这些才是意图证据)
|
||||||
$ms17 = "ms17-010" ascii nocase
|
$art1 = /--smb/ ascii
|
||||||
$smbghost = "SMBGhost" ascii nocase
|
$art2 = /\bms17-010\b/ ascii nocase
|
||||||
$dnsspoof = /dns[-_]spoof|dns[-_]mitm/ ascii nocase
|
$art3 = /\bSMBGhost\b/ ascii nocase
|
||||||
$arppoison = /arp[-_]poison|arp[-_]spoof/ ascii nocase
|
$art4 = /\bdns[-_]spoof\b|\bdns[-_]mitm\b/ ascii nocase
|
||||||
|
$art5 = /\barp[-_]poison\b|\barp[-_]spoof\b/ ascii nocase
|
||||||
|
$art6 = /--Mitm|-T\s+text|\bec_sleep\b|\bntlm_relay\b|\bRunFinger\.py\b/ ascii nocase
|
||||||
condition:
|
condition:
|
||||||
any of ($ettercap, $responder, $dnsspoof, $arppoison)
|
// 投毒手法名本身就是明确证据
|
||||||
|
any of ($art4, $art5)
|
||||||
or
|
or
|
||||||
$nmap and ($smb or $ms17 or $smbghost)
|
// 工具名 + 攻击参数
|
||||||
|
($tool1 and any of ($art1, $art2, $art3, $art6))
|
||||||
|
or ($tool2 and any of ($art1, $art2, $art3, $art6))
|
||||||
|
or ($tool3 and any of ($art1, $art2, $art3))
|
||||||
}
|
}
|
||||||
|
|||||||
437
rules/pe.yar
437
rules/pe.yar
@@ -2,26 +2,34 @@
|
|||||||
// MeshSafe L2B YARA 规则集 —— Windows PE 恶意程序专项(C1 + C8)
|
// MeshSafe L2B YARA 规则集 —— Windows PE 恶意程序专项(C1 + C8)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
//
|
//
|
||||||
// 设计原则:
|
// 设计原则(v0.2.4 起,为消除干净软件误报而重写):
|
||||||
// - 所有规则使用 2-3 特征组合命中(API 组合 + C2 URL + Mutex 持久化等)
|
// 1. 全文搜 API 名 = 必然误报。系统 DLL 会「导出」这些 API(kernel32 里就有
|
||||||
// - binary.py 的 PE 结构分析负责:节熵 + 签名 + 可疑导入(单 API)
|
// VirtualAllocEx/WriteProcessMemory/CreateRemoteThread 的明文字符串),
|
||||||
// - YARA 负责:多 API 组合 + 字符串组合 + 壳 + 勒索专项
|
// 安全 SDK、安装器、调试组件同理。因此 C1 系列一律要求:
|
||||||
// - 命中后作为 finding 交给 PolicyEngine 权重累加,不直接零容忍
|
// - 文件是 PE(uint16(0) == 0x5A4D),且
|
||||||
|
// - 相关 API 名出现在同一个窗口内(导入名表是连续的,窗口 256~512B)。
|
||||||
|
// 导出表按字母序排列、跨度通常上 KB,落在窗口外 → 不误报;
|
||||||
|
// 真注入器/下载器的导入名表三条名字通常挨在一起 → 命中。
|
||||||
|
// 2. 不单独用「API 组合」定性,必须有硬指标(可疑路径 / 持久化键名 / C2 URL)。
|
||||||
|
// 3. 不用 3~4 字母裸串(曾经的 $persist5 = "RUN")和裸工具名。
|
||||||
|
// 4. binary.py 的 PE 结构分析负责节熵/签名/导入 API 分级;YARA 只做组合特征。
|
||||||
//
|
//
|
||||||
// 范围:
|
// 范围:
|
||||||
// - C1: Windows PE 恶意程序(注入器 / 下载器 / RAT / Loader)
|
// - C1: Windows PE 恶意程序(注入器 / 下载器 / RAT / Loader)
|
||||||
// - C8: 勒索 / 挖矿专项
|
// - C8: 勒索 / 挖矿专项(勒索样本常以文本/脚本形态投递,不加 PE 头判定)
|
||||||
// - C9: 壳 / 签名 / 结构异常(binary.py 已覆盖大部分,YARA 补充壳特征)
|
// - C9: 壳特征(识别后建议送沙箱,不作为恶意证据)
|
||||||
//
|
//
|
||||||
// 维护:
|
// 维护:
|
||||||
// - 新恶意家族出现时,优先补充"API 组合 + Mutex 名 + C2 URL"三件套
|
// - 新增规则先跑「干净 PE 回归」:C:/Windows/System32 全量 900+ 文件必须 0 命中
|
||||||
// - 壳特征(UPX/Themida/VMP)可识别后自动送沙箱深检
|
// - meta.risk_level 会被 binary.py 读取用于严重度展示,必须与档位权重一致
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
import "pe"
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C1-1 进程注入三件套(VirtualAllocEx + WriteProcessMemory + CreateRemoteThread)
|
// C1-1 进程注入三件套(VirtualAllocEx + WriteProcessMemory + CreateRemoteThread)
|
||||||
// binary.py 扫了 SuspiciousImports(单个 API),但 3 个同时出现 = 必然是注入器
|
// 判定要点:三个导入名必须在同一张导入名表里(512B 窗口),否则是导出表明文
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C1_PE_ProcessInjection {
|
rule C1_PE_ProcessInjection {
|
||||||
@@ -30,37 +38,66 @@ rule C1_PE_ProcessInjection {
|
|||||||
category = "C1"
|
category = "C1"
|
||||||
false_positive_rate = "low"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1055 Process Injection"
|
reference = "MITRE ATT&CK T1055 Process Injection"
|
||||||
description = "PE 文件同时导入 进程注入三件套 API = 远控/注入器特征"
|
description = "PE 导入表含进程注入三件套(同表紧邻)= 远控/注入器特征"
|
||||||
strings:
|
strings:
|
||||||
$s1 = "VirtualAllocEx" ascii
|
$alloc = "VirtualAllocEx" ascii
|
||||||
$s2 = "WriteProcessMemory" ascii
|
$write = "WriteProcessMemory" ascii
|
||||||
$s3 = "CreateRemoteThread" ascii
|
$thread = "CreateRemoteThread" ascii
|
||||||
// 备选:Nt 系列(NtUnmapViewOfSection + VirtualAllocEx + CreateRemoteThread)
|
// 进程镂空(Process Hollowing)
|
||||||
$n1 = "NtUnmapViewOfSection" ascii
|
$unmap1 = "NtUnmapViewOfSection" ascii
|
||||||
$n2 = "NtMapViewOfSection" ascii
|
$unmap2 = "ZwUnmapViewOfSection" ascii
|
||||||
$n3 = "ZwUnmapViewOfSection" ascii
|
// APC / Hook 注入
|
||||||
// 也可能是 QueueUserAPC 注 APC 注入
|
|
||||||
$apc = "QueueUserAPC" ascii
|
$apc = "QueueUserAPC" ascii
|
||||||
// 或 SetWindowsHookEx DLL 注入
|
|
||||||
$hook = "SetWindowsHookEx" ascii
|
$hook = "SetWindowsHookEx" ascii
|
||||||
|
$open = "OpenProcess" ascii
|
||||||
condition:
|
condition:
|
||||||
// 经典三件套(必须同时命中)
|
uint16(0) == 0x5A4D
|
||||||
$s1 and $s2 and $s3
|
and
|
||||||
|
(
|
||||||
|
// 经典三件套:同表紧邻
|
||||||
|
(
|
||||||
|
$alloc and $write and $thread
|
||||||
|
and for any i in (1..#alloc): (
|
||||||
|
$write in (@alloc[i] - 512..@alloc[i] + 512)
|
||||||
|
and $thread in (@alloc[i] - 512..@alloc[i] + 512)
|
||||||
|
)
|
||||||
|
)
|
||||||
or
|
or
|
||||||
// Nt 系列内存注入(至少 2 个 Nt + 1 个注入)
|
// 镂空注入:unmap 必须和 write+thread 同表(kernel32/shell32 只是从
|
||||||
(any of ($n1, $n2, $n3) and any of ($s1, $s3, $apc))
|
// ntdll 导入成对的 NtMapView/NtUnmap,没有写入+远线程就不算镂空注入)
|
||||||
|
(
|
||||||
|
$write and $thread
|
||||||
|
and for any i in (1..#unmap1): (
|
||||||
|
$write in (@unmap1[i] - 512..@unmap1[i] + 512)
|
||||||
|
and $thread in (@unmap1[i] - 512..@unmap1[i] + 512)
|
||||||
|
)
|
||||||
|
)
|
||||||
or
|
or
|
||||||
// APC 注入 + VirtualAllocEx
|
(
|
||||||
$apc and $s1
|
$write and $thread
|
||||||
|
and for any i in (1..#unmap2): (
|
||||||
|
$write in (@unmap2[i] - 512..@unmap2[i] + 512)
|
||||||
|
and $thread in (@unmap2[i] - 512..@unmap2[i] + 512)
|
||||||
|
)
|
||||||
|
)
|
||||||
or
|
or
|
||||||
// Hook DLL 注入
|
// APC / Hook 注入:目标进程 API + 注入 API 同表紧邻
|
||||||
$hook and $s1
|
(
|
||||||
|
$alloc and $open and ($apc or $hook)
|
||||||
|
and for any i in (1..#alloc): (
|
||||||
|
$open in (@alloc[i] - 512..@alloc[i] + 512)
|
||||||
|
and ($apc in (@alloc[i] - 512..@alloc[i] + 512)
|
||||||
|
or $hook in (@alloc[i] - 512..@alloc[i] + 512))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C1-2 下载器组合(网络 API + 本地持久化 / 执行)
|
// C1-2 下载器 / Loader(下载 API + 落地持久化键名,且两者同表紧邻)
|
||||||
// 单一 URLDownloadToFile 不致命,但 "下载 + 执行 + 持久化" 三件套 = Loader
|
// v0.2.3 的写法:任意下载 API + "WriteFile" + "CreateProcess" + 裸串 "RUN"
|
||||||
|
// → 任何带自动更新的正常软件必中。现在要求持久化硬指标。
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C1_PE_Downloader_Loader {
|
rule C1_PE_Downloader_Loader {
|
||||||
@@ -69,157 +106,172 @@ rule C1_PE_Downloader_Loader {
|
|||||||
category = "C1"
|
category = "C1"
|
||||||
false_positive_rate = "low"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1105 Ingress Tool Transfer"
|
reference = "MITRE ATT&CK T1105 Ingress Tool Transfer"
|
||||||
description = "下载器组合:HTTP/FTP 下载 + 写入到可疑路径 + 执行"
|
description = "下载 API 与自启动/服务注册持久化键名同表共存 = Loader"
|
||||||
strings:
|
strings:
|
||||||
// 网络下载 API(任选其一)
|
// 下载类 API(保留专有名词,不用裸串)
|
||||||
$net1 = "URLDownloadToFile" ascii
|
$net1 = "URLDownloadToFile" ascii
|
||||||
$net2 = "InternetOpenUrl" ascii
|
$net2 = "InternetOpenUrl" ascii
|
||||||
$net3 = "WinHttpOpen" ascii
|
$net3 = "WinHttpOpenRequest" ascii
|
||||||
$net4 = "WinHttpOpenRequest" ascii
|
$net4 = "WinHttpSendRequest" ascii
|
||||||
$net5 = "FtpOpenFile" ascii
|
$net5 = "URLDownloadToCacheFile" ascii
|
||||||
$net6 = "DownloadFile" ascii
|
$net6 = "FtpOpenFile" ascii
|
||||||
$net7 = "WinHttpSendRequest" ascii
|
$net7 = "WinHttpOpen" ascii
|
||||||
|
$net8 = "InternetOpen" ascii
|
||||||
|
|
||||||
// 写入/保存到本地
|
// 自启动 / 服务注册持久化硬指标(不是裸 "RUN")
|
||||||
$save1 = "WriteFile" ascii
|
$p1 = "Software\\Microsoft\\Windows\\CurrentVersion\\Run" ascii nocase
|
||||||
$save2 = "SaveToFile" ascii
|
$p2 = "CurrentVersion\\RunOnce" ascii nocase
|
||||||
$save3 = "URLDownloadToCacheFile" ascii
|
$p3 = "schtasks.exe" ascii nocase
|
||||||
|
$p4 = "CreateService" ascii
|
||||||
|
$p5 = "Start Menu\\Programs\\StartUp" ascii nocase
|
||||||
|
$p6 = "reg add" ascii nocase
|
||||||
|
$p7 = "StartupApproved" ascii nocase
|
||||||
|
|
||||||
// 执行下载的载荷
|
// 临时目录落地(Loader 常先落 %TEMP%)
|
||||||
$exec1 = "WinExec" ascii
|
$tmp1 = /\\AppData\\Local\\Temp\\[a-z0-9_. -]{3,60}\.(exe|dll|tmp|dat)/ ascii nocase
|
||||||
$exec2 = "CreateProcess" ascii
|
$tmp2 = /%TEMP%\\[a-z0-9_. -]{3,60}\.(exe|dll|tmp)/ ascii nocase
|
||||||
$exec3 = "ShellExecute" ascii
|
$tmp3 = /\\Windows\\Temp\\[a-z0-9_. -]{3,60}\.(exe|dll|tmp)/ ascii nocase
|
||||||
$exec4 = "CreateRemoteThread" ascii
|
|
||||||
|
|
||||||
// 持久化(注册表/启动项)
|
|
||||||
$persist1 = "RegSetValueEx" ascii
|
|
||||||
$persist2 = "SetServiceObjectSecurity" ascii
|
|
||||||
$persist3 = "CreateService" ascii
|
|
||||||
$persist4 = "schtasks.exe" ascii
|
|
||||||
$persist5 = "RUN" ascii // HKCU\Software\Microsoft\Windows\CurrentVersion\Run
|
|
||||||
condition:
|
condition:
|
||||||
// 下载 + 保存 + 执行(三件套)
|
uint16(0) == 0x5A4D
|
||||||
any of ($net*) and any of ($save*) and any of ($exec*)
|
and any of ($net*)
|
||||||
or
|
and any of ($p*)
|
||||||
// 下载 + 持久化(即使没执行也是准备投毒)
|
and
|
||||||
any of ($net*) and any of ($persist*)
|
(
|
||||||
or
|
// 下载 API 与持久化键名同处一个导入/字符串表块
|
||||||
// 执行 + 持久化(下载可能在运行时动态完成)
|
for any i in (1..#net1): ( any of ($p*) in (@net1[i] - 1024..@net1[i] + 1024) )
|
||||||
any of ($exec1, $exec2, $exec3) and any of ($persist*)
|
or (any of ($tmp*) and 2 of ($p*))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C1-3 RAT / 远控特征(C2 通信 + 命令执行 + 持久化)
|
// C1-3 RAT / 远控特征
|
||||||
|
// v0.2.3 的写法用 send( / recv( / system( 这类通用符号,且
|
||||||
|
// (bot|serv|gate|ops)\.(com|net) 会匹配任意域名字符串 → 正常联网程序必中。
|
||||||
|
// 现在要求:C2 硬指标(IP:PORT、可执行下载 URL、家族标记)+ 套接字 API 同表。
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C1_PE_RAT_CommandControl {
|
rule C1_PE_RAT_CommandControl {
|
||||||
meta:
|
meta:
|
||||||
risk_level = "critical"
|
risk_level = "critical"
|
||||||
category = "C1"
|
category = "C1"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1071 Application Layer Protocol"
|
reference = "MITRE ATT&CK T1071 Application Layer Protocol"
|
||||||
description = "RAT C2 通信特征:WinHTTP/WSASend + 命令执行 API + 可疑 URL/域名"
|
description = "RAT C2 特征:套接字 API + 明确 C2 指标(IP:端口 / .exe 下载 / 家族标记)"
|
||||||
strings:
|
strings:
|
||||||
// C2 网络通道
|
// 套接字 API(不用裸 send/recv/connect)
|
||||||
$c2_send = /WSASend|send\s*\(|sendto|WinHttpSendRequest|InternetWriteFile/ ascii
|
$sock1 = "WSASend" ascii
|
||||||
$c2_recv = /WSARecv|recv\s*\(|recvfrom|WinHttpReadData|InternetReadFile/ ascii
|
$sock2 = "WSARecv" ascii
|
||||||
$c2_connect = /WSAConnect|connect\s*\(|WinHttpConnect|InternetConnect/ ascii
|
$sock3 = "WSAConnect" ascii
|
||||||
|
$sock4 = "WSASocket" ascii
|
||||||
|
$sock5 = "inet_ntoa" ascii
|
||||||
|
$sock6 = "getpeername" ascii
|
||||||
|
|
||||||
// 命令执行
|
// 命令执行(真实 RAT 才会与 C2 同表出现)
|
||||||
$cmd = /CreateProcess|ShellExecute|WinExec|system\s*\(|popen/ ascii
|
$cmd1 = "CreateProcess" ascii
|
||||||
|
$cmd2 = "ShellExecute" ascii
|
||||||
|
$cmd3 = "WinExec" ascii
|
||||||
|
|
||||||
// 可疑 C2 URL(可执行文件里出现 http://...exe 或常见 C2 域名)
|
// C2 硬指标
|
||||||
$c2url1 = /http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}/ ascii
|
$c2ip = /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:[1-5][0-9]{3}/ ascii
|
||||||
$c2url2 = /https?:\/\/[^\"\s'\)]+\.(ru|top|xyz|tk|ml|ga|gq|cf|pw|click|stream|xyz|buzz|loan)\/[^\"\s'\)]*\.exe/ ascii
|
$c2exe = /https?:\/\/[^\s"']{4,}\.(exe|dll|scr|vbs|ps1|bat)\b/ ascii nocase
|
||||||
$c2url3 = /(c2|panel|gate|bot|ops|serv|beacon|stage|stager)\.(ru|top|xyz|cc|io|com|net)/ ascii
|
$c2uri = /(gate|panel|submit|checkin|beacon|report)\.(php|aspx|html)\b/ ascii nocase
|
||||||
|
$c2tld = /\b[a-z0-9-]{4,30}\.(ru|top|xyz|tk|ml|ga|gq|cf|pw|buzz|loan)\b/ ascii nocase
|
||||||
condition:
|
condition:
|
||||||
// C2 通信 + 命令执行(至少两个网络 + 一个命令)
|
uint16(0) == 0x5A4D
|
||||||
any of ($c2_send, $c2_recv, $c2_connect) and any of ($cmd*) and any of ($c2url*)
|
and 2 of ($sock*)
|
||||||
or
|
and any of ($c2*)
|
||||||
// C2 URL 模式 + 网络 API
|
and
|
||||||
any of ($c2url*) and any of ($c2_connect, $c2_send)
|
(
|
||||||
|
any of ($cmd*)
|
||||||
|
or $c2exe
|
||||||
|
or $c2uri
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C1-4 Mutex 互斥体名(勒索 / RAT / Loader 的持久化标记)
|
// C1-4 Mutex 互斥体名(勒索 / RAT / Loader 的持久化标记)
|
||||||
// 单一 Mutex 名有一定价值,但必须配合其他特征才触发
|
// 需要 CreateMutex 与家族 Mutex 名同表,避免文本文档里提一嘴家族名就命中
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C1_PE_Mutex_Persistence {
|
rule C1_PE_Mutex_Persistence {
|
||||||
meta:
|
meta:
|
||||||
risk_level = "medium"
|
risk_level = "medium"
|
||||||
category = "C1"
|
category = "C1"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1057 Process Discovery (mutex as persistence marker)"
|
reference = "MITRE ATT&CK T1057 Process Discovery (mutex as persistence marker)"
|
||||||
description = "可疑 Mutex 名称 + CreateMutex API = 持久化标记"
|
description = "CreateMutex + 已知家族 Mutex 名同表 = 持久化标记"
|
||||||
strings:
|
strings:
|
||||||
$createmutex = "CreateMutexA" ascii
|
$createmutex = "CreateMutexA" ascii
|
||||||
$createmutexw = "CreateMutexW" ascii
|
$createmutexw = "CreateMutexW" ascii
|
||||||
// 常见勒索/RAT Mutex 名(大小写不敏感,含 Unicode 变体用 wide)
|
|
||||||
// LockBit
|
// LockBit
|
||||||
$lockbit1 = /Global\\[Ll]ock[Bb]it/ ascii
|
$lockbit1 = /Global\\[Ll]ock[Bb]it/ ascii
|
||||||
$lockbit2 = /Local\\[Ll]ock[Bb]it/ ascii
|
$lockbit2 = /Local\\[Ll]ock[Bb]it/ ascii
|
||||||
// BlackCat / ALPHV
|
// BlackCat / ALPHV
|
||||||
$blackcat = /Global\\[Bb]lack[Cc]at|[Aa][Ll][Pp][Hh][Vv]/ ascii
|
$blackcat = /(Global|Local)\\[Bb]lack[Cc]at|\bALPHV\b/ ascii
|
||||||
// Ryuk
|
// Ryuk
|
||||||
$ryuk = /RyukTask[\d]|Global\\Ryuk/ ascii
|
$ryuk = /RyukTask[\d]|Global\\Ryuk/ ascii
|
||||||
// Conti
|
// Conti
|
||||||
$conti = /Global\\[Cc]onti|Local\\[Cc]onti/ ascii
|
$conti = /(Global|Local)\\[Cc]onti/ ascii
|
||||||
// Pseudo/Dharma
|
// Pseudo/Dharma
|
||||||
$dharma = /Global\\[Dd]harma|Local\\[Dd]harma/ ascii
|
$dharma = /(Global|Local)\\[Dd]harma/ ascii
|
||||||
// 通用可疑模式:Mutex 名包含 "Mutex" + UUID 变体
|
// 通用可疑模式:Mutex 名含 UUID 前缀
|
||||||
$generic1 = /Global\\Mutex[0-9a-f]{8}/ ascii
|
$generic1 = /Global\\Mutex[0-9a-f]{8}/ ascii
|
||||||
// Kryptik/Qakbot
|
// Kryptik/Qakbot
|
||||||
$qakbot = /[Qq]akbot|[Kk]ryptik/ ascii
|
$qakbot = /\b[Qq]akbot\b|\b[Kk]ryptik\b/ ascii
|
||||||
// Cobalt Strike named pipe
|
// Cobalt Strike named pipe
|
||||||
$cs1 = /\\\\\.\\pipe\\msagent_[0-9a-f]+/ ascii
|
$cs1 = /\\\\\.\\pipe\\msagent_[0-9a-f]+/ ascii
|
||||||
$cs2 = /\\\\\.\\pipe\\[Bb]eacon_[0-9a-f]+/ ascii
|
$cs2 = /\\\\\.\\pipe\\[Bb]eacon_[0-9a-f]+/ ascii
|
||||||
condition:
|
condition:
|
||||||
// CreateMutex + 已知家族 Mutex 名
|
uint16(0) == 0x5A4D
|
||||||
(any of ($createmutex*)) and any of ($lockbit1, $lockbit2, $blackcat, $ryuk, $conti, $dharma, $qakbot)
|
and
|
||||||
or
|
(
|
||||||
// Cobalt Strike 命名管道特征(不需要 CreateMutex)
|
|
||||||
any of ($cs*)
|
any of ($cs*)
|
||||||
or
|
or (
|
||||||
// CreateMutex + 通用可疑 UUID Mutex
|
any of ($createmutex*)
|
||||||
(any of ($createmutex*)) and $generic1
|
and any of ($lockbit1, $lockbit2, $blackcat, $ryuk, $conti, $dharma, $qakbot, $generic1)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C1-5 DLL 劫持 / LoadLibrary 动态加载
|
// C1-5 DLL 劫持 / 侧加载
|
||||||
|
// 只看「LoadLibrary + GetProcAddress + 临时目录里的 DLL 名」三件套;
|
||||||
|
// \ProgramData\ 与 \Public\ 是正常安装器路径,不再单独作为可疑路径
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C1_PE_DLLHijacking {
|
rule C1_PE_DLLHijacking {
|
||||||
meta:
|
meta:
|
||||||
risk_level = "high"
|
risk_level = "high"
|
||||||
category = "C1"
|
category = "C1"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1574 Hijack Execution Flow"
|
reference = "MITRE ATT&CK T1574 Hijack Execution Flow"
|
||||||
description = "PE 动态加载多个系统 DLL + 非系统路径 = DLL 劫持迹象"
|
description = "动态加载 API + 临时目录 DLL 落地 = DLL 侧加载/劫持"
|
||||||
strings:
|
strings:
|
||||||
$loadlib = "LoadLibraryA" ascii
|
$loadlib = "LoadLibraryA" ascii
|
||||||
$loadlibw = "LoadLibraryW" ascii
|
$loadlibw = "LoadLibraryW" ascii
|
||||||
$getproc = "GetProcAddress" ascii
|
$getproc = "GetProcAddress" ascii
|
||||||
// 可疑加载路径(非 System32)
|
$copy1 = "CopyFileA" ascii
|
||||||
$path1 = /\\Users\\[^\\]+\\AppData\\Local\\Temp/ ascii
|
$copy2 = "CopyFileW" ascii
|
||||||
$path2 = /\\AppData\\Local\\Temp/ ascii
|
$move = "MoveFileEx" ascii
|
||||||
$path3 = /\\ProgramData\\/ ascii
|
$temp1 = /\\AppData\\Local\\Temp\\[a-z0-9_. -]{3,40}\.dll/ ascii nocase
|
||||||
$path4 = /\\Public\\Documents\\/ ascii
|
$temp2 = /%TEMP%\\[a-z0-9_. -]{3,40}\.dll/ ascii nocase
|
||||||
|
$temp3 = /\\Windows\\Temp\\[a-z0-9_. -]{3,40}\.dll/ ascii nocase
|
||||||
|
$sysdir = "SetDllDirectory" ascii
|
||||||
|
$searchorder = "LoadLibraryEx" ascii
|
||||||
condition:
|
condition:
|
||||||
// LoadLibrary + 可疑路径 + GetProcAddress = 完整劫持链
|
uint16(0) == 0x5A4D
|
||||||
(any of ($loadlib*)) and any of ($path*) and $getproc
|
and any of ($loadlib*, $searchorder)
|
||||||
or
|
and any of ($temp*)
|
||||||
// LoadLibrary + 多个可疑路径
|
and (any of ($getproc, $copy1, $copy2, $move, $sysdir))
|
||||||
(any of ($loadlib*)) and 2 of ($path*)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C1-6 LOLBin(Living Off The Land Binary)调用特征
|
// C1-6 LOLBin(Living Off The Land Binary)调用特征
|
||||||
// 已知合法 Windows 二进制被恶意利用来执行命令
|
// 命令行串 + 进程创建 API 必须同表紧邻(正常程序会把两者写进同一个导入/资源块)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C1_PE_LOLBin_Usage {
|
rule C1_PE_LOLBin_Usage {
|
||||||
@@ -228,31 +280,57 @@ rule C1_PE_LOLBin_Usage {
|
|||||||
category = "C1"
|
category = "C1"
|
||||||
false_positive_rate = "low"
|
false_positive_rate = "low"
|
||||||
reference = "https://lolbas-project.github.io/"
|
reference = "https://lolbas-project.github.io/"
|
||||||
description = "PE 文件中出现 LOLBin 调用:certutil / mshta / rundll32 / regsvr32 / bitsadmin / wmic / msiexec"
|
description = "PE 中出现 LOLBin 调用命令行 + 进程创建 API(同表)"
|
||||||
strings:
|
strings:
|
||||||
$certutil = /certutil(\.exe)?\s+(-urlcache|-decode|-encode|-hashfile)/ ascii
|
$lolbin1 = /certutil(\.exe)?\s+(-urlcache|-decode)\s+-split/ ascii nocase
|
||||||
$mshta = /mshta(\.exe)?\s+(https?:|javascript:|vbscript:)/ ascii
|
$lolbin2 = /mshta(\.exe)?\s+(https?:|javascript:|vbscript:)/ ascii nocase
|
||||||
$rundll32 = /rundll32(\.exe)?\s+[A-Za-z0-9_.]+,/ ascii
|
$lolbin3 = /regsvr32(\.exe)?\s+\/s\s+\/u:http/ ascii nocase
|
||||||
$regsvr32 = /regsvr32(\.exe)?\s+\/s\s+\/u/ ascii
|
$lolbin4 = /bitsadmin(\.exe)?\s+\/transfer\s+\/source:/ ascii nocase
|
||||||
$bitsadmin = /bitsadmin(\.exe)?\s+(\/transfer|\/create)/ ascii
|
$lolbin5 = /wmic(\.exe)?\s+process\s+call\s+create/ ascii nocase
|
||||||
$wmic = /wmic(\.exe)?\s+(process\s+call\s+create|product\s+call)/ ascii
|
$lolbin6 = /msiexec(\.exe)?\s+\/[ia]\s+https?:/ ascii nocase
|
||||||
$msiexec = /msiexec(\.exe)?\s+\/[ia]\s+https?:/ ascii
|
$lolbin7 = /installutil(\.exe)?\s+\/u\s+\/logfile=https?:/ ascii nocase
|
||||||
$installutil = /installutil(\.exe)?\s+\/u\s+\/logfile=/ ascii
|
$lolbin8 = /psexec(\.exe)?\s+(-accepteula\s+)?-w\s+https?:/ ascii nocase
|
||||||
$syncapp = /syncapp(\.exe)?\s+\/compattrack/ ascii
|
|
||||||
$psexec = /psexec(\.exe)?\s+(-accepteula|-i\s+\d+|-w\s+https?:)/ ascii
|
|
||||||
// 命令执行 API(用于组合条件)
|
|
||||||
$exec1 = "CreateProcess" ascii
|
$exec1 = "CreateProcess" ascii
|
||||||
$exec2 = "ShellExecute" ascii
|
$exec2 = "ShellExecute" ascii
|
||||||
$exec3 = "WinExec" ascii
|
$exec3 = "WinExec" ascii
|
||||||
condition:
|
condition:
|
||||||
// 至少 1 个 LOLBin 调用 + 命令执行 API
|
uint16(0) == 0x5A4D
|
||||||
any of ($certutil*, $mshta*, $rundll32*, $regsvr32*, $bitsadmin*, $wmic*, $msiexec*, $installutil*, $syncapp*, $psexec*)
|
and any of ($lolbin*)
|
||||||
and any of ($exec*)
|
and any of ($exec*)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C8-1 勒索加密 API 组合(CryptEncrypt + BCrypt* + 文件遍历 + 删除卷影)
|
// C1-7 PE 高熵 + 网络 API(疑似加壳下载器)
|
||||||
|
// 需要「多个网络 API + 明确壳节名」同时成立
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
rule C1_PE_EntropyHighNetwork {
|
||||||
|
meta:
|
||||||
|
risk_level = "medium"
|
||||||
|
category = "C1"
|
||||||
|
false_positive_rate = "low"
|
||||||
|
reference = "binary.py HighEntropySection 联动"
|
||||||
|
description = "PE 加壳节名 + 多个下载 API = 加壳下载器"
|
||||||
|
strings:
|
||||||
|
$net1 = "InternetOpenUrl" ascii
|
||||||
|
$net2 = "URLDownloadToFile" ascii
|
||||||
|
$net3 = "WinHttpOpenRequest" ascii
|
||||||
|
$net4 = "WSAConnect" ascii
|
||||||
|
condition:
|
||||||
|
uint16(0) == 0x5A4D
|
||||||
|
and 2 of ($net*)
|
||||||
|
and (
|
||||||
|
for any i in (0..pe.number_of_sections - 1): (
|
||||||
|
pe.sections[i].name matches /UPX0/ or pe.sections[i].name == "UPX1"
|
||||||
|
or pe.sections[i].name matches /\.vmp[0-9]/ or pe.sections[i].name matches /Themida|\.themida/
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// C8-1 勒索加密链(加密 API + 卷影删除/勒索信,勒索样本常以文本形态投递,不加 PE 头)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C8_Ransomware_Encryption {
|
rule C8_Ransomware_Encryption {
|
||||||
@@ -261,9 +339,9 @@ rule C8_Ransomware_Encryption {
|
|||||||
category = "C8"
|
category = "C8"
|
||||||
false_positive_rate = "low"
|
false_positive_rate = "low"
|
||||||
reference = "MITRE ATT&CK T1486 Data Encrypted for Impact"
|
reference = "MITRE ATT&CK T1486 Data Encrypted for Impact"
|
||||||
description = "勒索加密链:CryptEncrypt/BCryptEncrypt + 大量文件遍历 + 卷影删除 = 勒索"
|
description = "勒索加密链:加密 API + 卷影删除命令或勒索信文件名"
|
||||||
strings:
|
strings:
|
||||||
// 加密 API(至少出现两个)
|
// 加密 API
|
||||||
$enc1 = "CryptEncrypt" ascii
|
$enc1 = "CryptEncrypt" ascii
|
||||||
$enc2 = "CryptDeriveKey" ascii
|
$enc2 = "CryptDeriveKey" ascii
|
||||||
$enc3 = "BCryptEncrypt" ascii
|
$enc3 = "BCryptEncrypt" ascii
|
||||||
@@ -276,25 +354,23 @@ rule C8_Ransomware_Encryption {
|
|||||||
$enum3 = "FindNextFileA" ascii
|
$enum3 = "FindNextFileA" ascii
|
||||||
$enum4 = "FindNextFileW" ascii
|
$enum4 = "FindNextFileW" ascii
|
||||||
|
|
||||||
// 卷影删除(勒索前置)
|
// 卷影删除(完整命令,不认裸 "wmic")
|
||||||
$vss1 = "vssadmin" ascii
|
$vss1 = "vssadmin" ascii nocase
|
||||||
$vss2 = "delete shadows" ascii
|
$vss2 = "delete shadows" ascii nocase
|
||||||
$vss3 = "DeleteShadowCopies" ascii
|
$vss3 = "DeleteShadowCopies" ascii
|
||||||
$vss4 = "wmic" ascii
|
$vss4 = "shadow copies" ascii nocase
|
||||||
|
|
||||||
// 勒索信文件名变体(出现在二进制里)
|
// 勒索信文件名 / 加密扩展名
|
||||||
$ransomnote1 = /(HOW_TO_RECOVER|RESTORE_FILES|DECRYPT_INSTRUCTIONS|README|HELP_DECRYPT|_readme|_restore)[\w.-]*\.(txt|html|png|jpg|bmp|url)/ nocase
|
$note1 = /(HOW_TO_RECOVER|RESTORE_FILES|DECRYPT_INSTRUCTIONS|HELP_DECRYPT)[\w.-]*\.(txt|html|png|jpg|bmp|url)/ nocase
|
||||||
$ransomnote2 = /\.(locky|zepto|cryptolocker|tor|crpt|encrypted|virus|wallet|pay2)/ nocase
|
$note2 = /\.(locky|zepto|cryptolocker|crpt|encrypted|pay2)\b/ nocase
|
||||||
$ransomnote3 = /bitcoin|bitcash|monero|ltc|BTC\b/ ascii
|
|
||||||
condition:
|
condition:
|
||||||
// 加密 API + 文件遍历 = 加密工具(必须至少 2 个加密 API)
|
any of ($enc*)
|
||||||
(2 of ($enc*)) and any of ($enum*)
|
and
|
||||||
or
|
(
|
||||||
// 加密 API + 卷影删除 = 完整勒索
|
($vss1 and ($vss2 or $vss3 or $vss4))
|
||||||
(any of ($enc*)) and any of ($vss*)
|
or (2 of ($enum*) and any of ($note*))
|
||||||
or
|
or (2 of ($enc*) and any of ($enum*) and $vss1)
|
||||||
// 加密 API + 勒索信 + 加密文件扩展名 = 明确勒索
|
)
|
||||||
(any of ($enc*)) and (any of ($ransomnote1, $ransomnote2)) and $ransomnote3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -310,53 +386,51 @@ rule C8_Mining_Monero {
|
|||||||
reference = "MITRE ATT&CK T1496 Resource Hijacking"
|
reference = "MITRE ATT&CK T1496 Resource Hijacking"
|
||||||
description = "挖矿程序特征:xmrig 命令行参数 / stratum+tcp / 矿池域名"
|
description = "挖矿程序特征:xmrig 命令行参数 / stratum+tcp / 矿池域名"
|
||||||
strings:
|
strings:
|
||||||
// xmrig 命令行特征
|
|
||||||
$xmrig1 = "xmrig" ascii nocase
|
$xmrig1 = "xmrig" ascii nocase
|
||||||
$xmrig2 = /--donate-level\s+\d+/ ascii
|
$xmrig2 = /--donate-level\s+\d+/ ascii
|
||||||
$xmrig3 = /-o\s+stratum\+tcp/ ascii
|
$xmrig3 = /-o\s+stratum\+tcp/ ascii
|
||||||
$xmrig4 = /--cpu-priority\s+\d/ ascii
|
$xmrig4 = /--cpu-priority\s+\d/ ascii
|
||||||
$xmrig5 = /--max-threads-hint/ ascii
|
$xmrig5 = /--max-threads-hint/ ascii
|
||||||
|
|
||||||
// 矿池域名(Monero)
|
|
||||||
$pool1 = "pool.xmrig.com" ascii nocase
|
$pool1 = "pool.xmrig.com" ascii nocase
|
||||||
$pool2 = "pool.minexmr.com" ascii nocase
|
$pool2 = "pool.minexmr.com" ascii nocase
|
||||||
$pool3 = "pool.supportxmr.com" ascii nocase
|
$pool3 = "pool.supportxmr.com" ascii nocase
|
||||||
$pool4 = "mine.moneropool.com" ascii nocase
|
$pool4 = "mine.moneropool.com" ascii nocase
|
||||||
$pool5 = "stratum+tcp://" ascii
|
$pool5 = "stratum+tcp://" ascii
|
||||||
|
|
||||||
// CPU 高占用循环特征
|
|
||||||
$loop = /(while|for)\s*\(\s*\d+\s*[<>=]/ ascii
|
|
||||||
$thread = "CreateThread" ascii
|
$thread = "CreateThread" ascii
|
||||||
condition:
|
condition:
|
||||||
// 至少 2 个 xmrig 特征
|
any of ($pool1, $pool2, $pool3, $pool4)
|
||||||
(any of ($xmrig1, $xmrig2, $xmrig3, $xmrig4, $xmrig5)) and 2 of ($*)
|
or ($pool5 and (any of ($xmrig*) or $thread))
|
||||||
or
|
or (2 of ($xmrig*) and any of ($pool*))
|
||||||
// stratum+tcp 矿池协议 + 多个线程创建
|
|
||||||
$pool5 and ($thread or 2 of ($xmrig*))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// C9 壳特征(UPX / Themida / VMProtect / Enigma)
|
// C9 壳特征(UPX / Themida / VMProtect)
|
||||||
// 壳识别后建议升级到 L3 沙箱深检,不单独阻断
|
// 加壳本身不是恶意证据(大量开源工具用 UPX),只作沙箱深检触发条件
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
rule C9_Packer_UPX {
|
rule C9_Packer_UPX {
|
||||||
meta:
|
meta:
|
||||||
risk_level = "medium"
|
risk_level = "low"
|
||||||
category = "C9"
|
category = "C9"
|
||||||
false_positive_rate = "low"
|
false_positive_rate = "low"
|
||||||
reference = "UPX Packer"
|
reference = "UPX Packer"
|
||||||
description = "UPX 加壳 PE:节名 UPX0 / UPX1 + UPX 签名"
|
description = "UPX 加壳 PE:节名 UPX0/UPX1 + UPX 签名"
|
||||||
strings:
|
strings:
|
||||||
$upx0 = "UPX0" ascii
|
|
||||||
$upx1 = "UPX1" ascii
|
|
||||||
$upx2 = "UPX!" ascii
|
$upx2 = "UPX!" ascii
|
||||||
$upx3 = /UPX\s+\d+\.\d+/ ascii // "UPX 3.96" 这类版本字符串
|
|
||||||
$upx4 = /UPX\b/ ascii
|
|
||||||
condition:
|
condition:
|
||||||
// 节名(两个)+ 签名
|
uint16(0) == 0x5A4D
|
||||||
($upx0 and $upx1) or ($upx2 and any of ($upx3, $upx4))
|
and (
|
||||||
|
(
|
||||||
|
for any i in (0..pe.number_of_sections - 1): (
|
||||||
|
pe.sections[i].name matches /UPX0/
|
||||||
|
)
|
||||||
|
and for any j in (0..pe.number_of_sections - 1): (
|
||||||
|
pe.sections[j].name matches /UPX1/
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or ($upx2 and pe.number_of_sections <= 4)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule C9_Packer_Themida_VMP {
|
rule C9_Packer_Themida_VMP {
|
||||||
@@ -365,39 +439,18 @@ rule C9_Packer_Themida_VMP {
|
|||||||
category = "C9"
|
category = "C9"
|
||||||
false_positive_rate = "low"
|
false_positive_rate = "low"
|
||||||
reference = "Themida / VMProtect"
|
reference = "Themida / VMProtect"
|
||||||
description = "商业壳特征:Themida / VMProtect 产品标识字符串"
|
description = "商业壳特征:Themida / VMProtect 节名 + 产品标识字符串"
|
||||||
strings:
|
strings:
|
||||||
$themida1 = "Themida" ascii wide
|
$themida1 = "Themida" ascii wide
|
||||||
$themida2 = "WinLicense" ascii wide
|
$themida2 = "WinLicense" ascii wide
|
||||||
$themida3 = "Oreans" ascii wide
|
$themida3 = "Oreans" ascii wide
|
||||||
$vmp1 = "VMProtect" ascii wide
|
$vmp1 = "VMProtect" ascii wide
|
||||||
$vmp3 = "VMProtect SDK" ascii wide
|
|
||||||
condition:
|
condition:
|
||||||
any of ($themida*) or any of ($vmp*)
|
uint16(0) == 0x5A4D
|
||||||
}
|
and (
|
||||||
|
for any i in (0..pe.number_of_sections - 1): (
|
||||||
|
pe.sections[i].name matches /\.themida|\.vmp[0-9]/
|
||||||
// =============================================================================
|
)
|
||||||
// C1-7 PE 高熵 + 大量网络 API(疑似加壳下载器)
|
or (any of ($themida*) and any of ($vmp1))
|
||||||
// binary.py 的 HighEntropySection 已单独扫描,但 YARA 看"熵 + 网络"组合
|
)
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
rule C1_PE_EntropyHighNetwork {
|
|
||||||
meta:
|
|
||||||
risk_level = "medium"
|
|
||||||
category = "C1"
|
|
||||||
false_positive_rate = "medium"
|
|
||||||
reference = "binary.py HighEntropySection 联动"
|
|
||||||
description = "PE 同时出现:多个网络 API + 加壳特征(熵高) + 无签名"
|
|
||||||
strings:
|
|
||||||
$net1 = "InternetOpenUrl" ascii
|
|
||||||
$net2 = "WinHttpOpen" ascii
|
|
||||||
$net3 = "URLDownloadToFile" ascii
|
|
||||||
$net4 = "WSAConnect" ascii
|
|
||||||
// 壳特征
|
|
||||||
$upx = "UPX0" ascii
|
|
||||||
$themida = "Themida" ascii
|
|
||||||
condition:
|
|
||||||
// 网络 API + 壳特征 = 加壳下载器
|
|
||||||
any of ($net*) and ($upx or $themida)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,26 +126,41 @@ rule C8_Ransomware_Note_Filename {
|
|||||||
meta:
|
meta:
|
||||||
risk_level = "medium"
|
risk_level = "medium"
|
||||||
category = "C8"
|
category = "C8"
|
||||||
false_positive_rate = "medium"
|
false_positive_rate = "low"
|
||||||
reference = "勒索信文件名通用模式"
|
reference = "勒索信文件名通用模式"
|
||||||
description = "勒索信典型文件名变体(匹配后触发需要配合加密 API 才升级高危)"
|
description = "勒索信文件名 + (加密 API + 赎金语言)或(赎金语言 + 钱包地址)"
|
||||||
strings:
|
strings:
|
||||||
|
// 只保留"文件名形态"的勒索信特征:家族专有名 + 扩展名。
|
||||||
|
// 不再用裸 README.txt / _readme.txt:任何安装包都带这两个字面量,
|
||||||
|
// 在二进制内容上做全文匹配时必然命中(Go 官方 MSI 即被这样判成恶意)。
|
||||||
$n1 = /HOW_TO_RECOVER[\w.-]*\.(txt|html|png|bmp|jpg|url|vbs|js)/ nocase
|
$n1 = /HOW_TO_RECOVER[\w.-]*\.(txt|html|png|bmp|jpg|url|vbs|js)/ nocase
|
||||||
$n2 = /RESTORE_FILES[\w.-]*\.(txt|html|png|bmp|jpg|url|vbs|js)/ nocase
|
$n2 = /RESTORE_FILES[\w.-]*\.(txt|html|png|bmp|jpg|url|vbs|js)/ nocase
|
||||||
$n3 = /DECRYPT_INSTRUCTIONS[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
$n3 = /DECRYPT_INSTRUCTIONS[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
||||||
$n4 = /README[_\s-]*[\d]*[_\s-]*[\w]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
$n4 = /README[._-]*(FOR[._-]*DECRYPT|TO[._-]*RESTORE)[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
||||||
$n5 = /HELP_DECRYPT[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
$n5 = /HELP_DECRYPT[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
||||||
$n6 = /_readme[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
$n6 = /\b_(readme|restore)[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
||||||
$n7 = /_restore[\w.-]*\.(txt|html|png|bmp|jpg|url)/ nocase
|
$n7 = /(how_to_decrypt|restore_my_files|decrypt_your_files)[\w.-]*\.(txt|html)/ nocase
|
||||||
$bitcoin = /bitcoin address|BTC\b|monero address|XMR\b|send money|ransom|payment/ nocase
|
|
||||||
$wallet = /1[A-Za-z0-9]{25,34}|bc1[A-Za-z0-9]{11,71}|4[0-9A-Za-z]{32}/ nocase
|
// 赎金语言:必须是完整词组。裸 "BTC"/"XMR"/"payment"/"ransom" 在压缩流里
|
||||||
|
// 会以随机字节出现,"ransom" 还会被 "Ransomware" 子串命中。
|
||||||
|
$payment = /(bitcoin|monero|xmr|btc)\s+(address|wallet)|send\s+(the\s+)?money|pay\s+(the\s+)?(ransom|fee)|\bransom\b\s+(note|payment|demand)|your\s+files\s+(are\s+)?encrypted|decrypt\s+your\s+files/ nocase
|
||||||
|
|
||||||
|
// 钱包地址:锚定到整个 token,并用真实地址字符集/长度。
|
||||||
|
// 原写法 1[A-Za-z0-9]{25,34} 无边界,单个 56MB 安装包命中 7.6 万次。
|
||||||
|
$btc_addr = /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/
|
||||||
|
$bech32 = /\bbc1q[a-z0-9]{38}\b|\bbc1q[a-z0-9]{58}\b/
|
||||||
|
$xmr_addr = /\b[48][0-9A-Za-z]{94}\b/
|
||||||
|
|
||||||
$encapi = /CryptEncrypt|BCryptEncrypt|CryptDeriveKey/ ascii
|
$encapi = /CryptEncrypt|BCryptEncrypt|CryptDeriveKey/ ascii
|
||||||
condition:
|
condition:
|
||||||
// 勒索信文件名 + 加密 API = 明确勒索
|
any of ($n*)
|
||||||
any of ($n*) and $encapi
|
and (
|
||||||
|
// 投递器:真加密 API + 赎金语言
|
||||||
|
($encapi and $payment)
|
||||||
or
|
or
|
||||||
// 勒索信文件名 + 比特币/XMR 地址 = 大概率勒索
|
// 纯文本勒索信:赎金语言 + 具体钱包地址
|
||||||
any of ($n*) and any of ($bitcoin, $wallet)
|
($payment and any of ($btc_addr, $bech32, $xmr_addr))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -199,11 +214,13 @@ rule C8_Ransomware_VolumeShadow {
|
|||||||
$vss2 = /vssadmin\s+delete\s+shadows\s+\/all\b/ ascii
|
$vss2 = /vssadmin\s+delete\s+shadows\s+\/all\b/ ascii
|
||||||
$wmic1 = /wmic\s+shadowcopy\s+delete/ ascii
|
$wmic1 = /wmic\s+shadowcopy\s+delete/ ascii
|
||||||
$wmic2 = /wmic\s+shadowcopy\s+where\s+\d+/ ascii
|
$wmic2 = /wmic\s+shadowcopy\s+where\s+\d+/ ascii
|
||||||
$vss3 = /ShadowCopy|VolumeShadowCopy|vss\b/i ascii // "i" flag = nocase in YARA
|
$vss4 = /vssadmin[^\n]{0,48}delete[^\n]{0,48}(shadows|shadow\s+copies)/ ascii
|
||||||
$enc = /CryptEncrypt|BCryptEncrypt|CryptDeriveKey/ ascii
|
$enc = /CryptEncrypt|BCryptEncrypt|CryptDeriveKey/ ascii
|
||||||
condition:
|
condition:
|
||||||
// 卷影删除 + 加密 API = 完整勒索前置
|
// 卷影删除 + 加密 API = 完整勒索前置
|
||||||
(any of ($vss1, $vss2, $wmic*, $vss3)) and any of ($enc*)
|
// 不用裸 "vss"/"ShadowCopy" 子串:任何备份/卷影库的符号表都有它们,
|
||||||
|
// 和 CryptEncrypt 同框就报勒索,是典型误报来源。
|
||||||
|
(any of ($vss1, $vss2, $wmic*, $vss4)) and any of ($enc*)
|
||||||
or
|
or
|
||||||
// 连续多次 vssadmin delete = 正在清卷影
|
// 连续多次 vssadmin delete = 正在清卷影
|
||||||
any of ($vss1, $vss2) and any of ($wmic*)
|
any of ($vss1, $vss2) and any of ($wmic*)
|
||||||
|
|||||||
@@ -57,12 +57,19 @@ rule C3_PHP_Webshell_Obfuscated {
|
|||||||
$rot13 = "str_rot13" ascii wide
|
$rot13 = "str_rot13" ascii wide
|
||||||
$assert = "assert(" ascii wide
|
$assert = "assert(" ascii wide
|
||||||
$createfn = "create_function" ascii wide
|
$createfn = "create_function" ascii wide
|
||||||
|
// PHP 上下文证据:没有 <?php / 超全局变量,就不要凭函数名定罪(vim、git 等
|
||||||
|
// 二进制里同样存在 base64_decode / eval( 字样)
|
||||||
|
$php1 = "<?php" ascii
|
||||||
|
$php2 = /\$_(GET|POST|REQUEST|COOKIE|FILES|SESSION)\b/ ascii
|
||||||
condition:
|
condition:
|
||||||
|
any of ($php*)
|
||||||
|
and (
|
||||||
// base64_decode + (eval|assert|create_function) = 解码后动态执行
|
// base64_decode + (eval|assert|create_function) = 解码后动态执行
|
||||||
$b64 and ($eval or $assert or $createfn)
|
($b64 and ($eval or $assert or $createfn))
|
||||||
or
|
or
|
||||||
// 解码链:base64 + gzinflate / str_rot13 + 执行
|
// 解码链:base64 + gzinflate / str_rot13 + 执行
|
||||||
$b64 and ($gzinflate or $rot13) and ($eval or $assert)
|
($b64 and ($gzinflate or $rot13) and ($eval or $assert))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHP Webshell —— 变量变形($$a = $_POST['cmd'];$$a($_POST['arg']))
|
// PHP Webshell —— 变量变形($$a = $_POST['cmd'];$$a($_POST['arg']))
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
-- 修复 real_type 字段长度:VARCHAR(128) → VARCHAR(256)
|
-- 修复 real_type 字段长度:VARCHAR(128) → VARCHAR(256)
|
||||||
-- libmagic 对 ZIP/ISO 等返回的描述可能超过 128 字符
|
-- libmagic 对 ZIP/ISO 等返回的描述可能超过 128 字符
|
||||||
ALTER TABLE scan_tasks ALTER COLUMN real_type TYPE VARCHAR(256);
|
ALTER TABLE scan_tasks ALTER COLUMN real_type TYPE VARCHAR(256);
|
||||||
|
|
||||||
|
-- 修复 real_type 字段长度:VARCHAR(256) → VARCHAR(512)
|
||||||
|
-- libmagic 对 MSI/OLE 复合文档会把 Title/Subject/Author/Creating Application 等元数据
|
||||||
|
-- 全部拼进描述串(实测 go1.25.0.windows-amd64.msi 超过 334 字符),超出列宽会让
|
||||||
|
-- 整条 scan_tasks INSERT 失败,连带的 scan_findings 一起丢失——被拒绝的文件反而不留痕。
|
||||||
|
ALTER TABLE scan_tasks ALTER COLUMN real_type TYPE VARCHAR(512);
|
||||||
|
|||||||
Reference in New Issue
Block a user