## 1. 概述
TCP Stack Fingerprinting 是通过分析 TCP/IP 协议栈实现差异来远程识别目标主机操作系统的技术。
RFC 标准在很多细节上只给出建议值或留有实现自由度,各 OS 内核做出了不同选择,这些差异构成了\"指纹\"。
指纹特征可以分为两个层面:
- **显性特征 (How TCP Writes)** — 直接读取包头字段即可采集:TTL、Window Size、TCP Options 排列顺序等。就像看一个人的笔迹。
- **隐性特征 (How TCP Reads)** — 需要发送特定刺激流量后,从对端的应答模式、时序、状态变迁等侧面反应中推断。就像观察一个人怎么理解和回应你说的话。
隐性特征采集难度远高于显性特征,但区分度也更高,且更难伪装。
---
## 2. 显性特征 — How TCP Writes
这些特征直接存在于包头中,被动抓包即可采集。
### 2.1 IP 层
| 特征 | 说明 | OS 差异 |
|------|------|---------|
| **TTL** | 初始生存时间 | Linux/macOS=64, Windows=128, Solaris/Cisco=255 |
| **IP ID** | 分片标识生成策略 | 递增(Windows) / 随机(现代Linux) / 全零(Linux+DF) |
| **DF 位** | Don't Fragment | 大多数现代 OS 默认 DF=1 |
| **TOS/DSCP** | 服务类型 | 通常为 0,SSH 等场景可能不同 |
### 2.2 TCP 层
| 特征 | 说明 | OS 差异 |
|------|------|---------|
| **初始窗口** | SYN 中的 Window 字段 | 各版本差异较大 |
| **Window Scale** | 窗口缩放因子 | Linux=7, Windows=8, macOS=6 |
| **MSS** | 最大报文段大小 | 以太网通常 1460,VPN/隧道更小 |
| **Options 排列** | SYN 中选项的顺序 | 每个 OS 排列方式不同,最核心的显性指纹 |
| **Timestamp** | TSval 递增频率 | Linux~1000Hz, Windows 不携带, OpenBSD 随机化 |
**各 OS 的 SYN Options 签名:**
```
Linux: MSS → SACK → TS → NOP → WS
Windows: MSS → NOP → WS → NOP → NOP → SACK (无 Timestamp)
macOS: MSS → NOP → WS → NOP → NOP → TS → SACK → EOL
FreeBSD: MSS → NOP → WS → SACK → TS
OpenBSD: MSS → NOP → NOP → SACK → NOP → WS → NOP → NOP → TS
```
这些特征的共同点:**一次抓包即可读取,无需交互**。对应的伪装也相对容易——改包头字段即可。
---
## 3. 隐性特征 — How TCP Reads
这些特征反映的是 TCP 栈**接收、处理、响应**数据的内部行为逻辑。不能从单个包中直接读取,需要通过构造刺激流量并观察反应模式来推断。
### 3.1 ACK 生成策略
TCP 收到数据后如何生成 ACK,各 OS 差异显著。
#### Delayed ACK 定时器
RFC 建议每收到 2 个满段数据或等待最多 200ms 后发送一个 ACK。但实际实现差异很大:
| OS | Delayed ACK 超时 | 触发条件 |
|----|-----------------|----------|
| Linux | **40ms**(动态,基于 HZ) | 每 2 个满段或定时器到期 |
| Windows | **200ms** | 每 2 个满段或定时器到期 |
| macOS | **100ms** | 每 2 个满段或定时器到期 |
| FreeBSD | **100ms** | 每 2 个满段或定时器到期 |
> 采集方法:以精确间隔向目标发送单个数据段(确保不触发每 2 段规则),测量 ACK 返回延迟。Linux 的 40ms 与 Windows 的 200ms 差异极为明显。
#### ACK 聚合行为 (Stretch ACK)
当高速接收大量数据时,部分 OS 会违反\"每 2 段 ACK 一次\"的约定,将多个段合并为一个 ACK:
- **Linux 4.x+**: 在高吞吐路径下可能每 4-8 个段才 ACK 一次 (GRO/LRO 影响)
- **Windows**: 严格遵守每 2 段 ACK
- **macOS**: 与 FreeBSD 类似,有轻微聚合
> 这个特征在高带宽连接中尤其明显。
#### Quick ACK 模式
Linux 在连接建立初期或检测到乱序时进入 Quick ACK 模式,暂时关闭 Delayed ACK,对每个包立即回复 ACK。其他 OS 没有这个机制或实现方式不同。
- **Linux**: 连接前 ~3-16 个包使用 Quick ACK,之后切换为 Delayed ACK
- **Windows**: 无类似机制,从第一个包开始就是 Delayed ACK 策略
- **FreeBSD**: 有类似机制但触发条件不同
> 采集方法:在连接刚建立时快速发送多个小包,观察前 N 个 ACK 的延迟模式。Linux 的前几个 ACK 延迟接近 0,然后突变为 ~40ms。
### 3.2 接收窗口管理
#### 窗口增长曲线
从初始窗口到稳态窗口的增长轨迹,不同 OS 的 buffer auto-tuning 策略不同:
- **Linux**: 基于 `tcp_rmem` (min, default, max) 三元组,自动根据 RTT 和吞吐量动态扩展接收缓冲区。增长较激进,通常在几个 RTT 内快速拉升。
- **Windows**: 接收窗口自动调优 (Receive Window Auto-Tuning),增长曲线更保守,阶梯式增长。
- **macOS**: 类似 FreeBSD,增长速度介于 Linux 和 Windows 之间。
> 采集方法:持续向目标发送数据,记录每个 ACK 中通告的窗口值,绘制窗口增长曲线。
#### 窗口更新策略 (Window Update Heuristic)
当接收缓冲区被读取后,OS 需要决定何时发送窗口更新通告:
- **Linux**: 当可用空间增长超过前一次通告窗口的某个比例(通常 >50%)时发送更新
- **Windows**: 更频繁地发送窗口更新
- **FreeBSD**: 类似 Linux 但阈值不同
#### Silly Window Syndrome (SWS) 防御
接收端的 SWS 防御决定了何时通告小窗口:
- **Linux**: 严格遵守 Clark 算法——除非可用空间 >= min(MSS, buffer/2),否则通告窗口为 0
- **Windows**: 实现略有不同,阈值和恢复时机存在差异
#### Zero Window 与 Window Probe 响应
当接收缓冲区满时:
- 各 OS 通告零窗口后,对发送端 Window Probe 的响应时机不同
- Linux 在缓冲区释放后几乎立即发送窗口更新
- Windows 可能延迟更久才发送
### 3.3 乱序处理与 SACK 行为
#### 乱序容忍度 (Reordering Tolerance)
收到乱序包后发送 Duplicate ACK 之前的等待行为:
- **Linux 4.x+**: 有 `tcp_reordering` 参数(默认 3),会动态调整对乱序的容忍度。收到乱序包后不会立即认为是丢包,而是等待一段时间。内核维护一个 reordering 估计值(rack_reo_wnd)。
- **Windows**: 乱序容忍度较低,更快触发 Duplicate ACK
- **FreeBSD**: 类似早期 Linux,固定阈值
> 采集方法:向目标发送故意乱序的数据段(如发送 seq 1,3,2),观察 Duplicate ACK 的数量和时机。
#### SACK 块生成
当启用 SACK 时,对乱序段的 SACK 报告方式存在差异:
| 特征 | Linux | Windows | macOS |
|------|-------|---------|-------|
| 最大 SACK 块数 | 4 (有 TS 时 3) | 4 | 4 |
| SACK 块排序 | 最近收到的在前 | 最近收到的在前 | 最近收到的在前 |
| D-SACK 支持 | 支持 (默认开启) | 支持 | 支持 |
| D-SACK 触发 | 收到重复数据时 | 收到重复数据时 | 行为略有不同 |
| SACK 块合并 | 积极合并相邻块 | 合并策略不同 | 类似 FreeBSD |
> D-SACK (Duplicate SACK, RFC 2883) 的支持和行为差异是一个很好的隐性指纹。发送一个已经被确认过的数据段,观察对方是否返回 D-SACK 以及格式。
#### 重叠段处理 (Overlapping Segment Reassembly)
当收到的 TCP 段与已有数据重叠时,是保留旧数据还是用新数据覆盖:
- **Linux**: 倾向于**保留旧数据**(first-come policy)
- **Windows**: 倾向于**用新数据覆盖**(last-come policy)
- **macOS/FreeBSD**: 保留旧数据
> 这个差异在 IDS 规避中被广泛利用(Ptacek & Newsham 1998),也是区分 OS 的有效手段。
### 3.4 拥塞控制的接收侧可观测行为
虽然拥塞控制运行在发送端,但接收端的行为间接影响发送端的拥塞判断,并且部分拥塞相关行为在接收端也有差异。
#### ECN 处理
ECN (Explicit Congestion Notification) 的支持和行为:
| OS | ECN 协商 | 收到 CE 标记的响应 |
|----|---------|-------------------|
| Linux | SYN 中默认不设置 ECE/CWR(可通过 sysctl 开启) | 在 ACK 中设置 ECE 标志,直到收到 CWR |
| Windows 10+ | 默认不协商 ECN | 行为类似 Linux |
| macOS | 默认协商 ECN | 接收处理中持续回复 ECE |
| FreeBSD | 可配置 | 类似 Linux |
> macOS 默认开启 ECN 协商本身就是一个区分特征。
#### 对拥塞信号的 ACK 时序
发送端进入拥塞后减速,接收端的 ACK 时序(inter-ACK gap)反映了发送端的拥塞控制行为,可以间接推断发送端的拥塞算法:
- **CUBIC**: 丢包后窗口降为原来的 0.7 倍,恢复曲线呈三次函数形
- **BBR**: 不依赖丢包,基于带宽估计,ACK 间隔更均匀
- **NewReno**: 丢包后窗口减半,恢复更慢
### 3.5 TCP 状态机边缘行为
#### TIME_WAIT 持续时间
连接关闭后 TIME_WAIT 状态的持续时间:
| OS | TIME_WAIT 持续 | 可配置性 |
|----|---------------|----------|
| Linux | 60 秒 (硬编码 `TCP_TIMEWAIT_LEN`) | 不可直接调整(除非改内核) |
| Windows | 120 秒 | 注册表可改 `TcpTimedWaitDelay` |
| macOS | 15 秒 | sysctl `net.inet.tcp.msl` |
| FreeBSD | 60 秒 | sysctl 可调 |
> 采集方法:与目标建立连接后主动关闭,然后尝试复用相同四元组重新连接,测量被拒绝(RST)的时间窗口。
#### FIN_WAIT / CLOSE_WAIT 行为
- Linux 有 `tcp_fin_timeout`(默认 60 秒)控制 FIN_WAIT_2 超时
- Windows 类似但超时值不同
- 一些 OS 在 FIN_WAIT_1 超时后直接发 RST,另一些静默丢弃
#### Simultaneous Close(双方同时关闭)
RFC 793 定义了 simultaneous close 路径,但实现差异很大:
- **Linux**: 正确处理 simultaneous close,进入 CLOSING 状态
- **Windows**: 早期版本有 bug,可能发送 RST
- 通过构造 simultaneous close 场景观察行为差异
#### RST 生成细节
不同 OS 在何种条件下生成 RST,以及 RST 包本身的特征:
| 特征 | Linux | Windows |
|------|-------|---------|
| RST 窗口字段 | 0 | 非零(当前窗口值) |
| RST 序列号 | 对方期望的 ACK 号 | 可能使用当前 SEQ |
| 带 ACK 的 RST | 某些场景会 RST+ACK | 更频繁使用 RST+ACK |
| RST rate limiting | 有限速机制 | 无明显限速 |
### 3.6 重传行为
这是隐性特征中信息量最大的一类。
#### 初始 RTO 与退避策略
| OS | 初始 RTO | SYN 重传 | 数据重传上限 |
|----|---------|---------|-------------|
| Linux | 1s | 1, 2, 4, 8, 16, 32s(最多 6 次) | 15 次 |
| Windows (新) | 1s | 1, 2 秒(最多 2 次) | 5 次 |
| Windows (旧) | 3s | 3, 6, 12 秒 | 5 次 |
| macOS | 1s | 1, 2, 4, 8, 16, 32 秒 | 12 次 |
| FreeBSD | 1s | 类似 Linux | 12 次 |
> Windows SYN 只重传 2 次就放弃的行为是一个非常显著的特征。
#### 快速重传 / 快速恢复
- **Linux**: 收到 3 个 Duplicate ACK 后进入快速重传 + SACK-based recovery
- **Windows**: 类似但恢复算法有差异
- **Linux RACK**: 基于时间而非计数判断丢包(4.x+ 默认)
- **Linux TLP (Tail Loss Probe)**: 在 RTO 到期前发送尾部探测包,减少尾部延迟
> TLP 是 Linux 独有的特征。采集方法:观察在应该 RTO 超时之前是否有探测性重传。
#### 伪重传检测 (Spurious Retransmission Detection)
- **Linux**: 支持 F-RTO (Forward RTO Recovery),RTO 超时后不立即减窗,先探测是否为伪超时
- **Windows**: 有类似机制但触发条件不同
- **Linux**: 还支持 Eifel Detection (基于 Timestamp)
### 3.7 流量控制的微观行为
#### Nagle 算法与交互行为
各 OS 的 Nagle 算法实现虽然目标一致(避免小包),但边界条件不同:
- **Linux**: 有 `TCP_CORK` 选项,可以更精细地控制合并
- **Windows**: 有 `SIO_TCP_SET_ACK_FREQUENCY` 可调 ACK 频率
- Nagle + Delayed ACK 的交互延迟在不同 OS 上表现不同(著名的 Nagle-delayed-ACK 问题)
#### PSH (Push) 标志的使用
- **Linux**: 在发送缓冲区中最后一个段上设置 PSH
- **Windows**: 几乎每个段都设置 PSH
- **macOS**: 类似 Linux
> 这是一个容易被忽略但区分度很高的特征。
#### Keep-Alive 行为
| OS | 默认间隔 | 探测次数 | 探测间隔 |
|----|---------|---------|---------|
| Linux | 7200s (2h) | 9 次 | 75s |
| Windows | 7200s (2h) | 10 次 | 1s |
| macOS | 7200s (2h) | 8 次 | 75s |
> Windows 的 Keep-Alive 探测间隔只有 1 秒,远小于其他 OS 的 75 秒,非常显眼。
### 3.8 TCP 时序指纹
#### Timestamp 时钟频率
通过多个包的 TSval 差值与实际时间差的比值推断时钟频率:
| OS | TSval 频率 | 特点 |
|----|-----------|------|
| Linux 2.6+ | 1000 Hz (HZ=1000) | 精确递增,可推算 uptime |
| Linux (旧) | 100-250 Hz | 取决于内核编译 HZ |
| macOS | ~1000 Hz | 类似 Linux |
| FreeBSD | ~1000 Hz | 类似 Linux |
| OpenBSD | 随机化 | 故意防指纹 |
| Windows | N/A | 不携带 Timestamp |
#### 时钟偏移 (Clock Skew)
通过长期观测 TSval 增长与真实时间的微小漂移,可以:
- 唯一标识同一 NAT 后的不同主机(即使 IP 相同)
- 区分虚拟机 vs 物理机(虚拟机时钟漂移更大)
- 检测代理——代理前后的 Timestamp 不连续
#### 包间时序 (Inter-Packet Timing)
- **中断合并 (Interrupt Coalescing)**: 不同网卡驱动和 OS 的中断合并策略导致微秒级时序差异
- **调度器影响**: 不同 OS 的任务调度器对网络栈处理的优先级不同,反映在应答延迟的抖动分布上
- **Idle 后的首包延迟**: 连接空闲一段时间后发送新数据时的处理延迟各不相同
---
## 4. 隐性特征的采集方法论
### 4.1 主动探测 (Active Probing)
向目标构造特定模式的流量,观察响应:
| 探测类型 | 目的 | 方法 |
|---------|------|------|
| **单段延迟** | 测 Delayed ACK 定时器 | 发送单段数据,测 ACK 延迟 |
| **双段间隔** | 测 Quick ACK vs Delayed ACK | 连续发 2 段,观察 ACK 行为 |
| **乱序注入** | 测乱序容忍度和 SACK | 发送 seq 1,3,2,观察 DupACK 和 SACK 块 |
| **重复段** | 测 D-SACK | 重发已确认数据,检查是否有 D-SACK |
| **重叠段** | 测重叠段处理 | 发送重叠数据段,比较最终接收到的内容 |
| **突发灌入** | 测窗口增长曲线 | 高速发送大量数据,跟踪窗口通告变化 |
| **连接关闭计时** | 测 TIME_WAIT | 关闭后尝试复用四元组 |
| **零窗口** | 测窗口探测响应 | 灌满接收缓冲,观察零窗口后的恢复 |
| **ECN 协商** | 测 ECN 支持 | SYN 中设置 ECE+CWR,观察 SYN-ACK |
| **丢包模拟** | 测重传策略 | 静默丢弃部分包,观察重传间隔序列 |
### 4.2 被动推断 (Passive Inference)
在代理场景下,仅通过观察客户端正常流量推断部分隐性特征:
| 可观测量 | 推断内容 |
|---------|---------|
| 客户端 ACK 的 inter-arrival 时间 | Delayed ACK 定时器 |
| 窗口通告的变化梯度 | Buffer auto-tuning 策略 |
| SACK 块的出现频率和格式 | SACK 实现差异 |
| PSH 标志的使用频率 | 发送端 OS |
| Keep-Alive 探测间隔 | OS 类型(1s vs 75s) |
| TSval 递增速率 | 时钟频率 |
| 重传间隔序列 | RTO 算法和初始值 |
| Quick ACK 到 Delayed ACK 的切换点 | Linux vs 其他 |
### 4.3 采集难点
| 难点 | 原因 |
|------|------|
| **需要有状态跟踪** | 不是看单个包,而是看包与包之间的关系 |
| **时序精度要求高** | Delayed ACK 差异在毫秒级,需要高精度时钟 |
| **需要足够样本** | 单次观测可能受抖动影响,需要统计分析 |
| **中间设备干扰** | NAT、负载均衡、TCP 加速器可能改变行为 |
| **应用层影响** | 应用的读取模式影响 ACK 和窗口行为 |
| **难以伪装但也难以还原** | 这些行为深嵌内核,既难以在代理中模拟,也难以被检测方精确测量 |
---
## 5. 跨层指纹关联
真正强大的检测不仅看单层,而是检查**多层之间的一致性**。
### 5.1 指纹层次模型
```
Layer 7 应用层 HTTP User-Agent / Accept-Language / Feature API
Layer 6 表示层 TLS ClientHello (JA3/JA4) / ALPN / Certificate
Layer 5 会话层 HTTP/2 SETTINGS / QUIC Transport Parameters
Layer 4 传输层 TCP Options / Window / 重传行为 / ACK 模式
Layer 3 网络层 TTL / IP ID / DF / TOS
Layer 2 链路层 MTU 推断
```
检测方会检查各层指纹是否自洽。例如:
| 矛盾 | 暴露信息 |
|------|---------|
| TCP 指纹 = Linux,User-Agent = Windows Chrome | 代理/模拟器 |
| TTL = 64 (Linux),TLS = Windows 特有密码套件 | 代理 |
| MSS = 1400 (隧道典型值),其他一切正常 | VPN/隧道封装 |
| Timestamp 不连续/突变 | 代理转发,不是直连 |
| HTTP/2 SETTINGS = Chrome,TLS 指纹 = curl | 使用了自定义 TLS 库的爬虫 |
| TCP 行为全部像 Linux,但 ACK 延迟 = 200ms | 用户态代理在 Linux 上伪装 Windows 但没控制 ACK 行为 |
### 5.2 TLS 指纹 (JA3 / JA4)
ClientHello 中的特征:
- **Cipher Suites 列表及顺序**: 不同浏览器/运行时的组合不同
- **Extensions 列表及顺序**: SNI、ALPN、supported_groups 等
- **Supported Groups (椭圆曲线)**: x25519、secp256r1 的优先级
- **Signature Algorithms**: 签名算法偏好
JA4 是 JA3 的改进版,增加了协议类型、SNI 状态等维度。
### 5.3 HTTP/2 指纹 (Akamai Fingerprint)
HTTP/2 连接的 SETTINGS 帧和 WINDOW_UPDATE 帧暴露指纹:
- **SETTINGS 参数及顺序**: HEADER_TABLE_SIZE、MAX_CONCURRENT_STREAMS、INITIAL_WINDOW_SIZE 等
- **WINDOW_UPDATE 初始值**
- **Priority 帧的使用方式**
- 不同浏览器的 HTTP/2 指纹差异非常大,比 TCP 层更容易区分具体应用
---
## 6. 伪装层级与可行性
### 6.1 可控性分层
```
Level 1 — 用户态 setsockopt(你的项目当前位置)
├── TTL / 缓冲区大小
├── 覆盖: 显性特征 ~30%
└── 隐性特征: 几乎无法控制
Level 2 — sysctl 全局参数
├── + tcp_rmem / tcp_wmem / tcp_fin_timeout 等
├── 覆盖: 显性 ~50%, 隐性 ~10%
└── 问题: 全局生效,无法按连接区分
Level 3 — eBPF / TC
├── + 出站包 TCP Options 重排 / Timestamp 覆写 / IP ID 修改
├── + 部分 ACK 行为调整 (通过 BPF_PROG_TYPE_SOCK_OPS)
├── 覆盖: 显性 ~80%, 隐性 ~30%
└── 限制: 只能修改出站,无法改变内核接收逻辑
Level 4 — 用户态协议栈 (如 lwIP / smoltcp)
├── 完全绕过内核 TCP 栈,自行实现所有行为
├── 覆盖: 显性 ~95%, 隐性 ~90%
└── 代价: 性能大幅下降,实现复杂度极高
Level 5 — 内核模块 / 修改内核
├── 直接修改内核 TCP 状态机
├── 覆盖: ~99%
└── 代价: 需要目标内核版本适配,维护成本极高
```
### 6.2 隐性特征的伪装困境
隐性特征之所以难伪装,根本原因在于它们是**内核 TCP 状态机在处理接收数据时的副产物**:
- Delayed ACK 定时器 → 内核 timer 子系统硬编码
- 窗口增长曲线 → 内核 buffer auto-tuning 算法
- SACK 块生成 → 内核 TCP 接收路径 `tcp_data_queue()` 内部逻辑
- 重传策略 → 内核 `tcp_retransmit_timer()` 实现
- Quick ACK → 内核 `tcp_enter_quickack_mode()` 实现
这些行为分散在内核 TCP 栈的各个角落,不暴露任何用户态调整接口。你在用户态只能看到最终结果(通过 socket 读取数据),无法干预内核如何生成 ACK、如何管理接收窗口。
### 6.3 不可伪装的残余
即使做到 Level 5,仍有部分维度可能暴露:
- **微秒级时序**: 代理引入的转发延迟在统计上可检测
- **MTU 不一致**: 隧道封装导致的 MSS 减小
- **跨层矛盾**: TCP 说 Windows 但 ACK 行为像 Linux
- **Timestamp 不连续**: 代理转发导致 TSval 序列中断
- **流量模式**: 代理的连接复用与直连的差异
- **地理/ASN 矛盾**: IP 位置与 OS/浏览器使用习惯不匹配
---
## 7. 工具与参考
| 工具 | 类型 | 关注层面 |
|------|------|---------|
| **p0f v3** | 被动 | 显性 TCP/IP 特征 |
| **Nmap** | 主动 | 显性 + 部分隐性(异常包响应) |
| **JA3/JA4** | 被动 (TLS) | TLS 层指纹 |
| **FATT** | 被动 | 多协议指纹聚合 |
### 参考资料
- RFC 793 — Transmission Control Protocol
- RFC 7323 — TCP Extensions for High Performance (Window Scale, Timestamp)
- RFC 2018 — TCP Selective Acknowledgment Options
- RFC 2883 — An Extension to SACK (D-SACK)
- RFC 3168 — ECN (Explicit Congestion Notification)
- RFC 5765 — Forward RTO-Recovery (F-RTO)
- Ptacek & Newsham, 1998 — *Insertion, Evasion, and Denial of Service: Eluding Network Intrusion Detection*
- p0f v3 — https://lcamtuf.coredump.cx/p0f3/
- Nmap OS Detection — https://nmap.org/book/osdetect.html
- JA3 — https://github.com/salesforce/ja3
- JA4+ — https://github.com/FoxIO-LLC/ja4