ICMP异常检测实战:从特征工程到机器学习识别网络威胁
1. 为什么我建议把 ICMP 放进检测优先级前排做流量侧安全这些年最容易被忽视的协议里ICMP 一定排在前三。很多人一看到 ping、traceroute 就默认是运维日常规则常年不看不打。但真正做过网络安全入侵检测的都知道ICMP 是侦察阶段最省事的探针也是最容易被业务日志淹没的协议之一。它本身报文短、字段少、载荷内容通常没什么可读信息但这种“看起来无害”的特征恰恰让它在异常行为分析和机器学习模型训练里变成了一个非常有意思的检测对象。我之前在一次内网排查中遇到过这样的情况一台办公网主机在凌晨两点左右每隔三十秒向外网某个固定 IP 发送一次 ICMP Echo Request载荷长度每次都一模一样连续跑了将近十天。当时要不是因为那个时间段业务流量极少恐怕就会被当作风毛麟角的正常运维脚本忽略掉。后来一查发现这台机器确实中了某种恶意程序用它来确认外部的受控节点是否还活着。这个案例让我彻底改变了对待 ICMP 的态度它不承载业务数据但能承载攻击者的“心跳”。1.1 攻击者看中 ICMP 的三个现实原因首先是大多数网络不会彻底封死 ICMP。企业需要 ping 来排查链路、验证设备存活、测试连通性所以防火墙往往会对 ICMP Echo Request 和 Echo Reply 放行。攻击者不需要什么高级技巧只要沿着这条被放行的通道发几个包就能判断目标主机是否存在、目标端口是否可达、路径上是否存在异常设备。这是性价比极高的探测手段。其次是 ICMP 的响应信息本身就有情报价值。Echo Reply 能确认存活主机Destination Unreachable 能反映端口状态Time Exceeded 能还原路径拓扑。攻击者拿到一条 ICMP 响应往往就能决定下一步是针对端口扫描还是直接放弃这个目标。尤其在横向移动阶段很多攻击者会用 ICMP 快速扫一遍内网网段找出哪些机器是在线状态然后才上真正的攻击工具。第三个原因要落到防御视角上大部分告警平台对 ICMP 的监控策略都很粗糙。HTTP 有 URL、UA、响应码这些丰富的检测维度DNS 有域名、记录类型、响应内容可以分析而 ICMP 包的内容太少导致分析引擎默认认为它不值得投入太多算力。这个认知盲区给了异常行为存活的空间。我处理过不少安全事件事后回溯流量时都发现攻击者早就通过 ICMP 扫描踩过点只是当时的告警列表里完全没有这一条。1.2 ICMP 检测为什么和 Web 流量检测不是一回事Web 流量检测靠的是“内容”请求 URL 是否恶意、响应页面是否有攻击特征、UA 是否属于扫描器、载荷里有没有编码后的命令。这套思路搬到 ICMP 上是行不通的因为 ICMP 的载荷要么为空要么是一串几乎看不出含义的填充字符你很难靠“内容命中”来判断恶意行为。所以 ICMP 的检测必须走另一条路看行为。关注包速率、时间间隔、目的 IP 的熵、TTL 分布、ID 和 Sequence 的变化规律、请求与响应之间的对称性。这也是我在这篇文章里反复强调“特征字段”的原因对 ICMP 而言单包里的字段只是基础真正的检测价值在字段之间的关系、在时间上的波动、在统计分布上的偏移。做一个优秀的行为特征集比单纯写几条固定规则更能应对不断变化的攻击手法。2. ICMP 协议底层机制类型、代码与头部结构ICMP 全称是 Internet Control Message Protocol工作在网络层主要用来传递错误报告、网络诊断和控制消息。它没有 TCP 那样复杂的连接状态也不需要 UDP 那样的端口号虽然简单但攻击者能用它做的文章一点不少。2.1 必须记住的类型与代码组合ICMP 头部的前两个字节分别是 Type 和 Code。Type 表示消息类型Code 在同一个 Type 下进一步细分含义。做入侵检测的时候这些组合就是最基础的特征来源。下面这张表是我在实际检测中一定会盯住的组合TypeCode含义检测关注点00Echo Reply响应洪泛、反射放大攻击的响应侧30/1/2/3/4/13Destination Unreachable端口不可达常被用于扫描MTU 探测出现频率异常时要注意50/1/2/3Redirect异常重定向可能被用来改变流量转发路径80Echo Request扫描、洪泛、隐蔽通信最常见的入口110/1Time Exceededtraceroute 正常使用高频出现需要排查130Timestamp Request老式侦察工具会被用来收集时间信息170Address Mask Request收集子网掩码信息现代网络中很少见检测过程中看到最多的是 8/0 和 0/0也就是 Echo Request 和 Echo Reply。Type 3 的情况要结合 Code 来看比如 Type 3 Code 3 表示目的端口不可达如果极短时间内大量出现往往意味着端口扫描器发起的 TCP/UDP 探测已经收到了响应。Type 3 Code 4 表示需要分片但设置了不可分片标志这是路径 MTU 探测的正常产物但如果频率异常高也可能说明网络中有人在故意触发错误路径。2.2 Echo 报文头部拆解一个 Echo 报文的 ICMP 头部由 Type8 位、Code8 位、Checksum16 位、Identifier16 位、Sequence Number16 位构成后面跟着载荷数据。抓包的时候我们看到的icmp.ident是 Identifier 字段icmp.seq是序号字段。这两个字段在特征工程里很有价值因为它们能反映发起主机的行为习惯。正常情况下同一个 ping 进程使用的 Identifier 是相对稳定的Linux 实现里它通常和进程 PID 相关Windows 则有一定的随机性。Sequence 虽然随着每个包递增但不同实现的重置逻辑和起始值不一样。攻击者使用的扫描工具、自定义脚本往往不会严格遵守这种模式。举例来说有些工具会在很短时间内把 seq 跳到一个很大的值或者每个会话重新设置一个 Identifier 并从这个值开始计数。这些细微的不一致正好可以作为异常行为分析的切入点。TTL 字段在 IP 头部虽然不属于 ICMP 头部但它是把 ICMP 包放进特征集时不能忽略的一个字段。常见操作系统初始 TTL 有 64、128、255 这几种抓包时看到的值是经过跳数衰减后的值。通过分析 TTL 的分布可以大致推断对方主机的操作系统类型也可以感知网络中是否存在异常路由变化。2.3 载荷数据比预想中更能说明问题ICMP Echo 载荷区域在正常情况下是有规律的。常见的 ping 工具会填充一串可打印字符比如 Windows 的填充序列是“abcdefghijklmnopqrstuvwabcdefghi”这类的重复字母很多 Linux 实现填充的是!#$%()*,-./0123456789:;?ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~这样的递增字符序列。也就是说正规工具发出的 Echo 包载荷内容是肉眼可辨的文本。恶意程序则往往不会这么礼貌。为了把尽可能多的指令或回传数据塞进包里它们会使用加密、压缩或者自定义编码后的二进制内容表现出来就是载荷熵值显著高于正常文本、长度变得固定且和发送指令的大小相关。还有一些程序会把大 payload 拆成多个 ICMP 包发送导致同一会话内的包长明显超过常规 ping 的默认长度。这些都是建立 ICMP 检测特征时非常值得记录的重要字段。3. 完整特征字段清单从单包到会话级再到聚合级构建 ICMP 特征集不能只停留在“把 pcap 里的字段导出来”这个层面。不同层级的信息有不同的检测作用我一般把特征分成三层单包静态特征、会话/流级统计特征、面向机器学习的聚合特征。3.1 单包静态特征这一层最容易理解就是从每个 ICMP 包中直接提取的字段字段位置正常表现异常暗示TypeICMP 头常见 8/0、0/0罕见类型或组合CodeICMP 头常见 0非标准 Code可能来自畸形包ChecksumICMP 头数值正确校验错误往往说明包是伪造生成IdentifierEcho 头同一个 ping 进程内比较稳定频繁跳变多 ID 并行出现SequenceEcho 头一般按序递增大跳变、逆序、周期性异常Payload 长度载荷区域固定且不长通常在几十字节以内超大载荷或长度不变且不规律TTLIP 头初始值减去跳数与基线相差悬殊说明路径跳数异常Packet length以太网/ IP 层常规值上下浮动非正常大包或者分片异常需要注意的是单包特征单独看很难下结论。比如 Sequence 跳变也可能是因为设备驱动问题Checksum 错误也可能是网络传输损坏。真正有用的做法是把这些字段保留下来作为后续统计特征的原材料。3.2 会话与流级统计特征所谓“会话”我一般用源 IP、目的 IP、ICMP Type 这三个维度来划分。在这个范围内可以计算出一系列统计特征请求响应比Echo Request 数量除以 Echo Reply 数量。正常 ping 接近 1洪泛场景下可能远大于 1。包长均值与方差正常 ping 包长波动小恶意程序传输数据时包长会出现明显分层变化。TTL 均值与方差同一个源 IP 到多个目的 IPTTL 的方差如果很大说明目标可能分布在不同网络层级扫描特征会比较明显。Sequence 增量分布计算相邻包之间 seq 的差值正常增量稳定且固定扫描器和大流量程序则会出现不规则跳变。时间间隔均值与方差间隔固定且差别极小往往代表自动化脚本或恶意心跳。Payload 熵值对载荷字符计算信息熵正常文本填充字符熵较低加密或随机内容熵会明显升高。如果你只做一个检测规则而不做建模请求响应比、时间间隔方差、TTL 方差这三个特征几乎能覆盖大部分异常场景。我早期做规则引擎时就是用这三个统计量筛出了一批可疑主机。3.3 面向机器学习的聚合特征机器学习模型需要更多“上下文”信息。我通常会拿一个滑动窗口来聚合窗口长度可以是 30 秒、1 分钟或 5 分钟根据被保护网络的性质调整每秒包数PPS窗口内 ICMP 包数量除以窗口时长用于洪泛检测。每秒字节数BPS同样用于洪泛场景尤其是大 payload 洪泛。目的 IP 熵在窗口内统计源 IP 访问了多少个不同目的 IP。目的 IP 熵值高说明扫描行为可能性大。源 IP 熵如果同一个目的 IP 收到了大量来自不同 IP 的 Echo Request需要考虑分布式扫描或反射放大。新出现目的 IP 数量一个源 IP 在过去窗口内从未访问过、但在当前窗口内集中出现的目的 IP 数量这个指标对扫描检测非常灵敏。载荷长度分位数窗口内所有包的载荷长度分布看是否有集中的异常大包。请求时间间隔的周期强度用自相关或者简单统计判断请求是否呈现极强的周期性。这些聚合特征做出来之后可以直接输入到随机森林、XGBoost 或孤立森林这类模型里。它们把单包信息升维成了“行为画像”模型学到的不是“某个包长得像攻击”而是“某台主机的 ICMP 行为模式偏离了本该有的样子”。4. 从 pcap 到训练集特征工程与建模实战流程上通常分四步抓包、提取字段、构造特征、打标签和建模。每一步都有容易踩坑的细节。4.1 用 tshark 提取原始字段我一般不在 Python 里直接解析 pcap而是先用 tshark 把字段导出成 CSV再用 pandas 做后续处理。原因是 tshark 对 ICMP 各种子类型的解析已经非常成熟自己解析容易遗漏边界情况。tshark -r icmp_capture.pcap -Y icmp -T fields \ -e frame.time_epoch -e ip.src -e ip.dst \ -e ip.ttl -e icmp.type -e icmp.code \ -e icmp.seq -e icmp.ident -e icmp.checksum \ -e frame.len -e data.len \ -E headery -E separator, icmp_raw.csv-Y icmp 是显示过滤器只需要 ICMP 包。frame.time_epoch是 Unix 时间戳这个字段要做时间序列分析必须得有。data.len表示载荷长度某些畸形包可能没有这个字段导出来会是空值后面的填充要处理好。4.2 构造单包与聚合特征拿到原始 CSV 后先做列名清洗和类型转换。tshark 导出的字段都是字符串必须转成数值否则 pandas 的聚合计算结果会和你预期差很远。import pandas as pd import numpy as np df pd.read_csv(icmp_raw.csv) df df.dropna(subset[icmp.type]) df[ts] pd.to_numeric(df[frame.time_epoch], errorscoerce) df[seq] pd.to_numeric(df[icmp.seq], errorscoerce).fillna(0) df[ident] pd.to_numeric(df[icmp.ident], errorscoerce).fillna(0) df[pkt_len] pd.to_numeric(df[frame.len], errorscoerce).fillna(0) df[payload_len] pd.to_numeric(df[data.len], errorscoerce).fillna(0) df[ttl] pd.to_numeric(df[ip.ttl], errorscoerce).fillna(0) df[icmp_type] df[icmp.type].astype(int) df[icmp_code] df[icmp.code].astype(int) df[is_echo_req] (df[icmp_type] 8).astype(int) df[is_echo_reply] (df[icmp_type] 0).astype(int)接着按“源 IP 目的 IP 类型 分钟窗口”分组算统计特征。这个聚合粒度可以保证同一对主机的 ping 行为在时间轴上连续而不是被拆散到不同窗口里。df[minute] (df[ts] / 60).astype(int) stat df.groupby([ip.src, ip.dst, minute, icmp_type]).agg( pkt_num(pkt_len, count), pkt_len_mean(pkt_len, mean), pkt_len_std(pkt_len, std), ttl_mean(ttl, mean), ttl_std(ttl, std), payload_len_max(payload_len, max), payload_len_mean(payload_len, mean), seq_min(seq, min), seq_max(seq, max), req_cnt(is_echo_req, sum), reply_cnt(is_echo_reply, sum), ).reset_index()然后再加上窗口内目的 IP 的多样性指标。这一步可以在生成 session 的分组后做也可以直接用数据集统计。dest_diversity df.groupby([ip.src, minute])[ip.dst].nunique().reset_index() dest_diversity.columns [ip.src, minute, dest_ip_num] stat stat.merge(dest_diversity, on[ip.src, minute], howleft)特征构造完成之后要检查是否有全空列和极端异常值。ICMP 流量特征中经常出现大数值突刺比如某一次大规模扫描瞬间把 count 冲到几千如果不处理会让模型学习到错误权重。我一般会对数值特征做分位数截断把 99.9 分位以上的值压缩掉保证模型训练的稳定性。4.3 打标签与模型选择的经验打标签这件事没有标准答案取决于你的数据来源。我常用的方式是分三类完全干净的基线流量在受控测试环境里用正常 ping、mtr、traceroute 生成一段流量标注为正常样本。攻击模拟流量用扫描器、洪泛工具、畸形包生成器等构造异常样本标注为异常。这样做的优点是标签可靠缺点是可能缺少现实中复杂的对抗手法。真实流量里的半自动标注先跑一遍基于规则的检测把规则命中的部分作为候选异常再人工审查确认。这种方式适合积累真实样本但要注意规则本身的误报会污染标签。模型选型上我的经验是表格特征用 XGBoost 或 LightGBM 通常比深度模型更稳。ICMP 特征维度不高样本量相对有限树模型不容易过拟合而且特征重要性可以直接作为后续规则优化的参考。如果是无标签数据可以先用孤立森林Isolation Forest做一轮初筛把离群样本挑出来人工分析再决定是否进入监督学习流程。训练时需要特别注意类别不平衡。异常样本在真实流量中占比通常很低。如果直接用原始比例训练模型会学成“永远预测正常”的形态准确率很高但没有任何检测价值。建议在训练时给异常类设置更高的class_weight或者在预处理阶段对少数类做采样增强评估指标用召回率和 F1 而不是准确率。5. 典型攻击场景的检测方法与我踩过的误报坑特征和模型最终要落到具体场景里。这里分享一下我在 Ping 扫描、洪泛、畸形包和隐蔽通信四类场景里的检测思路以及被网络设备“坑”过的几次教训。5.1 Ping 扫描和 Ping 洪泛的行为判定Ping 扫描的典型模式是同一个源 IP 短时间内向多个不同目的 IP 发送 Echo Request。检测时不一定需要看每个包更重要的是看窗口内目的 IP 的多样性。如果一个常规办公网段里某台主机过去一小时只访问过两三个固定地址现在突然在三十秒内访问了几十个网段的不同地址那就要提高告警级别。具体阈值要看环境规模小网络里 20 个目的 IP 已经很多大型网段里可能需要 100 个以上。我建议先统计两周正常基线的“窗口内新目的 IP 数量”用均值加三倍标准差做为告警阈值。Ping 洪泛看的是单速率维度。单个源 IP 触发的 ICMP 包速率如果超过基线均值很多比如从每秒几包跳到每秒几千包基本可以确认异常。洪泛场景里源 IP 可能是伪造的直接封禁 IP 并不可靠建议联动边界设备做限速或者针对目的 IP 做黑洞路由。5.2 Smurf 放大与畸形报文识别Smurf 攻击的基本方式是把源地址伪造成受害主机把 Echo Request 发往广播地址导致大量设备同时向受害主机发送 Echo Reply形成放大效果。虽然现在很多路由器默认关闭了广播转发但在部分遗留网络中仍然可能见效。检测 Smurf 的关键不是 Echo Request 本身而是 Echo Reply 的目标地址集中度。如果一段时间内大量 Echo Reply 都发往同一个目的 IP而这个目的 IP 自身并没有对应的请求记录就需要立刻检查是不是发生了反射放大。可以计算成功响应数与请求数的比值正常 ping 在这个窗口内接近 1Smurf 攻击场景下会异常高企。畸形报文检测相对简单但非常重要。Checksum 错误、Type 和 Code 组合不存在、报文长度小于 ICMP 最小头部长度等都属于异常畸形报文。这些包通常不会来自正常操作系统而是出于扫描、探测或破坏目的由自定义工具生成。早期我在规则引擎里加了一条“icmp.checksum_status 1”的检测规则一上线就抓到了几个内网扫描器它们发出的探测报文的 checksum 计算方式和常规实现有差异暴露了攻击工具的痕迹。5.3 隐蔽通信类异常的特征线索ICMP 隐蔽通信是恶意程序常用的数据外传手法本质上就是恶意程序把指令或数据放进 ICMP Payload伪装成正常 ping 流量。这类异常很难靠单包规则抓全原因在于攻击者可以把 payload 做的极短、极像正常填充内容。所以我会重点看以下几组特征载荷熵值异常高正常 ping 的 payload 是打印字符串熵一般不会太高。如果检测到一个会话的 payload 熵值持续性偏高说明里面装的不是填充文本而是加密或编码数据。载荷长度整齐划一正常 ping 虽然载荷长度固定但不同程序的默认长度有差异恶意程序往往为了便于拆包重组会把每段数据切成同样的长度看起来比正常流量还要“整齐”。时间节律极度稳定恶意程序的心跳通常每隔固定时间发送一次间隔方差极小。正常人类手工 ping 反而会有波动运维脚本虽然也稳定但往往会关联着固定的运维窗口。请求与响应成对出现且长度吻合如果内部主机和外部节点之间出现了一来一回、长度几乎一致的 ICMP Echo 交互持续长时间不断就很值得怀疑。我碰到的实际案例里攻击者还刻意把 ICMP 载荷伪装成字母数字串看起来和 ping 工具默认填充很像。当时是依靠“请求响应时间间隔标准差过低”这个特征暴露的因为那台机器的心跳间隔精准到几十毫秒几乎没有偏差。5.4 误报处理经验从 TTL 过期到 traceroute除了恶意行为ICMP 检测最常遇到的误报来源是网络设备和运维工具正常发出的探测包。traceroute 是一个典型例子。它会主动构造 TTL 从 1 开始递增的探测包沿途路由器返回 Time Exceeded 消息同时目标主机会返回 Destination Unreachable 或 Echo Reply。如果只看“大量 ICMP Time Exceeded”就告警网络运维做一次路径验证就能把告警淹没。排除的关键是识别 TTL 是否从 1 开始逐包递增以及这类包是否和目标地址、目的端口固定对应。检测规则里我会把“TTL 从 1 递增”的模式单独跳过或者把运维网管服务器的地址加入白名单。路径 MTU 探测也会产生大量 Type 3 Code 4 的报文。这个类型代表“需要分片但设置了 DF 标志”是正常网络路径调优的一部分。但如果是同一条路径上反复出现、且请求方不固定也可能是有人在故意探测网络边界。我通常会对这个类型的报文单独做监控但不直接告警而是记录数量变化趋势在趋势明显上升时才提示排查。还有一类误报来自监控软件。有些监控平台用 ICMP 做主机存活检测每三十秒 ping 一次所有被监控主机从数据上看就像一台机器在稳定、高频率地向所有资产发 Echo Request。如果模型只看目的 IP 多样性很可能会把它识别为扫描。解决办法是把监控平台的源 IP 加入白名单或者在特征里增加“目的 IP 范围是否与被监控资产列表一致”这个上下文字段。5.5 一个小习惯先建立基线再做异常判断刚部署检测系统时最忌讳直接拿固定阈值去套所有网络。不同的网络环境ICMP 流量画像差异非常大。生产网一段安静的环境里每天可能只有几十个 ICMP 包办公室网络上有人打游戏、做链路测速频率就会高很多数据中心有监控系统、路由协议探活ICMP 包量又完全是另一个量级。我的做法是部署后的前两周只采集、只统计不告警。把每个源 IP 在滑动窗口内的包量均值、方差、新目的 IP 数量、载荷长度分布都记录下来形成基线。之后每天的检测都相对基线做偏差判断而不是对着一张写死的规则表硬碰。这个思路在机器学习模型上也适用模型每过一段时间要重新训练因为网络变化太快旧数据学出的“正常”很快就不再正常。ICMP 这种轻量协议检测价值不在于单包有多奇特而在于时序上的变化、分布上的偏移。凡是稳定到刻板、规律到诡异的重复都值得你多看一眼。它在攻击链里往往不是最后一步但经常是第一声敲门。把这声敲门接住后面很多麻烦都可以避免。