基于机器学习与异常检测的APT攻击识别实践

📅 2026/8/27 12:25:24
基于机器学习与异常检测的APT攻击识别实践
简介在网络安全领域高级持续性威胁APT因其隐蔽性强、攻击链长而难以被传统特征匹配方案发现。基于机器学习的异常检测技术通过构建正常行为基线识别偏离基线的可疑活动为APT检测提供了新思路。其核心原理是利用无监督模型如孤立森林从海量网络日志中召回离群点再结合有监督模型进行精排从而在可接受的误报率下提升攻击发现能力。这一技术路径在安全数据分析、态势感知等场景中具有重要应用价值尤其适合中小型企业基于会话日志的检测场景。本文围绕基于Python与scikit-learn的工程实现重点介绍了特征工程、模型组合及调优经验展示了如何将机器学习落地到APT检测的实际业务中。 如果你在安全领域待过一段时间一定听说过 APT高级持续性威胁这个名字。它不像普通木马那样打一枪换一个地方而是有组织、有耐心地潜伏在企业内网里可能几个月甚至一两年都不被发现。传统基于特征库的检测规则在面对 APT 时往往力不从心——不是不够勤奋而是攻击者太会变通换一个 C2 域名、改一下载荷特征签名就失效了。所以这两年业内越来越多地把目光转向机器学习用行为画像替代特征匹配用异常发现替代已知匹配。这篇文章就围绕一个基于机器学习的 APT 检测项目来写我会把整套代码的核心思路、特征怎么设计、模型怎么选、踩过哪些坑都展开讲一遍。内容面向两类人一类是刚接触安全数据分析、想用机器学习试水 APT 检测的初学者另一类是已经在做日志分析或态势感知、想提升检测能力的乙方或甲方安全工程师。文章里的代码用 Python scikit-learn 实现不需要 GPU普通服务器就能跑。1. 为什么用机器学习做 APT 检测从特征匹配到异常发现1.1 传统检测方案在 APT 面前的短板先要弄清楚传统检测为什么对 APT 不灵光。市面上大多数 IDS/IPS包括很多 EDR 产品底层依赖的还是规则和特征库。规则引擎的工作模式是把流量或主机行为拆成一个个证据然后和规则库里预先写好的签名做比对命中就告警。这套逻辑对付脚本小子和已知攻击确实有效但 APT 的核心手法是定制化攻击——攻击者会针对目标环境专门改写工具、更换通信协议、甚至利用 0day 漏洞。只要特征没被收录流量在检测引擎眼里就是正常的。再补一个更尴尬的点APT 攻击链很长从最初的钓鱼邮件到最终的数据外传中间要经历横向移动、权限维持、凭据窃取等多个阶段。如果规则引擎只盯着单一阶段的特征很多中间行为单独看和正常运维操作没有区别。比如一条凌晨 3 点的远程桌面登录记录从规则上很难判定它是不是攻击但如果把它放进用户行为历史 全网横向对比的上下文里异常程度立刻就会凸显出来。机器学习做的事本质上是把检测从查已知变成找不同。它不看某个特征是否命中签名而是看这批数据整体上是否符合正常基线。偏离基线越多越有可能是异常。这个思路天然适合 APT 检测因为你不需要知道攻击者长什么样只需要知道他干了和平时不一样的事。1.2 机器学习检测的核心逻辑用机器学习做 APT 检测管线一般是这样的流量/日志采集 - 特征提取 - 模型推断 - 告警。其中特征提取是承上启下的关键一环模型能不能学到东西取决于你喂给它的特征矩阵里有没有区分度。这里要区分两类任务有监督分类和无监督异常检测。有监督的方案比如随机森林、XGBoost需要大量打好标签的攻击样本和正常样本。问题在于 APT 攻击样本稀缺很多企业内部根本找不到足够的正样本去训练。无监督的方案比如孤立森林、One-Class SVM、自编码器只使用正常数据训练然后找出偏离正常分布的离群点。在某些场景下还会用半监督方案用少量已知攻击样本来辅助校准正常模型。在实际工程里我见过最多的落地组合是无监督做召回、有监督做精排先用孤立森林在所有流量里筛出一批可疑会话再由一个训练好的二分类模型对这批准告警做二次判定降低误报率。这个思路在后文代码里也会体现。很多人一上来就想上深度网络其实对于安全告警场景可解释性比模型复杂度更重要。为此这条技术路线里应该优先选择能输出哪个特征贡献最大的模型而不要一上来就堆 LSTM 或 Transformer。2. 整体设计思路与方案选型2.1 数据来源与场景假设在开始写代码之前得先确定数据从哪来。APT 检测的数据来源主要有四个层面网络流量数据NetFlow、全包抓取、DNS 日志、HTTP 会话日志终端行为数据进程创建、注册表写入、文件操作、计划任务变更登录认证日志成功/失败登录、远程访问、账号异常使用邮件与办公数据附件哈希、钓鱼链接、宏行为我这里选择的场景是基于网络会话日志的异常检测因为网络日志最容易获取也最贴近多数中小企业的实际能力。输入是一批 CSV 格式的会话记录每条记录是一个网络连接的聚合信息比如源 IP、目标 IP、目标端口、协议、时长、上下行流量包数、字节数等。这里要强调一个实际操作中的观点不要一开始就追求全流量抓包分析那是大厂才玩得起的基础设施。对大多数团队而言把 NetFlow 或防火墙日志里的会话记录拿来做分析已经能覆盖相当比例的横向移动和数据外传行为。先把这条线跑通再考虑要不要扩容。2.2 特征工程决定模型天花板很多入门者容易犯一个错误把原始字段直接丢进模型期望它自己学出规律。但流量数据里很多字段是 ID 类、枚举类或高度偏态的不加工直接用模型很难学到有意义的信息。特征工程要做的是把一条会话记录转化为一组能反映行为意图的数字特征。我在这套项目里用的特征分三类第一类是基础统计特征。包括会话时长、上行包数、下行包数、上行字节数、下行字节数、平均包长、包长标准差等。这些特征刻画了单条会话的基本体格。第二类是分布与熵特征。比如目标端口的熵值、DNS 查询的域名长度、域名熵、访问的独立目标 IP 数量等。这里尤其推荐目标端口熵正常业务访问的端口集中在少数几个而扫描行为会均匀访问大量端口熵值显著偏高。第三类是时序与上下文特征。包括同一源 IP 在过去 1 小时内的连接频次、失败连接占比、访问的外部目标域名数量等。这些特征能捕捉到 APT 典型的低频慢速动作模式——不是突然爆发一次而是每天稳定地多那么几条可疑外联。特征工程需要结合具体场景反复迭代先做出一个基线版本再根据漏报和误报反推特征是否需要增强。完整代码里我封装了 fetch_features 函数输入原始 DataFrame输出可以直接进模型的特征矩阵。2.3 模型选型为什么选择无监督 监督组合整个项目里我并没用单一模型而是采用了孤立森林 随机森林的组合方案。理由如下孤立森林非常适合高维异常检测它速度快、对线性不可分的数据也不敏感而且不依赖样本分布假设。但它有一个弱点对局部异常不敏感也就是那些在特定群体内部偏离、但整体看来并不突出的点。随机森林作为二次判定模型可以弥补这个短板。先用孤立森林在全量数据上做大范围召回把得分最高的前 5% 或前 10% 会话捞出来然后由随机森林模型做细粒度判定。随机森林训练时有标签它可以学习到什么样的特征组合下更像恶意行为而非只靠偏离度。深度学习方案我也试过比如用 AutoEncoder 重建误差做异常检测。效果并不比孤立森林好多少反而训练和推理成本高了一大截。在一个需要快速交付的检测项目里简单模型带来的可维护性优势是很明显的。3. 核心代码实现从数据集到检测模型3.1 环境与依赖我用的环境是 Python 3.9依赖如下pandas1.5.0 numpy1.23.0 scikit-learn1.2.0 matplotlib3.6.0 joblib1.2.0安装直接pip install pandas numpy scikit-learn matplotlib joblib就行。没有特殊版本要求保持默认安装即可。Windows / Linux 都能跑但生产环境建议部署在 Linux 上。3.2 数据加载与特征预处理预处理这一步要注意几个细节。一是缺失值处理网络日志经常有字段缺失如果某条记录的目标端口为空可以直接丢弃不需要强行填充。二是时间字段要转成 datetime 类型才能做按小时的窗口统计。三是对枚举字段协议类型、目标端口做编码时最好保留原始数值型字段不要做 one-hot 展开否则特征维度爆炸、模型训练变慢。预处理代码大概长这样import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler def load_and_preprocess(file_path): df pd.read_csv(file_path) df[timestamp] pd.to_datetime(df[timestamp]) df df.dropna(subset[dst_port, src_ip, dst_ip]) df[hour] df[timestamp].dt.hour df[day_of_week] df[timestamp].dt.dayofweek return df实际项目里建议再加一步基于 src_ip 做分组聚合生成每个源 IP 的窗口统计特征。这一步要用 groupby 做注意窗口大小的选择。太短比如 5 分钟容易得到大量全零的稀疏特征太长比如 24 小时又会把瞬时爆发的异常淹没。我常用的是 1 小时滑动窗口既不漏掉低频慢速外联也能看到短时间段内的突发行为。3.3 模型训练与评估训练阶段分为两步先训练孤立森林模型再做有监督精排模型的训练。孤立森林的训练不需要标签直接丢进特征矩阵就可以。它有一个核心参数是 n_estimators默认值是 100数据量不大时够用如果特征维度很高可以适当调到 200。contamination 参数表示预期异常比例AP T 场景下我一般设 0.05 到 0.1 之间不要设得太高否则会引入大量误报。随机森林精排模型的训练需要构造标签。方案是这样的把孤立森林得分最高的 10% 数据标记为可疑正样本再从剩余数据里随机抽样等量的正常样本交给专家做人工复核后生成标签。这一步在代码里叫 semi_labeling本质上是一种伪标签策略。它不需要标注全部数据只需要标注模型捞出来的少量可疑样本标注成本大大降低。训练完成后用 precision、recall、F1 三个指标做评估。这里要提醒一下安全场景里 F1 不是唯一标准业务上通常更关注在可接受的误报率下能达到多高召回率。比如每天误报 100 条但能抓到 80% 的横向移动行为可能比每天误报 10 条但只能抓到 30% 更有价值。这个权衡需要在部署前和安全团队对齐。3.4 异常检测完整流程代码下面我把核心检测流程完整写出来。这个代码可以在真实环境里跑通但数据格式需要调整成你自己的字段名。import pandas as pd import numpy as np from sklearn.ensemble import IsolationForest, RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score import joblib class APTDetector: def __init__(self, contamination0.1): self.contamination contamination self.scaler StandardScaler() self.iso_forest IsolationForest( n_estimators200, contaminationcontamination, n_jobs-1, random_state42 ) self.rf_clf RandomForestClassifier( n_estimators200, max_depth10, min_samples_leaf5, n_jobs-1, random_state42 ) self.feature_columns None def fetch_features(self, df): df df.copy() df[total_bytes] df[up_bytes] df[down_bytes] df[total_packets] df[up_packets] df[down_packets] df[avg_packet_len] df[total_bytes] / (df[total_packets] 1e-9) df[packet_len_std] df[[avg_packet_len]].rolling( window10, min_periods1 ).std().fillna(0) # 目标端口熵特征 port_freq df.groupby(src_ip)[dst_port].transform(nunique) df[dst_port_entropy] - (port_freq / (port_freq.sum() 1e-9)) * \ np.log((port_freq / (port_freq.sum() 1e-9)) 1e-9) # 1小时窗口源IP连接频次 df[ip_window_count] df.groupby(src_ip)[timestamp].transform(count) # 失败连接占比 df[fail_ratio] (df[resp_code] -1).astype(int) feature_cols [ duration, up_bytes, down_bytes, up_packets, down_packets, total_bytes, total_packets, avg_packet_len, packet_len_std, dst_port_entropy, ip_window_count, fail_ratio ] self.feature_columns feature_cols return df[feature_cols] def fit(self, df): X self.fetch_features(df) X_scaled self.scaler.fit_transform(X) # 无监督阶段孤立森林 self.iso_forest.fit(X_scaled) scores self.iso_forest.decision_function(X_scaled) # 伪标签生成 threshold np.percentile(scores, 10) pseudo_label (scores threshold).astype(int) # 以伪标签训练随机森林精排模型 X_train, X_test, y_train, y_test train_test_split( X_scaled, pseudo_label, test_size0.3, random_state42, stratifypseudo_label ) self.rf_clf.fit(X_train, y_train) y_pred self.rf_clf.predict(X_test) print(classification_report(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, self.rf_clf.predict_proba(X_test)[:, 1])) def predict(self, df): X self.fetch_features(df) X_scaled self.scaler.transform(X) iso_score self.iso_forest.decision_function(X_scaled) rf_score self.rf_clf.predict_proba(X_scaled)[:, 1] # 综合得分无监督监督组合 final_score 0.4 * (1 - (iso_score - iso_score.min()) / (iso_score.max() - iso_score.min() 1e-9)) \ 0.6 * rf_score return final_score def save_model(self, pathapt_model.joblib): joblib.dump({ scaler: self.scaler, iso_forest: self.iso_forest, rf_clf: self.rf_clf, feature_columns: self.feature_columns }, path) def load_model(self, pathapt_model.joblib): data joblib.load(path) self.scaler data[scaler] self.iso_forest data[iso_forest] self.rf_clf data[rf_clf] self.feature_columns data[feature_columns]这段代码里最值得说明的是 predict 方法里的综合得分逻辑。孤立森林的 decision_function 输出的是正常程度得分值越大越正常。我在组合前先把它反转映射到 0-1 区间让它的含义变成异常程度。随机森林的输出本身就是异常概率两者加权平均得到最终分数。这里有个细节加权系数 0.4 和 0.6 是怎么来的我是通过网格搜索在验证集上试出来的scope 是让 F1 最高的组合。你也可以改成 0.5 对 0.5差别不会太大。但不要给随机森林权重太高否则在正样本极少时模型会倾向于输出低概率什么都报不成异常。3.5 端到端检测脚本模型训练完成后需要把它接入一个定时扫描流程。我习惯写一个脚本每天凌晨跑一次读取过去 24 小时的日志数据调用模型预测把得分超过阈值的会话写入告警表。import pandas as pd from apt_detector import APTDetector detector APTDetector() detector.load_model(apt_model.joblib) # 加载当天新增日志 df pd.read_csv(/data/logs/2024-01-01_sessions.csv) scores detector.predict(df) # 阈值可以用分位数动态调整也可以用固定值 threshold 0.8 alerts df.loc[scores threshold].copy() alerts[score] scores[scores threshold] # 按分数排序输出 TOP 200 alerts alerts.sort_values(score, ascendingFalse).head(200) alerts.to_csv(/data/alerts/alerts_2024-01-01.csv, indexFalse)实际部署时不要用固定阈值因为数据分布会随时间漂移。我常用的做法是用近 7 天得分的 95 分位数作为动态阈值。这样在业务高峰期阈值会自动抬升业务低谷期阈值自动下降比较不容易产生大批量误报。4. 模型调优与效果评估4.1 关键指标解读搞安全的人不一定都熟悉机器学习指标。这里做个通俗解释精确率Precision表示模型报出来的可疑事件里有多少是真攻击召回率Recall表示所有真实攻击里模型抓到了多少。安全场景里两者永远在打架追求高召回必然带来高误报追求高精确又会漏报。我建议在 APT 检测这个场景里优先看 RecallTopK。也就是说在模型输出的得分最高的前 100 条或前 500 条里有多少条是人工核实过的真攻击。这个指标比全局 F1 更贴近实际使用体验因为它模拟了分析师每天只看前 N 条告警的真实工作方式。完整代码里我用 classification_report 输出所有指标但真正决策时看的是 TopK 命中率。另一个容易踩的坑是数据不平衡。APT 检测场景里正样本可能只占万分之几甚至更少。如果你直接把原始分布丢给随机森林它会把几乎所有样本都预测为正常因为这样做准确率也有 99.9%。所以代码里用了伪标签和 stratify 分层采样保证训练集里正负样本比例不至于太离谱。4.2 阈值选择与误报控制误报率是 APT 检测系统能不能长期跑下去的关键。一个每天生成几百条告警、但 99% 都是误报的系统用不了两周就会被安全团队弃用。所以在调优阶段阈值选择要格外谨慎。我常用的方法先让模型对近 30 天的日志做一次全量回放输出每天得分阈值为 0.9、0.8、0.7 时的告警量画一条趋势线。正常业务下每天的告警数量应当相对平稳。如果某一天告警量突然暴涨要么那天发生了异常事件要么数据源出现故障——这两种情况都值得关注。还有一个工程小技巧给每个告警加一个上下文维度比如源 IP 是否关联到域控服务器、目标 IP 是否为外部地址、是否属于首次出现的外部通信。这些规则和机器学习打分做与逻辑组合可以把误报率再压低一个量级。这里要注意主次规则是辅助不是主体否则你又走回了传统检测的老路。4.3 特征组合的迭代思路模型上线后并不是一劳永逸的。APT 攻击者在不断变化你的特征也需要持续迭代。迭代方向有两个维度一是增加新特征源比如把威胁情报里的恶意 IP 列表作为特征合并进来二是改造特征计算方式比如从固定窗口统计升级为动态滑动窗口统计。判断特征是否有效最简单的方法是看特征重要性排序。随机森林训练完成后可以直接打印 feature_importances_。我发现很多场景里排在前三的特征往往是同一源 IP 的连接数、目标端口的熵值和平均包长这符合 APT 横向移动和数据外传的行为特征它会突然访问很多不同端口的主机同时外传时的流量包往往比正常业务更整齐。一个我反复踩过的坑不要在特征里加入任何未来信息。比如用整天的数据统计窗口特征然后用这个特征预测当天下午的会话这属于特征泄露。正确做法是统计窗口截止于当前时间之前比如预测 t 时刻的会话特征统计应该只使用 t-1 小时之前的数据。5. 常见问题与排错实录5.1 问题速查表下面是这个项目跑下来最常遇到的几个问题直接整理成表格方便对照排查现象可能原因解决办法训练时损失不收敛、AUC 接近 0.5特征泄露统计特征混入了未来信息检查特征计算窗口确保只使用历史数据告警数每天基本都是 0阈值设置过高降低固定阈值或改用近 7 天得分的 95 分位数告警数每天几千条全是误报数据源异常常见于 NetFlow 日志缺失导致端口字段为 0检查数据清洗环节剔除 dst_port0 的会话模型在测试集效果好上线后变差数据分布漂移加入周期性重训练流程建议每周跑一次特征重要性里时间字段排第一正常业务有明显的昼夜规律时间特征与业务强相关区分业务系统特征不同系统分组建模训练速度过慢特征维度太高或 n_estimators 过大先跑一次特征重要性删掉尾部不重要的特征5.2 踩坑记录这里分享几个实际项目中让我印象深刻的踩坑瞬间都是代码层面和经验层面的教训。第一个坑是重复样本导致模型过拟合。网络日志里同一条会话可能被多个采集器重复上报直接进模型会让模型记住这些重复样本误以为它们是重要特征。解决办法是在预处理阶段对 session_id 或src_ip, dst_ip, dst_port, timestamp做去重。这个动作看起来不起眼但能显著提升模型的泛化能力。第二个坑是孤立森林对离群点过于敏感。孤立森林本来就擅长找离群点但日志数据里有大量天然离群的运维行为比如管理员手动执行数据库全表扫描、备份系统发起大流量传输。这些都会被模型标记为高分异常。我的应对措施是维护一个白名单源 IP列表在特征里加一个 is_whitelisted 的布尔特征让模型学会忽略这些正常运维行为。第三个坑是模型文件名和版本管理混乱。这个项目迭代了十几个版本如果每个版本都叫 apt_model.joblib回滚时会非常痛苦。我后来改成了按日期命名apt_model_20240101.joblib并且保留最近 30 天版本。模型回滚在安全系统里很重要一旦某个新版误报率激增必须能快速切回旧的稳定版本。6. 部署落地与日常运营经验6.1 模型上线前要做的事先把一个安全检测系统部署上线不只是把模型文件放到服务器上完事。我强烈建议先做两周的影子模式运行模型在线跑但告警只写入日志库不推送给安全团队。这样做的目的是积累足够多的预测结果用于估算真实误报率、验证阈值是否合理同时也能发现数据源和特征计算环节的潜在问题。影子模式的另一个好处是可以积累一批高质量的标注样本。这些样本来自模型召回、人工不介入的纯自然分布标注出来的数据比上线后的人工复核样本更干净可以用于后续重新训练精排模型。6.2 告警运营闭环模型只是检测环节完整的闭环还包括告警处置和反馈回传。我建议每次安全分析师处理一条告警时都记录一个最终判定字段true_positive真攻击、false_positive误报、duplicate重复告警。这些反馈数据定期回流到训练集重新训练随机森林精排模型形成一个非常朴素但有效的反馈闭环。没有反馈闭环的检测系统用半年后效果会肉眼可见地变差。原因很简单业务在变、IT 环境在变静态模型永远追不上动态环境。有了反馈数据模型才能持续学习哪些新出现的行为其实是正常的。6.3 这套方案的适用边界最后说点掏心窝子的话。这套基于机器学习的 APT 检测方案更适合中小型企业和单一业务线的场景。它成本低、见效快、有可解释性能覆盖大部分横向移动和数据外传场景。但如果你面对的是国家级攻击者或者需要在一线城市的大型企业网络里做全流量检测这套方案就不够用了——那种场景需要更复杂的图神经网络、深度行为建模和规模化实时计算平台不是一两篇文章能讲完的。从我个人经验看机器学习在安全检测里的定位一直是辅助人而不是替代人。模型帮分析师生成候选、排序优先级、压缩攻击面最后由人来做最终判定。这个定位放清楚你设计系统的时候就不会纠结要不要上大模型、要不要做全自动化而是会把精力放在怎么把特征做扎实、把反馈闭环跑通上。本文还有配套的精品资源点击获取