简介:基于Python构建的恶意软件检测系统源码包,面向网络安全学习者、Python开发者及安全运维人员,以威胁情报为切入点,完整演示如何将网络地址黑名单、恶意域名、签名库与行为特征融入程序检测流程。压缩包共19个文件,其中包含9个Python脚本、7个编译后的字节码文件,以及一个JSON配置、一个Markdown说明和一个TXT文本,整体仅13KB,轻量易读,适合快速剖析文件间调用关系与实现细节。源码内部按照应用层、接口层与验证逻辑层组织,覆盖应用初始化、API接口、路由分发、状态码定义与防伪验证等核心模块,并附带依赖清单与说明文档;项目主目录采用E-Search-master方式组织,便于按功能模块索引与二次开发。目前已有795人学习,特别适合用于理解Flask框架下的威胁情报查询、数据解析与规则匹配思路,也可作为毕业设计或安全工具开发的参考原型。借助Pandas、Scapy、Yara等第三方库的典型调用,读者能够掌握特征提取、流量分析与恶意样本识别的实现技巧,为构建实时监控、日志分析与自动化响应系统打下扎实基础。
1. 基于威胁情报的恶意软件检测系统:先解决情报新鲜度,再谈检出率
在 macOS 上看到“未打开 party.ape.helper,因其包含恶意软件”这类提示时,背后并不是某一条单一规则在起作用,而是一套由本地特征和云端情报共同支撑的判定链路。使用 Python 开发一款基于威胁情报的恶意软件检测系统,解决的是这条链路里最核心的一段:把落到磁盘上的文件与外部持续更新的指标做比对,在文件执行前给出恶意、可疑或干净三种结论。它适合两类人:一类是手里已经有样本管道、想给内部工具加情报能力的蓝队开发;另一类是准备做安全产品原型的后端工程师,想验证威胁情报这条路能不能形成闭环。下面的方案不依赖商业沙箱,用常见开源情报源加 Python 就能搭起来。
2. 恶意软件检测依赖的情报形态:从 IOC 到 STIX 再到情报源组合
2.1 哈希、域名、IP:检测系统里最常用的三类 IOC
IOC,即失陷指标(Indicator of Compromise),描述的是攻击者留在主机或网络里的痕迹。它和漏洞特征不一样:漏洞特征是“哪里有洞”,IOC 说的是“哪里已经被动过”。恶意软件检测系统把 IOC 当作黑名单查询项——文件、流量或者进程行为一旦和 IOC 对上,就可以给出置信度判断。最常见的 IOC 有三类:文件哈希(记录已知恶意样本的摘要)、网络地址(C2 域名和下载服务器 IP)、行为规则(YARA/Sigma 描述的文件内容特征或日志特征)。单机设备上能产生 IOC 的能力有限,威胁情报的本质就是把别的机构观测到的 IOC 汇入你本地的匹配引擎。
| 类型 | 匹配对象 | 时效特点 | 常见来源 |
|---|---|---|---|
| 文件哈希 | 已知样本的 SHA-256 | 样本更新快,需要小时级刷新 | 样本平台、开源情报库 |
| 域名 | C2 / 分发地址 | 新域名存活期短 | DNS 情报、ThreatFox |
| IP 地址 | 外联地址 | 复用频繁,误报风险高 | 信誉库、蜜罐日志 |
| 内容规则 | 文件特征(YARA) | 慢,可跨家族识别 | 公开规则社区 |
从表格能看出一个选型逻辑:哈希匹配最快、最准,但只能防已知;域名和 IP 覆盖未知样本的外联过程,但误报率高;YARA 规则负责兜底识别家族行为。检测系统最忌讳只做单一查表,要把这几类情报混在一起用,再按置信度加权。
2.2 从 STIX 和 OpenIOC 格式说起:情报文件怎么落库
开源情报社区里最常见的两种描述格式是 STIX 和 OpenIOC。STIX 以 JSON 表达,适合机器解析;OpenIOC 是 XML 风格,年代更早。检测系统不需要完整实现 STIX 的全部对象,只需要把 indicator 类型抽取出来,转成内部统一结构。
{ "type": "indicator", "spec_version": "2.1", "name": "已知勒索软件样本", "pattern": "[file:hashes.'SHA-256' = 'ef1b5f8f2c64b2a1e9f5d3a7c4b8d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4']", "labels": ["malicious-activity"], "kill_chain_phases": [ {"kill_chain_name": "lockheed-martin-cyber-kill-chain", "phase_name": "command-and-control"} ], "valid_from": "2024-01-01T00:00:00Z" }这份 JSON 里真正参与匹配的是pattern字段,其余是标签与时间属性,用来决定这条 IOC 的生效窗口。落地时,我一般把它拆成两条路径:哈希类指标直接写入 SQLite 的hash_ioc表,pattern 里带文件名或网络地址的则转成 YARA 源文件或域名/IP 列表。时间字段valid_from不要丢掉,情报是有生命周期的,过期条目应该由定时任务清走。
2.3 情报源选型:免费 API 与本地规则库的搭配方案
常见做法是本地规则库加远程 API 双通道。本地规则库存放哈希和 YARA 规则文件,启动即加载到内存,扫描时不依赖外部链路;远程 API 负责补充实时信誉,比如域名今天才被标记为恶意,本地库还没有种子。我用 OTX 的开放接口做示例,它对个人开发者免费且返回结构直观。
import os import requests def query_domain_reputation(domain: str) -> bool: endpoint = f"https://otx.alienvault.com/api/v1/indicators/domain/{domain}/general" resp = requests.get( endpoint, headers={"X-OTX-API-KEY": os.environ["OTX_API_KEY"]}, timeout=3, ) if resp.status_code != 200: return False data = resp.json() pulse_count = data.get("pulse_info", {}).get("count", 0) return pulse_count > 0pulse_count表示这条域名被多少个威胁情报社群标记过,数量越大可信度越高。这里最关键的两个参数是timeout=3和pulse_count > 0:前者防止外部接口拖慢整条扫描链路,后者作为最低阈值避免单点误报。如果不想绑定单一厂商,ThreatFox 和 MISP 的模型也类似,核心差异只在鉴权头和返回字段名。
3. 用 Python 搭一个多引擎检测骨架:哈希、YARA、网络指标提取
3.1 源码包入口:detector 对象与规则加载
这类项目的源码包打开之后,入口通常是一个 detector 类和一个 cli 脚本。detector 负责调度,cli 负责把命令行参数转成任务。先给出核心类的骨架:
import glob import sqlite3 import yara class MalwareDetector: def __init__(self, ioc_db: str, rules_path: str): self._hash_set = set() self._load_hashes(ioc_db) rule_files = { os.path.basename(p): p for p in glob.glob(f"{rules_path}/*.yar") } self.rules = yara.compile(filepaths=rule_files)规则用filepaths字典一次性编译,key 是规则文件相对名,value 是磁盘路径。这样做的原因是 YARA 允许多文件之间使用 include,命名空间能避免不同规则文件之间的命名冲突。启动阶段就把规则编译进内存,扫描阶段不再做磁盘读取。注意这里用os.path.basename而不是字符串分割,Windows 路径下反斜杠不会踩坑。
3.2 哈希匹配:集合查询比逐行查库快一个数量级
哈希匹配的常规实现容易写成“读取每个文件,然后SELECT * FROM ioc WHERE hash = ?”。当样本数量到万级,这会造成大量无关的数据库 I/O。我会在启动时把全表加载进内存。
def _load_hashes(self, db_path: str): with sqlite3.connect(db_path) as conn: for row in conn.execute("SELECT sha256 FROM hash_ioc"): self._hash_set.add(row[0]) def is_known_hash(self, sha256: str) -> bool: return sha256 in self._hash_set这里的hash_ioc表只需要一个sha256字段加source、created_at两个元信息字段。内存方案的限制是:如果本地情报库超过一千万条,加载时间会明显变长,那时再换布隆过滤器。对多数安全团队来说,几十万级情报量用集合完全够用。
3.3 YARA 规则加载与命中评分
YARA 是恶意软件样本特征匹配的事实标准,规则里可以写十六进制字节序列、字符串、正则表达式和条件表达式。检测系统需要从规则里取出一个可量化的分数,常见做法是在规则 meta 段里自定义score字段。
rule Generic_Ransomware_Indicators { meta: score = 65 family = "ransomware" strings: $a = "encrypt" nocase $b = "decrypt_files" ascii $c = "ransom" ascii wide condition: any of them }扫描端调用rules.match时,把命中规则的 meta 值汇总:
def scan_with_yara(self, data: bytes): score = 0 matched = [] for m in self.rules.match(data=data, timeout=30): score += int(m.meta.get("score", 60)) matched.append(m.rule) return score, matchedtimeout=30是容易被忽略的参数:某些畸形文件会让 YARA 引擎陷入长匹配,没有超时保护,单文件扫描可能卡死整个工作线程。int(m.meta.get("score", 60))的默认值 60 需要写在项目配置里,规则维护者忘写 score 时不至于直接报错。
3.4 从样本中提取域名和 IP:正则只解决一半问题
检测系统只查文件哈希是不够的——一个从来没有见过的二进制文件,哈希查不到,YARA 规则也可能没覆盖。这时网络型 IOC 就有意义:从文件内容里提取可能的外联域名和 IP,去情报源查它的信誉。提取过程分三步:抽字符串、正则匹配、白名单过滤。
import re import ipaddress DOMAIN_RE = re.compile(rb"(?![0-9])(?:[A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,}") IP_RE = re.compile(rb"(?<![0-9.])(?:\d{1,3}\.){3}\d{1,3}") def extract_strings(data: bytes, min_len: int = 6): chunks, cur = [], bytearray() for byte in data: if 32 <= byte < 127: cur.append(byte) else: if len(cur) >= min_len: chunks.append(cur.decode("ascii")) cur = bytearray() if len(cur) >= min_len: chunks.append(cur.decode("ascii")) return chunks def extract_network_indicators(data: bytes, whitelist=None): whitelist = whitelist or {".png", ".jpg", ".gif", ".exe", ".txt", ".pdf"} domains, ips = set(), set() for s in extract_strings(data): for m in DOMAIN_RE.finditer(s.encode("ascii", "ignore")): domain = m.group().decode().lower() if any(domain.endswith(ext) for ext in whitelist): continue domains.add(domain) for m in IP_RE.finditer(s.encode("ascii", "ignore")): token = m.group().decode() try: ip = ipaddress.ip_address(token) if not ip.is_private: ips.add(str(ip)) except ValueError: pass return list(domains)[:50], list(ips)[:50]这个函数有两个关键取舍。一是最小可打印字符串长度设为 6,太短会产生大量无效匹配;二是对 IP 做is_private过滤,内网地址查到情报源也没有意义,还浪费配额。末尾的[:50]限制每次扫描的查询数量,防止一个大文件带出几百个域名,把远程 API 配额打光。
3.5 汇总评分表:多引擎结果不是简单相加
多引擎判定追求的是组合置信度,单点命中不直接翻红。哈希命中本来就代表“见过这个样本”,直接给高分;YARA 命中代表内容特征符合某个家族;域名情报命中只能说明这个文件可能外联到已知 C2,误报空间较大。汇总策略如下表:
| 命中来源 | 加分 | 说明 |
|---|---|---|
| 本地哈希 | +90 | 已知恶意样本,近乎实锤 |
| YARA 规则 | 按 meta.score | 没有 score 时按默认 60 |
| 网络域名 | +40 | 每个域名单独计算,取最高分 |
| 网络 IP | +30 | 同样取最高分 |
最终判定时,总分聚到 0~100,然后按阈值切分:
def verdict(total_score: int) -> str: if total_score >= 80: return "malicious" if total_score >= 50: return "suspicious" return "clean"汇总时用min(total, 100)做上限,并且把每个命中来源记进日志,方便后面回溯“为什么这个文件被判了恶意”。搜索场景里,判定结果里必须带得分明细,否则运营人员没法根据结果反查误报根因。
4. 让检测系统持续可用:情报更新、增量扫描与日志告警
4.1 定时拉取远程情报,用 upsert 写进 SQLite
静态检测系统最怕情报过期:昨天还正常的域名今天变成了 C2,本地规则库却没有同步。常见做法是写一个独立的同步任务,每小时或每六小时拉一次远程情报源,用 upsert 写进本地库。
import sqlite3 import requests def sync_remote_iocs(api_url, token, db_path, source_name): headers = {"Authorization": f"Bearer {token}"} resp = requests.get(api_url, headers=headers, timeout=20) resp.raise_for_status() rows = resp.json().get("results", []) with sqlite3.connect(db_path) as conn: for item in rows: sha = item.get("sha256") or item.get("hash") if not sha: continue conn.execute( """ INSERT INTO hash_ioc (sha256, source, created_at) VALUES (?, ?, ?) ON CONFLICT(sha256) DO UPDATE SET source = excluded.source """, (sha, source_name, item.get("created_at")), )ON CONFLICT(sha256) DO UPDATE这一段是关键:只用INSERT OR REPLACE会把旧记录的created_at覆盖成当前时间,导致“这条情报是什么时候入库的”这个信息失真。SQLite 的 upsert 语法要求版本不低于 3.24.0,部署环境比较老的项目可以先做一次SELECT再决定 insert 或 update。调度不用上 Celery,cron 加一行命令就够,重点是把同步过程和扫描进程解耦,避免互相阻塞。
4.2 增量扫描:用 mtime 和 size 做状态跳过
对目录持续监控通常想到 watchdog,但 watchdog 的事件回调在进程重启后会丢失历史状态。我一般用轮询方案:记录每个文件的(mtime, size),扫描时先比对状态,没变化就直接跳过。
import json import os from pathlib import Path def incremental_scan(root_dir: str, state_file: str, detector): state = {} if os.path.exists(state_file): state = json.loads(open(state_file, encoding="utf-8").read()) for path in Path(root_dir).rglob("*"): if not path.is_file(): continue st = path.stat() fingerprint = [st.st_mtime, st.st_size] old = state.get(str(path)) if old == fingerprint: continue result = detector.scan_file(str(path)) write_detection_log(result) state[str(path)] = fingerprint tmp = state_file + ".tmp" with open(tmp, "w", encoding="utf-8") as fp: json.dump(state, fp) os.replace(tmp, state_file)这段代码里最重要的是“先写临时文件再os.replace”的动作。如果直接写回原文件,进程在写一半被杀掉,状态文件损坏,下次启动会把所有文件重新全量扫一遍。(mtime, size)这个组合指纹覆盖绝大多数“文件没变”的场景;攻击者用 touch 改时间戳的样本另说,那就得上文件哈希了,成本完全不同。
4.3 日志字段设计与告警回调
检测日志要能被后续的运营系统消费,直接打印在屏幕上没有意义。我建议输出 JSON 行格式,字段统一,写文件或直送消息队列都方便。
| 字段 | 类型 | 示例 |
|---|---|---|
| ts | string | 2024-06-01T12:30:00Z |
| file_path | string | /data/uploads/b.bin |
| sha256 | string | 5d41402abc4b2a76b9719d911017c592 |
| verdict | string | malicious / suspicious / clean |
| score | int | 90 |
| matched_rules | list | ["Ransomware_A", "ioc:hash"] |
告警部分只需要一个回调函数:当 verdict 是 malicious 时,把这条 JSON 行 POST 到 webhook 地址。这里不展开企业级消息队列,生产环境把 webhook 换成 Kafka 或钉钉的入站机器人是同样的结构。注意请求要带timeout,告警通道挂掉不能反过来阻塞主扫描线程。
5. 在真机上跑起来:最小验证流程与三个容易被忽略的边界
5.1 一条命令把检测流程跑通
先准备一个验证环境:Python 3.10+,安装 yara-python 和 requests,建一个rules/目录放 YARA 规则。验证流程不需要真实恶意样本,用 EICAR 测试文件就可以把链路打通。
cat > /tmp/eicar.txt <<'EOF' X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* EOF python cli.py scan /tmp/eicar.txt --rules rules/ --db iocs.db这里用 heredoc 而不是 printf 直接写内容,是因为%和\都会被 shell 解释,heredoc 加单引号定界符能原样保留文件内容。跑通后观察输出里的score字段:如果 YARA 规则命中 EICAR 特征,分数会抬到恶意区间;没命中就检查规则路径是否被正确加载。
5.2 回归验证:正反样本各准备一组
验证检测系统不能只用一个测试文件。常见做法是准备两个目录,pos/放已知的恶意样本或 EICAR 变体,neg/放正常的系统工具和文档,然后跑批量扫描,统计真阳性率和误报率。命令行里加一个--expect参数,批量扫描时自动比对预期结果,输出行数差异即可。
python cli.py batch-scan --dir pos/ --expect malicious python cli.py batch-scan --dir neg/ --expect clean误报率比检出率更重要:在一个十万文件的目录上跑出 1% 误报,意味着每天有一千条假告警要处理。新增一条 YARA 规则,先放到独立的staging/目录试跑一轮,别直接合并进正式规则库。
5.3 三个容易被忽略的边界
第一,文件扩展名不可信,扫描前先识别真实文件类型。用 filetype 读文件头,只对 PE、ELF、脚本和文档执行对应引擎,避免在图片资源上浪费时间。第二,远程情报查询要做降级处理。OTX 这类免费接口有配额限制,批量扫描时同一个地址会在多个样本里反复出现,内存缓存把重复查询挡在 HTTP 之前;接口超时直接跳过,不能让情报查询阻塞整条流水线。第三,YARA 规则的版本要锁住。第三方规则仓库更新频繁,某次更新可能把正常软件误判为恶意,建议给规则目录加版本号,检测结果里带上规则版本,排错时才知道这次判定是用哪套特征算出来的。
本文还有配套的精品资源,点击获取