以下是基于对 tcpip.sys (Windows 11 21H2, version 10.0.22000.3260, x64) 的完整静态反汇编分析,涵盖架构、核心算法和实现细节。
基本信息
| 字段 | 值 |
|---|---|
| 版本 | 10.0.22000.3260 (Win11 21H2 Build 160101.0800) |
| 架构 | PE32+ x64 (native kernel driver) |
| 镜像基址 | 0x1C0000000 |
| 镜像大小 | 0x318000 (约 3.1 MB) |
| 代码段大小 | 0x206000 (~2 MB) |
| 函数数量 | 7,122 个(来自 .pdata 展开表) |
| 入口点 | 0x1C0271010 (DriverEntry stub) |
一、分段布局与代码架构
.pdata 展开表揭示了内存分段意图,这在内核驱动里远比 ELF 更有诊断价值:
| 段名 | VA | 虚拟大小 | 属性 | 作用 |
|---|---|---|---|---|
.text |
+0x1000 | 1.86 MB | 可执行、不可分页 | 核心协议处理热路径 |
.rdata |
+0x1D2000 | 204 KB | 只读 | 常量、函数名字符串、dispatch table |
.data |
+0x205000 | 96 KB | 可读写 | 全局状态变量、统计计数器 |
.pdata |
+0x21E000 | 84 KB | 只读 | SEH 展开信息,7,122 个函数记录 |
.idata |
+0x233000 | 34 KB | 只读 | 导入表(9 个依赖模块) |
NONPAGE |
+0x23C000 | 176 B | 不可分页 | 必须常驻内存的极小代码 |
PAGE |
+0x23D000 | 55 KB | 可分页 | 不常用路径(错误处理、配置) |
PAGEIPSE |
+0x24B000 | 115 KB | 可分页 | IPsec 处理代码 |
PAGEIDP |
+0x268000 | 11 KB | 可分页 | IDP/诊断路径 |
PAGERSS |
+0x26B000 | 12 KB | 可分页 | RSS 相关代码 |
INIT |
+0x271000 | 9 KB | 初始化后可丢弃 | DriverEntry 及初始化代码 |
关键发现 :热路径全部在 .text(不可分页),IPsec 加/解密在独立的 PAGEIPSE 段,说明正常转发路径不会被 IPsec 代码影响 TLB。
二、DriverEntry 与初始化链
反汇编入口点 0x1C0271010:
asm
DriverEntry @ 0x1C0271010:
mov [rsp+8], rbx ; 保存注册表路径
push rdi
sub rsp, 0x20
mov rbx, rdx ; RegistryPath
mov rdi, rcx ; DriverObject
call 0x1C0271044 ; ① stack cookie 检验 (GS guard)
mov rdx, rbx
mov rcx, rdi
call 0x1C0271fbc ; ② TcpipDriverEntry (真正的初始化)
ret
0x1C0271044 是 Windows 11 引入的 stack cookie 双向验证 ------对比 [rip - 0x5d78b] 与 0x2b992ddfa232(一个已知的 GS 哨兵值),不匹配则 int 0x29 (fast fail)。
真正的初始化函数 TcpipDriverEntry @ 0x1C0271FBC(帧尺寸 0x3F0,保存 8 个被调用保存寄存器):
asm
TcpipDriverEntry @ 0x1C0271FBC:
; 栈帧建立:sub rsp, 0x180; 使用 RBP 做帧指针
call 0x1C0271078 ; ③ ETW 提供者注册 (RtlEtwRegister)
call 0x1C00C6860 ; ④ IsKernelDebuggerEnabled (KD 检测)
je ...safe_mode ; 根据 KD 状态设置调试标志
call 0x1C00C67F8 ; ⑤ IsVerifierEnabled (Driver Verifier)
call [rip - 0x3d971] ; ⑥ KeQueryPerformanceCounter (时钟基准)
call 0x1C00BD2C4 ; ⑦ 读取 BootMode(安全启动检测)
call 0x1C00BD968 ; ⑧ 读取 OS 版本/功能开关
; ... 后续注册 NMR 提供者、NDIS 协议驱动、WSK 等
初始化严格串行,体现了功能发现优先原则:先探 KD/Verifier 再决定调试模式,再初始化功能,最后注册对外接口。
三、外部依赖与子系统划分
导入表精确划分了职责边界:
3.1 ntoskrnl.exe(336 个函数)--- 内核基础服务
重要分类:
| 类别 | 代表函数 | 用途 |
|---|---|---|
| 内存池 | ExAllocatePool2/3, ExAllocatePoolWithTag |
网络对象生命周期 |
| 锁原语 | ExAcquireSpinLockExclusive, KeAcquireInStackQueuedSpinLock |
多核并发控制 |
| 定时器 | ExAllocateTimer, KeSetCoalescableTimer, KeInitializeTimerEx |
TCP 超时/重传 |
| Hash 表 | RtlCreateHashTableEx, RtlInsertEntryHashTable |
连接/路由查找 |
| 泛型 AVL 树 | RtlInitializeGenericTableAvl, RtlLookupElementGenericTableFullAvl |
有序集合存储 |
| IP 地址工具 | RtlIpv4AddressToStringExW, RtlIpv6StringToAddressW |
地址转换 |
| ETW 跟踪 | EtwWrite, EtwWriteTransfer |
事件追踪 |
| 安全 | SeAccessCheck, SeTokenFromAccessInformation |
socket 权限检查 |
| PCW 性能 | PcwAddInstance, PcwRegister |
性能计数器 |
| 特性标志 | RtlQueryFeatureConfiguration, RtlNotifyFeatureUsage |
A/B 功能开关 |
特别值得注意 :同时使用 ExAllocatePool2(Win10 2004+ 新 API,默认清零,无 NX 页)和老的 ExAllocatePoolWithTag,说明此版本处于迁移期。
3.2 NETIO.SYS(302 个函数)--- 网络 I/O 引擎
这是 tcpip.sys 最大的外部依赖,承担了大量非 TCP 协议语义的工作:
| 前缀 | 函数数 | 职责 |
|---|---|---|
Netio* |
~100 | NBL 生命周期、工作队列、MDL 操作 |
Kfd* |
~35 | WFP 过滤引擎分类(KFD = Kernel Filter Driver) |
Wfp* |
~25 | WFP 流检测、NBL 信息标签 |
Nsi* |
~12 | Network Store Interface(配置存储) |
Pt* |
~11 | Patricia 前缀树操作(路由表核心数据结构) |
Rtl*(NETIO) |
~20 | 定时器轮、Toeplitz 哈希、MDL 拷贝 |
Nmr* |
~10 | Network Module Registrar(模块解耦注册) |
Wsk* |
~4 | Winsock Kernel Provider 注册 |
关键发现 :路由表使用的是 Patricia Trie (PtGetLongestMatch, PtGetNextShorterMatch, PtInsertEntry),而非简单哈希表,这与 Linux FIB(基数树)思路相同,支持 LPM(最长前缀匹配)。
3.3 NDIS.SYS(50 个函数)
NdisRegisterProtocolDriver / NdisDeregisterProtocolDriver → 协议注册
NdisSendNetBufferLists / NdisReturnNetBufferLists → 发送/归还 NBL
NdisOidRequest / NdisDirectOidRequest → 硬件 OID 控制
NdisOpenNDKAdapter / NdisCloseNDKAdapter → NDK(RDMA)支持
NdisIfRegisterInterface / NdisIfDeregisterInterface → 接口注册
NdisAllocateNetBufferListPool → NBL 内存池
tcpip.sys 以协议驱动 身份注册到 NDIS,通过 NdisSendNetBufferLists 提交发送,通过 NDIS 回调接收。注意 NdisOpenNDKAdapter 的存在说明支持 **NDK(Network Direct Kernel)**即内核态 RDMA 加速路径。
3.4 fwpkclnt.sys(54 个函数)--- WFP 内核客户端
IPsecDriverProcessClearTextResponse → IPsec 明文包处理
IPsecDriverInitiateAcquire → IKE 协商触发
FwpsInjectNetworkReceiveAsync0 → 注入重新分类包
FwppVpnTriggerEventFire0 → VPN 触发事件
FwpsFlowAssociateContext0 → 流上下文绑定
KfdClassify / KfdClassify2 → 内核过滤器分类
3.5 cng.sys(15 个函数)--- 加密
BCryptOpenAlgorithmProvider / BCryptCloseAlgorithmProvider
BCryptGenerateSymmetricKey / BCryptDestroyKey
BCryptEncrypt / BCryptDecrypt
BCryptHash / BCryptHashData / BCryptFinishHash
BCryptGenRandom
所有密码学操作通过 CNG(Cryptography Next Generation)完成,tcpip.sys 自身不含任何密码学原语实现。用于 TCP MD5 签名(RFC 2385)和 IPsec 内嵌处理。
四、核心算法分析
4.1 IP 路径缓存:IppFindOrCreatePath @ 0x1C0050910
这是迄今找到的最大函数(3,732 字节),负责 IP 路径对象的查找与创建:
asm
IppFindOrCreatePath @ 0x1C0050910:
; 函数签名 (推断):
; RCX = IP 接口对象
; RDX = 目标地址 (IN6_ADDR*)
; R8D = 协议族 (AF_INET/AF_INET6)
; R9 = 下一跳提示
sub rsp, 0x208 ; 大栈帧,局部路径对象构建
; ① 按 CPU 编号做分片锁(per-CPU sharding)
mov eax, [gs:0x1A4] ; 读当前处理器编号 (KeGetCurrentProcessorNumber)
and rax, r15 ; 对 CPU 数取模(掩码)
inc rax
shl rax, 6 ; × 64(缓存行对齐的 per-CPU bucket)
lock inc [rbx + rax] ; 原子递增该 CPU 的引用计数
; ② 尝试从路径缓存读取(乐观读路径)
call [rip + 0x1E3BDD] ; AcquireSharedLock
test al, al
jne found_in_cache
; ③ 未命中,创建新路径对象
lock dec [rbx] ; 释放共享引用
call [rip + 0x1E3BB2] ; AllocatePathObject
lock inc [rbx]
lea rcx, [rbp + 0x38]
call [rip + 0x1E3B8F] ; InsertPathToCache
; ④ 下一跳解析
call 0x1C00517AC ; IppResolveNextHop
lock dec [rax + rcx + 0x300] ; 释放 per-CPU 引用
设计模式 :[gs:0x1A4] 直接读取 KPCR.CurrentPrcb.Number(处理器编号),然后 CPU_ID & mask × 64 计算 cache-line 对齐的 per-CPU 计数器地址。这是 Windows 内核避免 NUMA 伪共享的标准范式,在 Linux 中对应 this_cpu_ptr()。
4.2 路由最长前缀匹配
通过 NETIO 的 Patricia Trie API:
PtGetLongestMatch(table, prefix, prefix_len)--- O(k) 其中 k = 地址位数PtGetNextShorterMatch--- 迭代找次长匹配(用于策略路由失败后回退)PtInsertEntry / PtDeleteEntry--- 路由表修改PtEnumOverTable--- 路由表遍历(netstat -r 路径)
Patricia Trie 相对于 hash 表的优势:天然支持聚合前缀、内存占用低、支持按前缀枚举。
4.3 TCP 连接状态机与 TCB 管理
从 0x1C0071AED 区域的大型 jump table(通过 jmp [rsp + 0x30] 间接跳转)和 .rdata 中的字符串可以重构状态机结构:
TCB(Transmission Control Block)布局关键偏移(由反汇编推断):
| 偏移 | 类型 | 语义 |
|---|---|---|
| +0x10 | QWORD |
接收队列头 |
| +0x18 | QWORD |
发送队列 / 重传队列 |
| +0x34 | DWORD |
序列号/状态标志 |
| +0x38 | QWORD |
关联的 TCP_ENDPOINT |
| +0x70 | DWORD |
连接标志位(bit 0x10 = 拥塞控制模式) |
| +0x72 | WORD |
目标端口(网络字节序,ror ax,8 换序) |
| +0x74 | DWORD |
拥塞控制相关标志(bit 0x600000 = Compound TCP) |
| +0x288 | DWORD |
连接属性位图 |
| +0x290 | QWORD |
IPsec 上下文指针 |
| +0x2C6 | BYTE |
IPv4/IPv6 标志 |
asm
; TcbCleanup 中观察到的状态检查
mov ecx, [rbx + 0x288] ; 加载连接属性
test cl, 1 ; bit 0: 是否有 IPsec SA
je skip_ipsec_cleanup
mov rcx, [rdi + 8] ; 加载 SA 句柄 1
call 0x1C0164C9C ; IpsecSaRelease
mov rcx, [rdi + 0x10] ; 加载 SA 句柄 2
call 0x1C016C0A4 ; IpsecSaRelease2
; ...
mov edx, 0x65535145 ; 'QUeE' --- ExFreePoolWithTag 的 tag
mov rcx, rdi
call [rip + 0x1304B5] ; ExFreePoolWithTag
连接终止时对多个 IPsec SA 句柄的精确释放顺序揭示了 TCP 与 IPsec 的深度耦合------不是简单的包过滤叠加,而是在 TCB 级别维护 SA 引用。
4.4 拥塞控制算法识别
在 .rdata 和 TcpLossHistogram 函数中发现的关键证据:
asm
; @ 0x1C0074510 (函数大小 5264 字节)
; 检查拥塞控制算法选择标志
mov ecx, [rdi + 0x74] ; TCB 标志
test ecx, 0x600000 ; 位掩码测试
jne 0x1C007562D ; → Compound TCP 分支
; CUBIC/New Reno 公共路径:
movabs rax, 0x624DD2F1A9FBE77 ; 除法优化常数(快速整数除法)
mul rbx ; 乘以魔数
sub rbx, rdx
shr rbx, 1
add rbx, rdx
shr rbx, 9 ; 相当于 ÷ 1000(毫秒→秒 RTT 计算)
0x624DD2F1A9FBE77 是 ⌈2^60 / 1000⌉ 的魔数,用于将 RTT(以 100ns 为单位的内核时间)高效转换为毫秒。这直接关联到 TCP 超时计算 和 RTO(Retransmission Timeout) 算法。
标志位 0x600000 区分两条路径,结合 .rdata 字符串 TcpCongestionAlgorithm 和 TcpThrottleInitialCwnd,确认实现了:
- CUBIC TCP(默认,来自 RFC 8312)
- Compound TCP(CTCP) (可配置,
tcpCongestionProvider注册表键),这是微软专有的复合拥塞控制算法,同时维护延迟和丢包两个窗口
从字符串 TcpSampleInitialCwnd 和 TcpSamplePacingProfile 还可以看出,该版本包含了初始拥塞窗口采样和**包速调度(Pacing)**实现。
4.5 TCP_ENDPOINT vs TCB 双层结构
.rdata 中区分了三种对象的生命周期错误消息:
"TCP_ENDPOINT was cleaned up without being closed" @ 0x1C01DB6F8
"TCB was cleaned up without being closed" @ 0x1C01E3B58
"TCP_LISTENER was cleaned up without being closed" @ 0x1C01E3F60
这揭示了微软 TCP 实现的三层对象模型:
TCP_LISTENER (被动监听)
└── TCP_ENDPOINT (半连接/连接端点,与 AFD socket 对应)
└── TCB (传输控制块,RFC 793 意义的连接状态机)
TCP_ENDPOINT 是 AFD.sys 可见的对象,包含缓冲区语义;TCB 是纯协议状态机,不对用户态直接可见。这种分层避免了 TCP 连接复用时的引用计数混乱。
4.6 Timer Wheel 实现
NETIO 导入中的 RtlInitializeTimerWheel, RtlUpdateCurrentTimerWheelTick, RtlGetNextExpiredTimerWheelEntry 等明确了定时器实现:
Windows TCP 使用**分层时间轮(Hierarchical Timer Wheel)**而非 Linux 的 hrtimer 或红黑树。时间轮适合 TCP 的大量短生命期定时器(SYN 超时、重传计时器、TIME_WAIT 计时器),在 O(1) 时间内进行定时器插入和到期检测。
RtlCompute37Hash / RtlComputeToeplitzHash 的存在表明:
- 37-hash 用于连接哈希表桶分布
- Toeplitz hash 用于 RSS(Receive Side Scaling)的 RSS Key 计算,确保同一流的包落在同一 CPU 队列
五、WFP 集成的深度
WFP 不是简单的包过滤钩子,而是深度嵌入在数据路径中:
asm
; AleRequestTcpConnectionAbort @ 0x1C0156508
; 分配 ALE (Application Layer Enforcement) 上下文
mov edx, 0x41656C41 ; 池标签 'AleA'
mov ecx, 0x38 ; 大小 0x38 字节
call 0x1C0060B6C ; ExAllocatePoolWithTag
; 填写 ALE 上下文:
mov [rbx], bp ; 地址族 (AF_INET=2)
mov [rbx + 8], r15 ; 远端 IP 地址
mov [rbx + 0x10], r14w ; 远端端口
mov [rbx + 0x14], eax ; 本地 IP (IPv4)
; 或者 IPv6:
movups xmm0, [rsi]
movdqu [rbx + 0x14], xmm0 ; 16 字节 IPv6 地址 SIMD 拷贝
; 然后调用 WFP 分类引擎决定是否允许中止
call [rip + 0xDDE56] ; KfdClassify (核心分类调用)
重要细节 :IPv6 地址拷贝使用 movups xmm0 + movdqu------16 字节 SIMD 单指令,与用 4 次 32 位拷贝相比效率高 4 倍。这种优化在协议栈高频路径上随处可见。
HandOffAuthFwIpsecStateToAleEntry 字符串揭示了 AuthFW(认证防火墙)→ IPsec → ALE 三者的状态交接机制,这是实现"连接级策略"(不是包级别)的关键------允许 WFP callout 在 TCP 连接建立时一次性决策,而非每包重新判断。
六、并发控制模式
从反汇编中识别出的锁策略:
| 场景 | 锁类型 | 代码证据 |
|---|---|---|
| 路径缓存读 | 共享推锁(SRW,per-CPU 分片) | lock inc/dec [rbx + rax*64] 模式 |
| 路由表写 | 排它推锁 | ExAcquirePushLockExclusiveEx |
| 连接哈希表 | 自旋锁(DPC 级) | ExAcquireSpinLockExclusiveAtDpcLevel |
| TCP 状态机 | 排它自旋锁 | lock cmpxchg [r8], edx (CAS 模式) |
| 引用计数 | 原子 inc/dec | lock inc/dec [addr] |
| 运行中保护 | RundownProtection | ExAcquireRundownProtectionCacheAwareEx |
在 0x1C01041A0 处发现了典型的 lock cmpxchg 模式:
asm
mov eax, ecx ; 加载当前状态
lock cmpxchg [r8], edx ; CAS: 如果 *r8 == eax, 写入 edx
cmp ecx, eax ; 检查是否成功
jne retry ; 失败则重试 → 自旋
cmp edx, 4 ; 检查新状态值
这是 TCP 状态机转换的无锁实现(SYN_SENT→ESTABLISHED 等),避免了重量级锁的开销。
七、完整调用链图(TCP 出站包)
根据反汇编重构的 TCP SYN 发送路径:
用户态 connect()
│ IOCTL: AFD_CONNECT → \Device\Afd
↓
AFD.sys: TdiConnect IRP
│
↓ IRP
TCPIP.sys:
TcpTlConnect()
→ TcpCreateTcb() [分配 TCB, 设置状态 SYN_SENT]
→ IppFindOrCreatePath() [0x1C0050910, 路径缓存查找/创建]
→ PtGetLongestMatch() [Patricia Trie LPM 路由查找]
→ IppResolveNextHop() [ARP/NDP 下一跳解析]
→ TcpBuildSynSegment() [构建 SYN, 设置 MSS/窗口缩放/时间戳选项]
→ ip_queue_xmit()
→ [gs:0x1A4] [获取 CPU 编号,选择 per-CPU 发送队列]
→ BCryptGenRandom() [ISN 随机化,RFC 6528]
→ KfdClassify() [WFP 出站 Transport Layer 分类]
→ ① ALLOW → 继续
→ ② BLOCK → 返回错误
→ ③ REDIRECT → 重路由
→ NdisSendNetBufferLists() [提交 NBL 到 NDIS 微端口]
→ [WFP NDIS LWF 层二次过滤]
→ 微端口 DMA → 硬件
八、关键技术总结
| 技术点 | 实现细节 | 意义 |
|---|---|---|
| 路由表 | Patricia Trie(via NETIO Pt* API) |
O(k) LPM,天然前缀聚合 |
| TCP 拥塞控制 | CUBIC(默认)+ Compound TCP(可选) | Windows 专有,含延迟感知 |
| 路径缓存 | Per-CPU 分片 + cache-line 对齐 | 避免 NUMA 伪共享 |
| 定时器 | 分层时间轮(Hierarchical Timer Wheel) | O(1) 插入/到期,适合大量 TCP 定时器 |
| TCP 对象 | 三层:LISTENER / ENDPOINT / TCB | 职责分离,支持半连接队列溢出防御 |
| WFP 集成 | 连接级 ALE 上下文 + 流状态传递 | 不是逐包策略,是连接级决策 |
| IPsec | TCB 内嵌 SA 句柄,独立 PAGEIPSE 段 | 热路径不受 IPsec 代码影响 |
| IPv6 地址操作 | movups xmm0 / movdqu SIMD |
16 字节单指令,4× 优于标量 |
| ISN 随机化 | BCryptGenRandom(CNG) |
符合 RFC 6528 |
| RSS 哈希 | Toeplitz Hash(via NETIO) | 多核包分发,保证流亲和性 |
| 锁策略 | CAS + 自旋锁 + per-CPU 分片读写 | 最小化锁竞争的多层次并发控制 |