病毒文件识别:从哈希到沙箱的完整检测链路

Published on with 0 views and 0 comments

病毒文件识别:从哈希到沙箱的完整检测链路

前段时间帮朋友看一个被安全软件报毒的脚本,查了半天发现是误报。这个过程中我意识到,很多人对“病毒扫描”的理解还停留在杀毒软件弹窗的层面。其实文件识别背后是一套分层技术栈,从最简单的哈希比对到动态沙箱,每一层都有它的用处,也有它的局限。

这篇文章把自己这些年整理的一套思路串起来,配上代码,可以直接跑。


一、先看清整体流程

一个实用的病毒检测系统,通常不是单点判断,而是多层结果叠加。我用下面这张图概括:

待检测文件 / URL / Stream 静态检测层 哈希比对 MD5/SHA256/SSDEEP 特征签名 YARA / ClamAV 结构分析 PE / ELF / Mach-O ML 模型 静态特征向量 文件预处理 解压 / 去壳 / 熵值 综合风险评分引擎 输出:恶意 / 可疑 / 干净

文件进来先做预处理,然后静态层同时跑哈希、YARA、结构分析和机器学习。静态层拿不准的,再进动态层执行。最后综合打分,给出一个标签。


二、检测流程的详细走法

实际扫描一个文件时,我会按下面这个流程走:

开始 读取文件 预处理:大小、类型、熵值 哈希库比对 命中? YARA 规则扫描 命中? PE/结构异常检测 高风险? 启发式行为评分 结束

这个流程图看着长,但核心思路很简单:先用最快、最准的手段筛掉已知样本,再逐步用更慢但更泛化的手段处理未知样本。


三、静态检测的代码实现

下面这段代码把哈希、YARA、PE 结构、启发式评分串成一个完整扫描器。先安装依赖:

pip install pefile yara-python

3.1 基础信息:熵值和哈希

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 就要怀疑是不是被压缩或混淆过。

3.2 哈希黑名单

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, ''

哈希比对的优点是极快、误报极低,缺点也很明显:变种改一个字节就失效。所以哈希只能作为第一层过滤。

3.3 YARA 规则

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 的好处是规则可以描述一类家族特征,不像哈希只能认单个样本。规则的维护成本比较高,需要持续积累。

3.4 PE 结构分析

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 导入、入口点异常。对未加壳的正常程序这三项通常都很干净。

3.5 启发式评分

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}

启发式评分没有标准答案,权重要根据实际误报情况反复调。我这套阈值是经验值,拿到你环境里大概率要微调。

3.6 完整扫描器

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 和网络行为:

  • CreateProcessWinExec → 看是否启动其他进程
  • RegCreateKeyEx → 看是否写注册表自启动
  • InternetOpen / WinHttp → 看 C2 连接
  • WriteProcessMemory + CreateRemoteThread → 经典注入

我自己常用的轻量方案是 Cuckoo Sandbox,或者用 Windows 容器 + API Monitor 做快速行为分析。沙箱的缺点是慢、成本高,但它是识别未知威胁的最后一道防线。


五、几个容易踩的坑

  1. 哈希误报:别把正常文件哈希误加入黑名单。我们曾经把某个系统 DLL 的某个版本加进去,导致一大片机器报毒。

  2. YARA 规则太宽泛:规则写得太模糊会误杀大量正常文件。建议新规则先在测试集上跑,确认误报率低于千分之一再上线。

  3. PE 分析只针对 Windows:Linux ELF 和 macOS Mach-O 要用不同的解析库,别混在一起。

  4. 熵值不是万能:压缩包、图片、视频本身熵值就高,不能直接当恶意依据,要结合文件类型判断。

  5. 沙箱逃脱:高级木马会检测虚拟机、沙箱环境,发现后直接不执行恶意逻辑。对抗手段包括伪装硬件指纹、随机化沙箱环境等。


六、写在最后

病毒文件识别没有银弹。生产环境里,哈希和 YARA 负责快速拦截已知威胁,PE 分析和启发式规则处理可疑样本,沙箱负责未知威胁。多层叠加,才能在海量文件中做到既快又准。

这套代码我整理成了可直接运行的脚本,感兴趣的话可以自己改改阈值、加几条 YARA 规则跑起来看看效果。

Tags: 病毒检测, 文件安全, YARA, PE 分析, 网络安全