2. 异常流量特征工程:流量特征提取方法论

聊到异常流量分析,很多人一上来就扔模型、调参数。我个人的习惯是——先别急,把特征工程做扎实了再说。你想想看,特征都没提好,模型再牛也是白搭。说白了,特征工程就是给流量数据“画像”,画得像不像,直接决定了你能不能抓到那个“鬼”。

这一节,我带你走一遍流量特征提取的完整方法论。我们从四个维度切入:时间序列、统计特征、载荷特征,以及它们背后的提取逻辑。嗯,这里要注意,每个维度都有坑,我会把我在项目中踩过的雷一并告诉你。

2.1 流量特征提取方法论

特征提取不是拍脑袋。我一般遵循三步走:

  1. 原始数据清洗:去重、去噪、补全时间戳。我曾经遇到过一个案例,因为PCAP包里的时间戳精度不一致,导致后续所有时序特征全偏了。
  2. 会话切分:按五元组(源IP、目的IP、源端口、目的端口、协议)切分成流。这一步很关键,切错了后面全白干。
  3. 特征计算:对每条流,计算我们下面要讲的各类特征。
我的小技巧: 切分会话时,建议设置一个空闲超时时间(比如60秒)。超过这个时间没新包,就认为会话结束了。这个值我一般根据业务场景调,别死用默认值。

2.2 时间序列特征

时间序列特征,说白了就是看流量在时间轴上的“节奏感”。正常流量和攻击流量,节奏完全不一样。

2.2.1 包间隔(Inter-arrival Time)

包间隔就是两个连续数据包到达的时间差。这个特征对检测慢速扫描、DDoS非常敏感。

  • 正常流量:包间隔通常比较随机,有长有短,符合某种分布。
  • 攻击流量:比如慢速HTTP攻击,包间隔会非常均匀,像机器打点一样。我见过一个案例,攻击者故意把间隔设在10秒整,结果一画图,跟心电图似的,一眼就暴露了。

计算方式很简单:

# 伪代码示例
packet_timestamps = [t1, t2, t3, ...]
inter_arrival_times = [t2-t1, t3-t2, ...]
# 然后可以计算均值、方差、最大值、最小值等

2.2.2 流持续时间(Flow Duration)

一条流从第一个包到最后一个包的时间差。这个特征对区分“快闪”型攻击和“持久”型攻击很有用。

  • 短流:比如DNS查询,通常几十毫秒就结束了。
  • 长流:比如大文件下载,可能持续几分钟甚至几小时。
  • 异常流:比如慢速攻击,流持续时间会异常长,但包数量却很少。我曾经抓到一个C2心跳流,持续了整整3天,每30秒发一个空包,流持续时间长得离谱。
重点: 流持续时间要和包数量、包间隔一起看。单看一个特征容易误判。比如一个长流可能是正常的视频流,但加上“包间隔均匀”这个特征,就值得怀疑了。

2.3 统计特征

统计特征,就是给流量数据做“体检”。均值、方差、熵,这三个是基本功。

2.3.1 均值与方差

均值和方差,大家都很熟了。但在流量分析里,它们有特殊的用法。

  • 均值:比如包长度的均值。正常HTTP请求的包长度均值通常在几百字节到几KB之间。如果突然变成几十字节,可能是扫描行为。
  • 方差:反映波动程度。正常用户访问网页,包长度方差很大(有请求、有响应、有图片)。而某些攻击流量,比如ICMP Flood,所有包长度几乎一样,方差接近0。

我举个例子:

# 计算包长度的均值和方差
import numpy as np

packet_lengths = [64, 64, 64, 64, 64]  # 攻击流量
mean_len = np.mean(packet_lengths)     # 64.0
var_len = np.var(packet_lengths)       # 0.0

normal_lengths = [64, 1500, 200, 512, 1024]  # 正常流量
mean_normal = np.mean(normal_lengths)   # 660.0
var_normal = np.var(normal_lengths)     # 很大

看到了吧?方差为0,基本可以断定是机器生成的流量。

2.3.2 熵(Entropy)

熵这个概念,我第一次接触时也觉得有点抽象。说白了,熵就是衡量“混乱程度”的指标。

  • 高熵:数据分布均匀,比如正常流量中,源IP、目的端口分布很散。
  • 低熵:数据集中,比如DDoS攻击中,所有流量都打向同一个IP和端口。

计算方式:

from collections import Counter
import math

def calculate_entropy(data):
    counter = Counter(data)
    total = len(data)
    entropy = 0
    for count in counter.values():
        p = count / total
        entropy -= p * math.log2(p)
    return entropy

# 例子:目的端口熵
ports_normal = [80, 443, 53, 80, 443, 8080, 22]  # 高熵
ports_attack = [80, 80, 80, 80, 80, 80, 80]       # 低熵(0)
避坑指南: 我曾经在分析一个反射放大攻击时,只看了目的IP的熵,发现熵值很高,就以为是正常流量。后来才发现,攻击者用了大量伪造源IP,导致源IP熵也很高。所以,熵要结合多个维度看,比如同时看源IP熵和目的端口熵。

2.4 载荷特征(Payload)

载荷特征,就是看数据包“肚子里装了什么”。这是最细粒度的分析,也是最能发现隐蔽攻击的地方。

2.4.1 Payload长度分布

不同协议的Payload长度有固定模式。比如:

  • DNS查询:通常几十字节。
  • HTTP GET请求:几百字节到几KB。
  • 文件上传:可能几MB。

异常情况:比如一个TCP SYN包,正常Payload长度为0。如果发现SYN包带了大量Payload,那基本可以断定是恶意构造的。

我习惯把Payload长度分布画成直方图。正常流量通常是多峰分布(不同协议混合),而攻击流量往往是单峰分布(所有包长度一样)。

2.4.2 字符频率

这个特征对检测SQL注入、XSS、命令执行等攻击特别有效。为什么?因为攻击载荷里通常包含大量特殊字符。

举个例子:

正常请求: "GET /index.html HTTP/1.1"
SQL注入: "GET /search?q=1' OR '1'='1 HTTP/1.1"

你看,正常请求里字母和数字占绝大多数。而SQL注入里,单引号、等号、空格的比例明显偏高。

计算字符频率的方法:

def char_frequency(payload):
    freq = {}
    for char in payload:
        if char in freq:
            freq[char] += 1
        else:
            freq[char] = 1
    # 可以进一步计算特殊字符占比
    special_chars = set("'\"=--%20<>()")
    special_count = sum(freq.get(c, 0) for c in special_chars)
    return special_count / len(payload) if payload else 0
我的经验: 字符频率特征最好结合n-gram(比如2-gram)一起用。单看字符频率可能会漏掉一些编码后的攻击。我曾经遇到一个Base64编码的恶意Payload,单字符频率看起来很正常,但2-gram(连续两个字符的组合)就暴露了异常。

2.5 特征组合与实战建议

单个特征往往不够用。我一般会组合使用:

攻击类型 推荐特征组合 说明
DDoS 包间隔方差 + 目的端口熵 + 流持续时间 包间隔方差小,目的端口熵低,流持续时间短
SQL注入 字符频率 + Payload长度 + 特殊字符占比 特殊字符占比明显偏高
慢速攻击 包间隔均值 + 流持续时间 + 包数量 包间隔大且均匀,流持续时间长,包数量少
端口扫描 目的端口熵 + 包间隔 + 流持续时间 目的端口熵高(扫描多个端口),包间隔均匀

最后说一句:特征工程没有银弹。每个网络环境都不一样,你需要根据实际流量反复调优。我刚开始做的时候,也经常被各种假阳性搞得头大。但只要你把上面这些特征吃透了,再配合一些业务理解,基本能覆盖90%以上的异常场景。

下一节,我们会把这些特征串起来,讲怎么用机器学习模型做自动化检测。到时候你会发现,特征工程做得好,模型训练就是水到渠成的事。