前段时间帮朋友看一个被安全软件报毒的脚本,查了半天发现是误报。这个过程中我意识到,很多人对“病毒扫描”的理解还停留在杀毒软件弹窗的层面。其实文件识别背后是一套分层技术栈,从最简单的哈希比对到动态沙箱,每一层都有它的用处,也有它的局限。
这篇文章把自己这些年整理的一套思路串起来,配上代码,可以直接跑。
一个实用的病毒检测系统,通常不是单点判断,而是多层结果叠加。我用下面这张图概括:
文件进来先做预处理,然后静态层同时跑哈希、YARA、结构分析和机器学习。静态层拿不准的,再进动态层执行。最后综合打分,给出一个标签。
实际扫描一个文件时,我会按下面这个流程走:
这个流程图看着长,但核心思路很简单:先用最快、最准的手段筛掉已知样本,再逐步用更慢但更泛化的手段处理未知样本。
下面这段代码把哈希、YARA、PE 结构、启发式评分串成一个完整扫描器。先安装依赖:
pip install pefile yara-python
import math
import hashlib
from pathlib import Path
def file_entropy(file_path: str) -> float:
"""计算文件熵值,加密/混淆样本通常熵值高。"""
with open(file_path, 'rb') as f:
data = f.read()
if not data:
return 0.0
entropy = 0.0
for x in range(256):
p_x = data.count(x) / len(data)
if p_x > 0:
entropy += -p_x * math.log2(p_x)
return entropy
def file_hashes(file_path: str) -> dict:
h_md5 = hashlib.md5()
h_sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
while chunk := f.read(8192):
h_md5.update(chunk)
h_sha256.update(chunk)
return {
'md5': h_md5.hexdigest(),
'sha256': h_sha256.hexdigest(),
}
def metadata(file_path: str) -> dict:
p = Path(file_path)
return {
'path': str(p.absolute()),
'size': p.stat().st_size,
'entropy': file_entropy(file_path),
'hashes': file_hashes(file_path),
}
熵值是判断文件是否被加壳或加密的重要指标。正常可执行文件熵值一般在 5-7 之间,超过 7.5 就要怀疑是不是被压缩或混淆过。
import os
from typing import Tuple
class HashBlacklist:
def __init__(self, db_path: str = 'blacklist_hashes.txt'):
self.db_path = db_path
self.hashes = self._load()
def _load(self) -> set:
if not os.path.exists(self.db_path):
return set()
with open(self.db_path, 'r', encoding='utf-8') as f:
return {line.strip().lower() for line in f if line.strip()}
def add(self, file_hash: str):
h = file_hash.lower()
if h not in self.hashes:
self.hashes.add(h)
with open(self.db_path, 'a', encoding='utf-8') as f:
f.write(h + '\n')
def check(self, file_path: str) -> Tuple[bool, str]:
hashes = file_hashes(file_path)
for algo in ('md5', 'sha256'):
if hashes[algo] in self.hashes:
return True, f'{algo}:{hashes[algo]}'
return False, ''
哈希比对的优点是极快、误报极低,缺点也很明显:变种改一个字节就失效。所以哈希只能作为第一层过滤。
import yara
MALWARE_YARA = {
'malware_signatures': r'''
rule Suspicious_PE_Header
{
strings:
$mz = { 4D 5A }
$upx = "UPX0" ascii wide
condition:
$mz at 0 and $upx
}
rule Ransomware_Strings
{
strings:
$a = ".locked" wide ascii
$b = "Bitcoin" wide ascii
$c = "RSA" wide ascii
condition:
2 of them
}
'''
}
def scan_with_yara(file_path: str) -> dict:
rules = yara.compile(sources=MALWARE_YARA)
matches = rules.match(file_path)
return {
'matched': len(matches) > 0,
'rules': [m.rule for m in matches],
}
YARA 的好处是规则可以描述一类家族特征,不像哈希只能认单个样本。规则的维护成本比较高,需要持续积累。
import pefile
def pe_analysis(file_path: str) -> dict:
try:
pe = pefile.PE(file_path)
except pefile.PEFormatError:
return {'is_pe': False}
result = {
'is_pe': True,
'entry_point': hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint),
'sections': [],
'suspicious': [],
}
for section in pe.sections:
name = section.Name.decode('utf-8', errors='ignore').strip('\x00')
entropy = section.get_entropy()
result['sections'].append({
'name': name,
'entropy': round(entropy, 4),
'executable': section.IMAGE_SCN_MEM_EXECUTE,
})
if entropy > 7.0 and section.IMAGE_SCN_MEM_EXECUTE:
result['suspicious'].append(f'高熵可执行节: {name}')
suspicious_apis = {
'VirtualAlloc', 'WriteProcessMemory', 'CreateRemoteThread',
'WinExec', 'ShellExecuteA', 'InternetOpenA'
}
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
for entry in pe.DIRECTORY_ENTRY_IMPORT:
for func in entry.imports:
if func.name and func.name.decode('utf-8', errors='ignore') in suspicious_apis:
result['suspicious'].append(f'可疑 API: {func.name.decode()}')
pe.close()
return result
PE 分析主要关注三点:加壳痕迹(高熵节)、可疑 API 导入、入口点异常。对未加壳的正常程序这三项通常都很干净。
class HeuristicEngine:
THRESHOLD = 60
def __init__(self):
self.rules = [
('entropy>7.5', lambda m: m['entropy'] > 7.5, 30),
('tiny_file', lambda m: m['size'] < 1024, 10),
('high_entropy_exec', lambda m: 'suspicious' in m and any('High entropy' in s for s in m['suspicious']), 25),
('suspicious_api', lambda m: 'suspicious' in m and any('Suspicious API' in s for s in m['suspicious']), 15),
('yara_hit', lambda m: m.get('yara', {}).get('matched'), 40),
('blacklist', lambda m: m.get('blacklist', False), 100),
]
def classify(self, meta: dict) -> dict:
score = sum(weight for name, check, weight in self.rules if check(meta))
if score >= 100:
label = 'MALICIOUS'
elif score >= self.THRESHOLD:
label = 'SUSPICIOUS'
else:
label = 'CLEAN'
return {'score': score, 'label': label}
启发式评分没有标准答案,权重要根据实际误报情况反复调。我这套阈值是经验值,拿到你环境里大概率要微调。
class VirusScanner:
def __init__(self, blacklist_path='blacklist_hashes.txt'):
self.blacklist = HashBlacklist(blacklist_path)
self.yara_rules = yara.compile(sources=MALWARE_YARA)
self.heuristic = HeuristicEngine()
def scan(self, file_path: str) -> dict:
meta = metadata(file_path)
meta['blacklist'] = self.blacklist.check(file_path)[0]
meta['yara'] = scan_with_yara(file_path)
if file_path.lower().endswith(('.exe', '.dll')):
pe_result = pe_analysis(file_path)
meta['pe'] = pe_result
if 'suspicious' in pe_result:
meta['suspicious'] = pe_result['suspicious']
result = self.heuristic.classify(meta)
return {
'file': file_path,
'score': result['score'],
'label': result['label'],
}
静态手段再强,也挡不住混淆和 0-day。所以真正重要样本要进沙箱跑一遍。
沙箱的核心是 Hook 关键 API 和网络行为:
CreateProcess、WinExec → 看是否启动其他进程RegCreateKeyEx → 看是否写注册表自启动InternetOpen / WinHttp → 看 C2 连接WriteProcessMemory + CreateRemoteThread → 经典注入我自己常用的轻量方案是 Cuckoo Sandbox,或者用 Windows 容器 + API Monitor 做快速行为分析。沙箱的缺点是慢、成本高,但它是识别未知威胁的最后一道防线。
哈希误报:别把正常文件哈希误加入黑名单。我们曾经把某个系统 DLL 的某个版本加进去,导致一大片机器报毒。
YARA 规则太宽泛:规则写得太模糊会误杀大量正常文件。建议新规则先在测试集上跑,确认误报率低于千分之一再上线。
PE 分析只针对 Windows:Linux ELF 和 macOS Mach-O 要用不同的解析库,别混在一起。
熵值不是万能:压缩包、图片、视频本身熵值就高,不能直接当恶意依据,要结合文件类型判断。
沙箱逃脱:高级木马会检测虚拟机、沙箱环境,发现后直接不执行恶意逻辑。对抗手段包括伪装硬件指纹、随机化沙箱环境等。
病毒文件识别没有银弹。生产环境里,哈希和 YARA 负责快速拦截已知威胁,PE 分析和启发式规则处理可疑样本,沙箱负责未知威胁。多层叠加,才能在海量文件中做到既快又准。
这套代码我整理成了可直接运行的脚本,感兴趣的话可以自己改改阈值、加几条 YARA 规则跑起来看看效果。
Tags: 病毒检测, 文件安全, YARA, PE 分析, 网络安全