ICMP异常检测实战:从特征工程到机器学习识别网络威胁
2026/9/24 18:57:32 网站建设 项目流程

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 头部由 Type(8 位)、Code(8 位)、Checksum(16 位)、Identifier(16 位)、Sequence Number(16 位)构成,后面跟着载荷数据。抓包的时候,我们看到的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 到多个目的 IP,TTL 的方差如果很大,说明目标可能分布在不同网络层级,扫描特征会比较明显。
  • 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 header=y -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"], errors="coerce") df["seq"] = pd.to_numeric(df["icmp.seq"], errors="coerce").fillna(0) df["ident"] = pd.to_numeric(df["icmp.ident"], errors="coerce").fillna(0) df["pkt_len"] = pd.to_numeric(df["frame.len"], errors="coerce").fillna(0) df["payload_len"] = pd.to_numeric(df["data.len"], errors="coerce").fillna(0) df["ttl"] = pd.to_numeric(df["ip.ttl"], errors="coerce").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"], how="left")

特征构造完成之后,要检查是否有全空列和极端异常值。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 在这个窗口内接近 1,Smurf 攻击场景下会异常高企。

畸形报文检测相对简单但非常重要。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 这种轻量协议,检测价值不在于单包有多奇特,而在于时序上的变化、分布上的偏移。凡是稳定到刻板、规律到诡异的重复,都值得你多看一眼。它在攻击链里往往不是最后一步,但经常是第一声敲门。把这声敲门接住,后面很多麻烦都可以避免。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询