首页 / 资讯中心 / 文章详情

Python实现简单杀毒软件:特征码匹配与启发式规则构建查杀闭环

Python实现简单杀毒软件:特征码匹配与启发式规则构建查杀闭环 ★ FEATURED ARTICLE
简介这是一份面向开发者和安全初学者的简单杀毒软件源代码工程基于Windows下的Visual C工程实现核心目标是演示木马与病毒的基本检测与清除流程。资源共70个文件以14个头文件、11个C源文件为代码主体另附图标、位图、数据库、可执行程序及工程配置文件压缩包仅1.1MB结构紧凑适合本地剖析。目前已有1034人学习下载。代码中可以看到恶意代码签名库、扫描引擎、文件隔离与清除、实时保护、日志报告、更新机制和简洁用户界面等关键模块还涉及进程监控、启动项免疫等辅助设计能够帮助读者理解杀毒软件如何通过特征匹配与行为监控发现威胁并掌握从界面触发扫描到后台处理的完整实现思路。对于想进入安全领域或为现有软件增加防护能力的程序员这是一份务实、可继续扩展的参考源码。1. 简单杀毒软件源代码不靠商业引擎也能把查杀闭环跑通“简单杀毒软件源代码”这个标题看着是一个教学向的小项目但把它当成一个正经的落地方向去看它其实回答了一个很实际的问题在没有商业引擎授权、没有云查杀接口的前提下一个人能不能用纯脚本实现可用的病毒扫描、隔离和恢复流程答案是能。这个量级的目标不是对抗 APT而是在授权环境下做批量文件体检、U 盘交接检查、内网共享目录巡检这类明确场景。适合两类人一类是刚接触安全开发的初学者想搞清楚扫描器内部到底怎么运转另一类是运维或研发需要快速产出一个能挂进现有脚本体系的检测工具而不是去买一套重引擎。我接下来说的这套方案核心思路是用特征码匹配加启发式规则把扫描主循环、签名库、隔离区、日志四件事拆开做全程只依赖标准库不引入重型框架。这样代码拿到任何一台机器都能跑后续要包成命令行工具或者加一个简单界面也容易。说句实在话这个项目的难点从来不是写扫描逻辑而是特征库怎么维护、误报怎么收敛、隔离怎么不出事故这三件事才是真正值得花时间的地方。2. 先想清楚再动手特征码 vs 启发式这个量级该选哪条路2.1 三种检测机制的区别以及为什么选“特征码为主”杀毒软件的核心是“判定一个文件是否可疑”判定机制常见有三种特征码匹配、启发式规则、行为监控。特征码匹配是把已知恶意文件的哈希值或字节序列存进签名库扫描时逐文件比对命中即告警。启发式规则则不依赖已知样本而是通过文件结构、扩展名组合、代码特征来推断风险。行为监控需要在程序运行过程中拦系统调用工程量大不适合这个量级。我一般会把行为监控排除在“简单杀毒软件源代码”这个项目之外原因很直接行为监控需要挂钩文件系统、进程创建、网络连接这些操作在 Windows 上要碰驱动层在 Linux 上要碰 audit 或 eBPF写出来就不是“简单”二字能覆盖的了。特征码匹配加启发式规则组合起来已经能覆盖大多数静态查杀场景——U 盘里的可疑脚本、下载目录里来历不明的可执行文件、被批量篡改的文档这些目标靠静态特征就足够发现。还有一点容易被忽略特征码匹配的查杀能力上限取决于签名库而签名库的维护成本是可控的。商业引擎每天推送几十万条新签名那是靠全球样本网络撑起来的自己做教学级或内部工具级扫描器维护几百到几千条精确哈希配合规则过滤性价比反而最高。误报率低于漏报率这是内部工具的第一原则。2.2 最小可行方案需要哪四个构件一个能号称“杀毒软件”的扫描器再简单也得有四个构件扫描器本体、签名库、隔离区、日志。扫描器本体负责遍历目录、计算文件摘要、和签名库比对签名库是一个独立的签名文件决定检测能力的边界隔离区存放被拦截的文件不能直接删除否则误报时没有后悔药日志记录每次扫描的时间、路径、文件哈希、命中规则和处置结果方便事后复盘。这四个构件里最容易被人低估的是签名库的文件格式。签名库用 JSON 承载即可原因有三个可读性好文本编辑器能直接改程序解析简单标准库 json 模块几行代码就能读进来后续要合并另一份签名列表写一个小的合并脚本也很容易。SQLite 虽然更“正式”但对于几百条签名来说属于过度设计而且会让整个项目的初始门槛变高。目录结构我建议这样规划sengine.py 放扫描主逻辑signatures.json 放特征库quarantine 目录放隔离文件scan.log 写日志run_scan.py 作为入口。这样拆分的好处是后续要加启发式规则、加白名单机制都是往对应模块里补逻辑不需要把主循环推倒重来。初学者最容易犯的错误是把所有逻辑塞进一个文件结果改一处崩三处。2.3 环境准备与代码骨架Python 标准库就够这个项目我强烈建议用 Python 实现不要一上来就上 C/C。Python 的跨平台行为一致在 Windows 和 Linux 上遍历目录、计算哈希的接口完全一致代码拿到哪都能跑。标准库里的 hashlib、pathlib、json、argparse、concurrent.futures 正好覆盖全部需求零第三方依赖意味着部署时不需要 pip install 任何东西。性能方面Python 计算文件哈希的速度大约在每秒 80 到 150 MB单机扫描几 GB 文件完全可以接受瓶颈往往在磁盘 IO不在语言。# run_scan.py —— 项目入口解析命令行参数并调用引擎 import argparse from pathlib import Path from concurrent.futures import ThreadPoolExecutor import hashlib, json, shutil, datetime SCAN_ROOT Path(__file__).parent SIGNATURE_FILE SCAN_ROOT / signatures.json QUARANTINE_DIR SCAN_ROOT / quarantine LOG_FILE SCAN_ROOT / scan.log def load_signatures(): with open(SIGNATURE_FILE, r, encodingutf-8) as f: return json.load(f)load_signatures 把签名文件一次性读入内存避免扫描每个文件都去读磁盘。签名文件用 UTF-8 编码打开是因为内部可能包含中文注释或说明字段Windows 下默认编码可能报错这一步不能省。quarantine 目录和日志文件放在项目根目录下方便出问题时直接翻看不要把隔离区放到系统临时目录临时目录会被清理误报时文件就真丢了。3. 把扫描引擎写出来主循环、哈希比对与隔离恢复3.1 扫描主循环递归目录遍历与系统路径过滤主循环是整个引擎的骨架它要做的事情是给定一个根目录递归地找出所有文件逐个交给检测函数。这里有一个常见的翻车点不假思索地把系统目录、内存映射文件、临时文件全部纳入扫描范围。系统目录权限受限扫描会报 PermissionErrorWindows 下页面文件pagefile.sys体积巨大哈希计算白白消耗 CPU临时文件高频变化也算不出稳定结果。所以要有一个“跳过规则”遇到系统目录和明显无风险的文件类型直接跳过不进入哈希计算流程。这一步不是偷懒而是工程上的必要取舍。内部工具的目标是批量检测可疑样本不是全盘杀毒把资源花在高价值文件上才是正道。def iter_files(root: Path): skip_dirs {$RECYCLE.BIN, System Volume Information, Windows, Program Files} skip_exts {.tmp, .log, .dmp, .iso, .vhd} for dirpath, dirnames, filenames in os.walk(root): dirnames[:] [d for d in dirnames if d not in skip_dirs] for fname in filenames: fpath Path(dirpath) / fname if fpath.suffix.lower() in skip_exts: continue yield fpathiter_files 是一个生成器用 os.walk 从上往下遍历每一层把跳过目录从 dirnames 里剔除这样 walk 就不会进入这些目录省掉无意义的递归。skip_exts 过滤掉明显不可能承载恶意载荷的临时文件和镜像文件扩展名判断用 lower() 统一成小写再比对避免 .TMP 和 .tmp 被当成两种类型。需要注意跳过目录的集合是按 Windows 环境写的如果在 Linux 上跑要换成 /proc、/sys、/dev 这些虚拟文件系统目录否则扫描结果会被大量伪文件污染。3.2 文件哈希计算与特征库比对用 SHA-256 还是 MD5计算文件摘要时我推荐优先用 SHA-256而不是 MD5。MD5 的碰撞成本已经低到普通人也能伪造虽然作为样本标识还算够用但既然标准库里同时支持两种算法没必要在 2025 年还守着一个已经被攻破的哈希。SHA-256 的计算速度比 MD5 慢 20% 左右在扫描场景下这点差距可以忽略。文件哈希计算有几种常见方式一次性读入内存、分块读取、开启内存映射。大文件一次性读入会导致内存峰值很高分块读取是最稳妥的做法chunk_size 设置在 64 KB 到 1 MB 之间兼顾 IO 效率和内存占用。def file_sha256(fpath: Path, chunk_size: int 65536) - str: h hashlib.sha256() try: with open(fpath, rb) as f: while chunk : f.read(chunk_size): h.update(chunk) except PermissionError: return None return h.hexdigest()file_sha256 用 while 循环加 walrus 运算符读取文件块直到文件末尾。chunk 取 64 KB 是因为这个大小在机械硬盘和固态硬盘上都能发挥比较好的读写性能太小的块会增加系统调用次数太大的块浪费内存且对性能提升有限。PermissionError 被捕获后返回 None由上层决定是跳过该文件还是继续扫描不会因为一个受保护文件让整个扫描任务中断。文件被占用、文件被删除等异常也需要一并捕获实际跑扫描时这两种情况在 Windows 上非常常见。3.3 命中判定与隔离操作把文件“请进”隔离区隔离操作是整个扫描器的安全阀。命中的文件不能直接删除而是移动到 quarantine 目录下并在文件名后面附加哈希值作为标识。这样即使签名库误报也可以通过扫描日志找到原路径把文件从隔离区恢复回去。移动比复制快因为跨目录移动只改文件系统记录。这里有一个坑如果隔离目录和原文件在同一个分区下Path.rename 一次就能完成如果跨分区rename 会抛 OSError必须改成 shutil.move 并用 try 兜底。def quarantine(fpath: Path, sig_id: str): ts datetime.now().strftime(%Y%m%d_%H%M%S) target QUARANTINE_DIR / f{fpath.name}.{sig_id}.{ts} try: shutil.move(str(fpath), str(target)) except OSError: # 跨分区回退先复制再删除保留原始文件的元数据 shutil.copy2(str(fpath), str(target)) fpath.unlink() log_entry(quarantine, fpath, target, sig_id) return targetquarantine 函数把文件移动到 QUARANTINE_DIR 下文件名拼接了签名 ID 和时间戳目的是保证即使有两个同名文件先后命中也不会互相覆盖。shutil.move 内部如果发现目标分区不同会自动回退到跨分区复制删除流程但会丢失部分元数据所以这里先尝试 move捕获 OSError 后再用 copy2 加 unlink 的组合兜底。日志里记录了原路径、隔离后路径和签名 ID为后续恢复提供完整信息。隔离目录的权限要特别注意——如果运行扫描器的账户没有写入隔离目录的权限整个隔离操作会失败建议把隔离目录放在项目根目录而不是系统受保护目录。3.4 主扫描流程代码串起整个逻辑把上面几个函数组合起来就是一个最小可用的扫描器主程序。主程序接收一个命令行参数作为扫描根目录遍历文件、算哈希、比签名库、执行隔离最后打印统计结果。这一步要处理扫描进度和退出状态不要等到全部扫完才输出信息否则长时间运行时使用者完全不知道任务是否卡死。def main(root_arg: str): root Path(root_arg) signatures load_signatures() hit_count 0 for fpath in iter_files(root): digest file_sha256(fpath) if digest is None: continue if digest in signatures: sig_info signatures[digest] quarantine(fpath, sig_info.get(id, unknown)) hit_count 1 if hit_count 0: continue print(f扫描完成隔离 {hit_count} 个文件详见 scan.log)main 函数的判断逻辑直接拿哈希值查签名库字典。字典查找的时间复杂度是 O(1)几千条签名完全不影响扫描速度。sig_info 里可选的 id 字段用于标识签名来源当签名库是从多个渠道合并过来时这个字段能帮你判断命中的是哪一批规则。当前版本没有做“每扫描到一个文件就写日志”而是只在命中时写日志这样日志文件不会因为扫描大量正常文件而急剧膨胀需要全量审计时再开启详细模式。4. 让它更像“杀毒软件”启发式规则与增量特征库4.1 启发式规则三行代码提高检出率特征码匹配只能查出已知样本面对同一恶意家族的微小变种容易失效。启发式规则的意义在于不依赖具体样本哈希而是通过文件结构特征推断风险。这个项目里我加了三类规则可疑扩展名组合、文件头魔法字节、最近修改时间窗口。可疑扩展名组合指的是检测“双重扩展名”手法比如xxx.pdf.exe这种命名方式在正常软件中几乎不会出现是攻击者常用的伪装手段。文件头魔法字节规则是检查文件开头几个字节是否对应某种格式——比如一个文件声称是 PNG 图片但文件头却是 MZ 可执行文件的标志这个矛盾本身就是强烈的告警信号。修改时间窗口规则是看文件是否在最近几天内被创建或修改适合发现批量投递的附件。SUSPECT_DOUBLE_EXT {.exe, .scr, .bat, .cmd, .vbs} MZ_HEADER bMZ IMAGE_SIGS {b\x89PNG, b\xff\xd8\xff, bGIF8} def heuristic_check(fpath: Path, data_head: bytes): reasons [] if fpath.suffix.lower() in SUSPECT_DOUBLE_EXT: stem fpath.stem.lower() if any(stem.endswith(ext) for ext in SUSPECT_DOUBLE_EXT): reasons.append(double_ext) if data_head.startswith(MZ_HEADER): if not fpath.suffix.lower() in {.exe, .dll, .sys}: reasons.append(mz_header_mismatch) return reasonsheuristic_check 接收文件路径和文件头 64 字节作为参数返回一个风险原因列表。双重扩展名判断先确认文件后缀本身是高风险类型再检查文件主名部分是否也以高风险扩展名结尾。文件头比对则是看可执行文件标志是否与扩展名匹配正常情况下一个 .jpg 文件不太可能以 MZ 开头。启发式规则不要直接触发隔离而是作为“疑似”标记写入日志交由人工复核这是收敛误报的关键策略。4.2 特征库增量更新让扫描器可持续使用固定的签名库无法应对新样本增量更新机制是这个项目能否长期使用的前提。签名库文件用 JSON 存储更新脚本从外部数据源拉取新签名并合并到本地文件。外部数据源可以是爬取的公开样本哈希列表也可以是内部恶意文件分析流水线产出的结果。更新脚本要做的核心是“合并非破坏”——保留本地已有签名只追加新的避免重复条目导致签名库膨胀。# update_signatures.py —— 增量合并外部签名 def merge_signatures(external_path: str): with open(SIGNATURE_FILE, r, encodingutf-8) as f: local json.load(f) with open(external_path, r, encodingutf-8) as f: external json.load(f) before len(local) for digest, info in external.items(): if digest not in local: local[digest] info with open(SIGNATURE_FILE, w, encodingutf-8) as f: json.dump(local, f, ensure_asciiFalse, indent2) print(f签名库从 {before} 条增加到 {len(local)} 条)merge_signatures 的逻辑非常朴素遍历外部签名键不存在的才加入存在则跳过。这样本地对某条签名的覆盖决策永远掌握在自己手里不会被外部源意外改掉。合并完成后用 indent2 重新写回 JSON保持文件人类可读。键的判重用not in local这个操作的复杂度是 O(n)几千条签名完全够用。更新操作的执行频率建议一天一次放在低峰时段跑更新完成后立即对内部共享目录做一次增量扫描。4.3 白名单机制把误报防在源头启发式规则必然带来误报白名单机制是对付误报最直接的工具。白名单可以按路径、按哈希、按签名规则三张表来做。按路径适合放行某个固定的开发目录按哈希适合放行某个明确的正常文件按规则 ID 适合放行某条启发式规则命中的所有文件。三张表都维护在 whitelist.json 里扫描时先查白名单再跑检测逻辑。def is_whitelisted(fpath: Path, digest: str, reasons: list): with open(WHITELIST_FILE, r, encodingutf-8) as f: wl json.load(f) if str(fpath) in wl.get(paths, []): return True if digest in wl.get(hashes, []): return True if any(r in wl.get(rule_ids, []) for r in reasons): return True return Falseis_whitelisted 每扫描一个文件就打开一次白名单文件性能上有优化空间。更优的做法是在主程序启动时把白名单一次性加载到内存这里写成每次打开是为了保持函数独立性实际项目中建议改造为一次性加载。白名单的粒度要控制好不要用大目录路径作为白名单入口否则等于关掉了该目录下的所有检测。5. 踩坑记录误报、卡顿、权限不足五个典型翻车现场5.1 扫描系统目录导致 CPU 瞬时满载现象第一次把扫描根目录设置为“我的电脑”程序开始后 CPU 瞬间拉满界面假死半个多小时没有输出。原因没有做系统目录过滤大量系统 DLL、缓存文件和高频变化的临时文件全部进入哈希计算流程。解决在 iter_files 里增加系统目录跳过集合针对 Windows 至少跳过$RECYCLE.BIN、System Volume Information、Windows目录这几个目录既没有检测价值又存在大量锁定的文件会拉慢整体扫描进度。加完过滤后同一个扫描目标耗时降到了原来的三分之一。5.2 同一个文件换个后缀就检不出来现象一个命中签名的样本把 .exe 改成 .bin 后再次扫描程序完全没有反应。原因签名库只存了文件哈希没有存文件头信息哈希匹配与文件内容一一对应改扩展名不会改变哈希值理论上应该仍能命中排查后发现问题出在迭代器把 .bin 当成了“可疑扩展名之外的安全类型”跳过了。解决iter_files 里的 skip_exts 不能盲目包含未知扩展名要改成只跳过明确安全的大文件类型其余全部进入哈希计算让签名库去做最终判定。5.3 隔离文件时 PermissionError 导致扫描中断现象扫描到某个文件时程序报 PermissionError整个扫描流程被中断之前的扫描结果全部丢失。原因open 文件或移动文件时遇到 NTFS 权限限制文件被其他进程占用或者文件属性是只读。解决file_sha256 和 quarantine 两个函数都要用 try 包裹异常单文件失败只记录日志并跳过不影响整个扫描任务的继续执行。只读属性问题要在隔离前先调用 os.chmod 把只读位去掉否则移动到隔离目录后后面的恢复流程也会被同样的属性拦一次。5.4 启发式规则误杀率过高用户直接不信任现象启用 mz_header_mismatch 规则后大量正常文件被标记为“疑似”尤其是那些没有扩展名的脚本文件和某些安装包的自解压模块。原因规则太宽泛把一个“可能可疑”的特征直接放大成了“高度可疑”没有区分风险等级。解决修改规则判定逻辑文件头是 MZ 且扩展名未知标记为“低风险”写入日志但不隔离只有文件头是 MZ 且扩展名是图片或文档类型才标记为“高风险”。经验法则是启发式规则的输出永远不能直接触发隔离只能触发人工复审命中率不足时先调阈值再调规则本身。5.5 打包成独立 EXE 后被其他杀软当成样本现象用 PyInstaller 打包后的扫毒工具发到另一台机器上直接被 Windows Defender 拦截提示是 HackTool。原因打包产物包含 Python 解释器特征和文件系统操作逻辑部分杀毒引擎会把自动化文件操作工具归类为“潜在不受欢迎的程序”这算是静态引擎的通病。解决给打包产物增加数字签名签名能显著降低误报率同时向杀毒软件厂商提交误报申诉把内部工具加入白名单。另一个备选方案是不要打包成单文件 EXE而是直接分发源码加一个启动脚本减少被引擎扫描的特征面。6. 还能再进一步从扫描脚本到批量检测工具的调优习惯扫描器跑通之后下一步不是加界面而是把验证方法和运维习惯补上。我会先手工构造一个测试样本集三个命中签名库的真实样本副本、两个双层扩展名文件、一个文件头与扩展名冲突的图片文件再放四五个完全正常的文件当对照组。每次修改规则后都跑一遍这个固定样本集记录检出数和误报数这套回归流程能防止改动 A 规则时破坏 B 规则。第二个要调的是日志内容。scan.log 不要只写“命中、隔离”这类状态要写全六个字段扫描时间、文件路径、SHA-256、命中规则、处理动作、恢复所需信息。这样过了一个月要复盘某次误报时不需要重新扫描原文件就能定位问题。日志文件本身要注意轮转单文件超过 10 MB 就重命名归档否则日志文件本身会变成磁盘占用大户。第三件事是做一个恢复工具。隔离区里存了文件就必须有恢复入口。恢复脚本按日志里的条目把文件从隔离区移回原目录并恢复原文件名这个操作看起来简单但没有独立工具的话误报后只能手动翻隔离目录找文件。恢复功能建议和扫描主程序放在同一个代码文件里通过--restore参数触发不单独建模块。分发源码时把签名格式说明写进 README让接手的人知道每条签名是什么格式、从哪来、如何验证有效性。我自己的习惯是每次在这个脚本上做改动后第一件事就是用它扫描自己的下载目录和 U 盘看有没有意外行为。有一回我加了一个新启发式规则用测试样本集跑通过然后随手扫了一下我的临时文件目录结果把自己写的一个自动化脚本误报了。当时的第一反应是“规则有问题”而不是“文件有问题”改规则还是加白名单取决于这条规则本身的价值——白名单防的是个案规则修正防的是整类误报别搞反。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站