简介:本资源为基于机器学习的APT检测完整代码包,面向网络安全从业者、安全方向学生及研究人员,帮助解决高级持续性威胁难以识别、传统规则防护易被绕过的问题。项目以随机森林算法为核心,结合欠采样、过采样与SMOTE合成样本等异常检测策略,应对正常与恶意行为样本严重不平衡的场景,并涵盖数据清洗、特征选择、模型训练与性能评估等完整流程。资源包共869个文件,约215.7MB,以326个Python源码与290个pyc编译文件为主体,辅以txt说明、docx设计文档、pkl模型文件、png图表及pptx汇报材料,另含少量sh脚本、xml配置与csv数据,结构完整便于复现。目前已有676人学习下载。读者可据此获得一套可直接运行的APT检测工程代码,理解随机森林在流量与行为特征上的建模思路,掌握类不平衡处理与特征工程方法,并参考需求分析、概要设计等文档完成二次开发与实验对比。
1. 从告警洪水到攻击链:机器学习 APT 检测到底在解决什么
凌晨两点,安全运营中心的屏幕上同时弹出四百多条告警,值班同事一条条点开,全是某台服务器在非工作时间发起的异常外连。等人工排查到第三十条时,真正的 APT 攻击已经在内网横向移动了三个小时。这不是段子,是我做应急响应时反复遇到的场景。APT(高级持续性威胁)的核心特征就是慢、隐蔽、长期潜伏,它不会像勒索软件那样一夜之间加密所有文件,而是用合法账号、合法工具、低频心跳,一点点把权限做大。传统基于规则和单点阈值的检测在这种对手面前基本失效,因为攻击者的每一步单独看都「像正常运维」。
机器学习 APT 检测要解决的,就是把「单点异常」升级成「行为序列异常」。它不指望某一条日志能直接判定攻击,而是把主机进程、网络连接、登录事件、文件操作这些多源数据拼成时间线,用模型去学习「正常长什么样」,再把偏离基线的行为簇标出来。适合谁做?有基础日志采集能力(EDR、Sysmon、NetFlow、Windows 事件日志任意一种起步)、能拿到 Python 环境、愿意花时间做特征工程的安全工程师。如果你手上只有一台笔记本和一份公开数据集,也完全可以先把整条链路跑通,再往生产环境迁移。这一篇就按「数据从哪来、特征怎么造、模型怎么选、代码怎么写、坑在哪」的顺序,把一套可复现的机器学习 APT 检测代码讲透。
2. 数据与特征:APT 检测模型的地基怎么打
2.1 为什么 APT 检测不能直接喂原始日志
很多人第一反应是把 Sysmon 日志直接丢给模型,结果训练出来的东西要么过拟合到某台机器的进程名,要么在测试集上 AUC 0.99、上线后全是误报。原因在于原始日志是「事件流」,而 APT 的行为是「模式」。同一个powershell.exe启动,正常运维和攻击者调用它的参数、父进程、后续动作完全不同。模型要学的是这些上下文关系,不是单个字段。
所以第一步一定是把事件流切成「行为窗口」。常见做法是以主机 + 用户为维度,按固定时间窗(比如 5 分钟)或会话边界聚合,把窗口内的进程创建、网络连接、文件写入、注册表修改统计成向量。这个思路在公开的 APT 检测数据集里是主流,比如 DARPA 的透明计算项目、以及各类主机审计日志竞赛数据,都是这么组织的。窗口太短会切碎攻击链,太长会稀释异常信号,5 到 15 分钟是我实测下来比较稳的区间,具体要看你的日志密度。
2.2 用 pandas 把原始日志聚合成行为窗口
下面这段代码假设你有一份 CSV 格式的进程与网络事件日志,字段包括timestamp、host、user、event_type、process、parent_process、dst_ip、dst_port、cmdline。这是最常见的起步格式,EDR 导出、Sysmon 转 CSV 都能对上。
import pandas as pd import numpy as np # 读取原始事件日志,parse_dates 保证时间列是 datetime 类型 df = pd.read_csv("events.csv", parse_dates=["timestamp"]) # 按主机+用户分组,5 分钟窗口聚合 df = df.sort_values("timestamp") df["window"] = df.groupby(["host", "user"])["timestamp"].transform( lambda s: s.dt.floor("5min") ) # 统计每个窗口内的行为计数特征 agg = df.groupby(["host", "user", "window"]).agg( proc_count=("process", "count"), unique_proc=("process", "nunique"), net_conn=("dst_ip", lambda x: x.notna().sum()), unique_dst=("dst_ip", "nunique"), unique_port=("dst_port", "nunique"), ).reset_index() # 补充「罕见进程」特征:该进程在全量数据中出现次数越少,越可疑 proc_freq = df["process"].value_counts() df["proc_rarity"] = df["process"].map(proc_freq).fillna(1) rarity = df.groupby(["host", "user", "window"])["proc_rarity"].min().reset_index() agg = agg.merge(rarity, on=["host", "user", "window"])逻辑说明:先按主机和用户切分,是因为不同机器的正常基线差异极大,混在一起训练等于让模型学一个不存在的「平均正常」。floor("5min")把时间对齐到窗口起点,避免窗口漂移。聚合特征里,unique_proc和unique_dst反映行为多样性,APT 在侦察阶段往往会出现异常多的目标 IP 或异常少的进程种类。proc_rarity是关键补充,它把「这个进程平时几乎不出现」这个信号量化出来,比单纯计数更能抓住白利用(living-off-the-land)攻击。
参数说明:窗口大小5min是超参,日志稀疏就调到15min,日志密集(每秒几十条)可以压到1min。proc_rarity用最小值而不是均值,是因为一个窗口里只要出现一个极罕见进程,就值得警惕,均值会被大量正常进程拉平。
2.3 标签怎么来:弱监督与攻击注入
APT 检测最头疼的是没有干净标签。真实环境里攻击样本极少,公开数据集又和你的环境分布不一致。我的做法是两条腿走路:一是用公开数据集的攻击标签做预训练,二是用攻击模拟(比如 Atomic Red Team 这类开源测试用例)在自己的测试环境里注入行为,生成带标签的样本。注入时要注意,别只跑一条命令就完事,要按 ATT&CK 的战术顺序串起来,否则模型学不到「链」的特征。
标签构造上,把窗口标记为1的条件是:该窗口内至少有一个事件属于已知攻击技术。这样会带来标签噪声,因为攻击窗口里大部分行为仍是正常的,但这是弱监督的常态,后面用集成模型和阈值调优来吸收。不要追求 100% 精确的标签,那在 APT 场景里不存在。
3. 模型选型与训练:从基线到集成
3.1 为什么我首选树模型而不是深度学习
一提到机器学习检测,很多人想上 LSTM、Transformer。但在 APT 这种表格型、特征维度几十到几百、样本量几千到几万的场景里,梯度提升树(XGBoost、LightGBM)几乎总是先赢一局。原因很实际:训练快、可解释、对特征缩放不敏感、小样本下不容易崩。深度学习要发挥优势,得有百万级样本和序列建模需求,而 APT 检测的瓶颈往往在特征和标签,不在模型容量。
所以我的路线是:先用 LightGBM 建一个强基线,把特征重要性和误报情况摸清楚,再考虑要不要上序列模型。如果一定要做序列,也是在树模型给出的高可疑窗口上做二次分析,而不是一上来就端到端。
3.2 LightGBM 训练代码与类别不平衡处理
import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import precision_recall_curve, auc # agg 是上一节聚合好的特征表,label 列是 0/1 feature_cols = ["proc_count", "unique_proc", "net_conn", "unique_dst", "unique_port", "proc_rarity"] X = agg[feature_cols] y = agg["label"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) # scale_pos_weight 处理正负样本不平衡,负样本数/正样本数 spw = (y_train == 0).sum() / max((y_train == 1).sum(), 1) clf = lgb.LGBMClassifier( n_estimators=500, learning_rate=0.05, num_leaves=31, max_depth=-1, scale_pos_weight=spw, subsample=0.8, colsample_bytree=0.8, random_state=42, ) clf.fit(X_train, y_train, eval_set=[(X_test, y_test)], eval_metric="auc", callbacks=[lgb.early_stopping(50)]) # 用 PR 曲线选阈值,APT 场景更关心查准率 proba = clf.predict_proba(X_test)[:, 1] prec, rec, thr = precision_recall_curve(y_test, proba) pr_auc = auc(rec, prec) print(f"PR-AUC: {pr_auc:.4f}")逻辑说明:stratify=y保证训练测试集里攻击样本比例一致,否则小样本类别可能全被分到一边。scale_pos_weight是 LightGBM 处理不平衡的内置手段,比盲目过采样更稳。early_stopping(50)防止树加太多导致过拟合,验证集 AUC 连续 50 轮不升就停。最后用 PR 曲线而不是 ROC,是因为 APT 正样本极少,ROC 会被大量真负例撑得虚高,PR-AUC 更能反映实际检出质量。
参数说明:num_leaves=31是中小数据集的稳妥值,特征多、样本多可以加到 63 或 127,但要注意和max_depth配合,别让单棵树太深。learning_rate=0.05配n_estimators=500是慢学稳收敛的组合,赶时间可以 0.1 配 300。subsample和colsample_bytree都设 0.8,是防过拟合的常规操作,数据量小就再降。
3.3 阈值不是拍脑袋:按告警容量反推
模型输出概率后,真正的落地问题是「阈值设多少」。很多团队直接取 0.5,结果要么漏报要么把分析师淹了。正确做法是先确定你每天能处理多少条告警,比如 200 条,然后在验证集上按概率排序,取第 200 条对应的概率作为阈值。这样阈值是运营容量驱动的,不是模型驱动的。代码上就是把thr数组和prec、rec对齐,找到满足告警量约束的那个点。这个思路在安全运营里叫「告警预算」,比追求 F1 最大值实用得多。
4. 避坑与排查:那些让模型上线就翻车的细节
4.1 时间泄漏:训练集里混进了未来信息
现象:离线评估 PR-AUC 0.95,上线后跌到 0.6。原因:做窗口聚合或特征标准化时,用了全量数据的时间范围,导致训练窗口「看到」了测试期的统计量。解决:所有涉及全局统计的特征(比如进程频率、均值方差)必须只在训练时间段内计算,再应用到验证和测试。用TimeSeriesSplit代替随机切分,或者手动按时间戳切。这个坑我踩过不止一次,属于最隐蔽也最致命的一类。
4.2 主机基线漂移:新机器一上线就狂报
现象:新采购的服务器接入日志后,模型对它疯狂告警。原因:模型学的是老机器的行为分布,新机器的正常行为在特征空间里就是「异常」。解决:给每台主机维护独立的基线,或者把主机 ID 做目标编码后作为特征,让模型知道「这台机器的正常和别人不一样」。更简单的做法是新机器先跑两周影子模式,只记录不告警,积累够基线再纳入检测。
4.3 特征穿越:用了事件结束才知道的字段
现象:某个特征重要性高得离谱,但线上拿不到。原因:特征里混入了「事件结果」类字段,比如进程退出码、连接是否成功,这些在告警触发时刻还没产生。解决:严格区分「触发时可获取」和「事后才可获取」的字段,训练时只用前者。做法是把特征表按时间戳对齐到窗口起点,任何晚于窗口起点的事件字段一律剔除。
4.4 类别不平衡下的评估幻觉
现象:准确率 99%,但一个攻击都没抓到。原因:攻击样本占比不到 1%,模型全预测为正常也能拿高准确率。解决:永远看 PR-AUC、召回率@固定查准率,或者直接看混淆矩阵里攻击类的召回。准确率在 APT 检测里基本没有参考价值,别被它骗了。
4.5 模型更新滞后:攻击者也在进化
现象:上线三个月后检出率持续下降。原因:攻击手法变了,模型还停在旧分布上。解决:建立定期重训练机制,至少每月用新数据微调一次,同时监控特征分布漂移(比如用 PSI 指标)。发现某个特征分布明显偏移,就要排查是环境变化还是攻击变化,别急着全量重训。
5. 进阶技巧:把单窗口模型升级成攻击链检测
单窗口模型有个天花板:它只能判断「这 5 分钟像不像攻击」,判断不了「这几个窗口连起来是不是一条攻击链」。APT 的精髓在链,所以进阶方向是把窗口序列喂给第二层模型。我的做法是:第一层 LightGBM 输出每个窗口的可疑分数,第二层用一个简单的序列规则或 HMM,把连续多个高分窗口、且行为符合 ATT&CK 战术顺序的片段标为「攻击链」。这样既保留了树模型的可解释性,又补上了时间维度。
具体实现上,先按主机和时间排序,取第一层分数超过低阈值(比如 0.3)的窗口作为候选,然后检查候选序列里是否出现「侦察 → 初始访问 → 执行 → 持久化 → 横向移动」这类战术标签的递进。战术标签可以从进程名和命令行关键词映射,比如whoami、net user映射到侦察,schtasks映射到持久化。命中递进模式的候选链,分数再乘一个放大系数,优先推给分析师。
验证这套链路是否有效,别只看离线指标。我一般会做两件事:一是用历史真实事件回放,看攻击链能不能被完整串起来,中间断在哪一环;二是统计「链级告警」相比「窗口级告警」的误报压缩比,通常能压到五分之一甚至更低。如果压缩比不明显,说明第二层规则太松,需要收紧战术顺序的约束。
还有一个容易被忽略的技巧:把模型的输出反过来喂给特征工程。比如第一层模型认为某类进程组合可疑,就去日志里专门统计这类组合的历史出现频率,作为新特征加回第二层。这种「模型指导特征」的迭代,比盲目加特征有效得多。我自己的习惯是每轮重训练前,先看一遍特征重要性和误报样本,从误报里找规律,往往能挖出比调参更有价值的改进点。做 APT 检测没有一劳永逸的模型,只有持续迭代的流程,把评估、排查、重训串成闭环,才敢说这套代码真的能用。希望帮到你。
本文还有配套的精品资源,点击获取