添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
首发于 BOT Man
从 Windows Filtering Platform 学习 TCP/IP(1)

从 Windows Filtering Platform 学习 TCP/IP(1)

原文: 《从 Windows Filtering Platform 学习 TCP/IP(1)》 ,公众号 BOTManJL ~

彼节者有间,而刀刃者无厚;以无厚入有间,恢恢乎其于游刃必有余地矣。——《庄子·养生主》

实现代码: github.com/BOT-Man-JL/W

TOC

1 背景

毕业设计要求开发一个匿名通信客户端(Windows 网络驱动):

换句话说,就是需要:

  • 拦截/修改 上层应用程序 传输的数据包,我们驱动的工作对其 透明
    • 需要实现修改(源/目的)TCP/UDP 端口、IP 地址、以太网 MAC 地址的能力
    • 例如,主机 A 访问 Baidu,而链路上黑客嗅探到的数据是 主机 B 在访问 Google
  • 必须在 WinPcap 嗅探数据 之前修改发送数据 ,在嗅探 之后修改接收数据 ,从而避免其嗅探到原始流量
    • 主机上黑客(恶意软件等)主要使用类似于 WinPcap 的工具进行流量嗅探,需要重点保护
    • 如果主机被黑客攻击,原始流量被嗅探,链路上伪造的流量也会被轻易还原

借此机会,学习了 TCP/IP 协议栈、Windows 驱动开发、Windows 网络架构。

2 调研

2.1 WinPcap 流量嗅探原理

WinPcap 的核心 是一个叫做 NPF (Netgroup Packet Filter) NDIS(Network Driver Interface Specification)协议驱动 ,在 Windows 网络驱动栈的较底层(下层直接对接网卡驱动),实现了对经过网卡(物理网卡/虚拟网卡)的所有流量的嗅探。

NPF 在 NDIS 中的位置

注:

Wireshark/WinDump 均使用 WinPcap 作为嗅探引擎
npcap Win10Pcap 是对 WinPcap 改进的版本,原理相似
- github.com/nmap/npcap
- github.com/SoftEtherVPN

2.2 技术方案

在 Windows Vista 以后,TCP/IP 成为了主流,微软升级了 TCP/IP 栈的驱动架构,将 TCP/IP 的核心逻辑 处理放入 tcpip.sys 驱动实现(例如,TCP 的建立连接三次握手、释放连接四次挥手、传输的过程中的可靠性和效率保障,IP 的维护 ARP 表、校验 checksum 等),对上层应用只暴露 简单的接口 (例如 socket 调用 connect/accept 函数,TCP 驱动就实现了三次握手;在 send/recv 函数背后,TCP 驱动实现了重传、差错控制、拥塞控制等机制)。

图 Windows 下一代 TCP/IP 协议栈(来源:Windows Internals, 6th Edition, Part 1)

彼节者有间

微软的 TCP/IP 驱动设计时,为了提供更好的 灵活性 (例如定制 TCP/IP 协议,实现 NAT、IPsec 等),在 TCP/IP 协议栈上 应用层、传输层、网络层(Windows 8 加入了 MAC 子层)暴露一系列 Windows Filter Platform(WFP) 接口。

以无厚入有间

利用这些接口,我们不仅可以过滤(放行/拦截)经过 TCP/IP 驱动的流量,还可以实现对流量的修改(修改 包头 (header) 载荷 (payload) )。进一步的,修改包头中的源/目的地址(TCP/UDP 端口号、IP 地址、MAC 地址),在进入下一层之前,可以实现对包的重定向(例如,NAT 驱动 ipnat.sys 就是通过 WFP 实现的)。

恢恢乎其于游刃必有余地矣

恰好,TCP/IP 驱动和 WinPcap 的 NPF 驱动都是 NDIS 协议驱动,处于 平级关系 。利用 WFP 提供的接口,在 TCP/IP 驱动内完成对流量的修改,一方面能更深入 TCP/IP 流程,另一方面可以绕过 WinPcap 的嗅探。

3 试验 —— 流量重定向

为了体验 WFP 的强大威力,我们基于微软官方的 WFP 流量拦截样例 进行修改,实现一个流量重定向的 proof of concept (PoC)

  • 修改 TCP/UDP 端口号
  • 修改 远端主机 IP 地址
    • 修改 发往 主机 A 包的 目的 IP 地址 重定向 到主机 B
    • 修改从主机 B 收到 包的 源 IP 地址 还原 为主机 A 的地址
  • 对上层应用 透明 ,即上层应用不知道上述修改的存在(用户用浏览器打开 Baidu,经过驱动修改后,可以变成 Google)

我们在同一个局域网内部署了两个主机,分别记作 202 103 。在主机 202 上进行如下测试:

  • 访问主机 104 (一个不存在的主机),将流量重定向到主机 103
  • 访问主机 103 81 端口,将流量重定向到 80 端口上
  • 访问主机 104 81 端口,将流量重定向到主机 103 80 端口上

3.1 传输层修改收包

最初,我们的 PoC 是从官方的 WFP 拦截样例改过来的,分别在 OUTBOUND_TRANSPORT INBOUND_TRANSPORT 层上完成对发送/接收的包的修改。架构模型大致如下:

socket              socket
            |                   ^
            |  TCP stream data  |
            ~                   |
  :--- TCP Driver --------- TCP Driver ---:
  |         |                   ^         |
  |         |    TCP segment    |         |
  |         ~                   |         |
  | OUTBOUND_TRANSPORT  INBOUND_TRANSPORT |  <- modify here!
  |         |                   ^         |
  |         |    TCP segment    |         |
  |         ~                   |         |
  :--- TCP Driver --------- TCP Driver ---:
            |                   ^
            |     IP packet     |
            ~                   |
        IP Driver           IP Driver
            |                   ^
            |   Ethernet frame  |
            ~                   |

3.1.1 结果

  • OUTBOUND_TRANSPORT 层成功重定向 IP 地址、端口,并送达主机 103
  • 主机 202 能接收到主机 103 80 端口发回的数据包(pcap 能抓到)
  • 但是, INBOUND_TRANSPORT 没有拦截到 接收的数据包
  • 从而导致,两个主机超时重传 SYN ;最后主机 103 发送 RST 结束这个过程

3.1.2 分析

每个 socket 都可以用一个 五元组 (5-tuple) 标识 —— 源 IP、源端口、目的 IP、目的端口、协议(TCP/UDP)。TCP/IP 驱动会根据五元组,识别接收数据的 socket:

  • 当 IP 驱动收到一个 IP 包,就会检查承载的内容是什么协议的
  • 如果是 TCP 协议,就会转交给 TCP 驱动处理
  • TCP 驱动会检查五元组,查找关联的 socket,并将这个数据传递给这个 socket

由于从主机 103 接收的包,没有及时把 IP 地址还原到 104 / 端口还原到 81 ,TCP 驱动 根据五元组 找不到 对应的 socket ,就会直接丢弃(有时候还会发送 RST),导致 INBOUND_TRANSPORT 层不能拦截到这个包。

^
  INBOUND_TRANSPORT -> we update 103:80 to 104:81 here!
          |  (unreachable)
     TCP Driver -> 103:80 not found (expect 104:81)! discard packet!
          |  (103:80 -> 202:xx / TCP)
      IP Driver -> IP 103/202 is fine, pass to upper layer
          |  (103:80 -> 202:xx / TCP)

所以,我们要在到达 传输层 之前,即在 网络层 ,修改收包的 IP/端口。

3.2 传输层修改发包

重新设计后,我们在 传输层 OUTBOUND_TRANSPORT )修改发包, 网络层 INBOUND_NETWORK )修改收包。架构模型大致如下:

socket              socket
            |                   ^
            |  TCP stream data  |
            ~                   |
  :--- TCP Driver --------- TCP Driver ---:
  |         |                   ^         |
  |         |    TCP segment    |         |
  |         ~                   |         |
  | OUTBOUND_TRANSPORT          |         |  <- modify here!
  |         |                   |         |
  |         |    TCP segment    |         |
  |         ~                   |         |
  :--- TCP Driver --------- TCP Driver ---:
            |                   ^
            |     IP packet     |
            ~                   |
  :---- IP Driver ---------- IP Driver ---:
  |         |                   ^         |
  |         |     IP packet     |         |
  |         |                   |         |
  |         |             INBOUND_NETWORK |  <- modify here!
  |         |                   ^         |
  |         |     IP packet     |         |
  |         ~                   |         |
  :---- IP Driver ---------- IP Driver ---:
            |                   ^
            |   Ethernet frame  |
            ~                   |

3.2.1 结果

  • INBOUND_NETWORK 层正常工作,TCP 建立连接三次握手、TCP 数据传输都没问题
  • 但是,释放连接的第四次挥手(主机 202 最后一个 ACK )发不出去,导致
    • 主机 202 先关闭了 socket(发完了最后一个 ACK 就可以关闭了)
    • 主机 103 等待最后一个 ACK ,不断重传 FIN+ACK ,最后发送 RST 结束这个过程

3.2.2 分析

在传输层,发送数据需要关联特定的 socket;在 OUTBOUND_TRANSPORT 层发送的数据包,调用 FwpsInjectTransportSendAsync 函数也需要指定 socket(上层句柄)对应端点的 endpointHandle (底层内核对象)。

根据 MSDN 描述 ,如果不及时发送数据,socket 关闭、端点资源释放后,会导致数据无法发送。(详见 Github Issue

在我们的试验中,主机 202 收到了 FIN+ACK 后,发送最后一个 ACK ,就可以 直接关闭 socket 。而这个 ACK 被我们的驱动 拦截 ,在修改后 重新发送 ,端点资源可能已经 被释放 ,导致不能正确发送。

TCP Driver -> send last ACK in response to FIN+ACK
          |  (last ACK / 202:xx -> 104:81 / TCP)
  OUTBOUND_TRANSPORT -> we update 104:81 to 103:80 here!
          |  (last ACK / 202:xx -> 103:80 / TCP)
     TCP Driver -> endpoint was closed! discard packet!
          |  (unreachable)
      IP Driver
          ~

为了解决这个问题,我们推迟到无状态的 网络层 修改发包的 IP/端口。

3.3 网络层修改发包

现在,收包、发包的修改都放到了 网络层 上,即 OUTBOUND_NETWORK INBOUND_NETWORK

socket              socket
            |                   ^
            |  TCP stream data  |
            ~                   |
       TCP Driver           TCP Driver
            |                   ^
            |     IP packet     |
            ~                   |
  :---- IP Driver ---------- IP Driver ---:
  |         |                   ^         |
  |         |     IP packet     |         |
  |         |                   |         |
  |  OUTBOUND_NETWORK     INBOUND_NETWORK |  <- modify here!
  |         |                   ^         |
  |         |     IP packet     |         |
  |         ~                   |         |
  :---- IP Driver ---------- IP Driver ---:
            |                   ^
            |   Ethernet frame  |
            ~                   |

3.3.1 结果

  • 出人意料, OUTBOUND_NETWORK 没有拦截到 发送的数据包
  • 当然,pcap 也抓不到任何流量

3.3.2 分析

从原理上:

  • 如果一个 IP 包要通过网卡发到以太网上,必须要知道目的主机的 MAC 地址,才能构造出完整的 以太网帧 (ethernet frame)
  • 如果要知道目的 MAC 地址,就需要带着目的主机的 IP 地址,利用 ARP 协议进行查询
  • 如果目的主机的 IP 地址查不到对应的 MAC 地址,就不能构造出正确的以太网帧,IP 驱动会将其丢弃

为了验证这个猜想,我们利用 pcap 嗅探了 ARP 流量 —— 不出所料,主机 202 一直在询问主机 104 的 MAC 地址。

由于主机 104 并不存在,主机 202 收不到对应的 ARP 响应,无法构造发往主机 104 的以太网帧,从而导致 OUTBOUND_NETWORK 层不能拦截到发往主机 104 的包。

TCP Driver -> construct IP packet and send
         |  (202:xx -> 104:81 / TCP)
     IP Driver -> search ARP table for host 104, but not found
         |  (unreachable)
  OUTBOUND_NETWORK -> we update 104:81 to 103:80 here!
         |  (unreachable)
     IP Driver
         ~

为了进一步验证这个猜想,我们把主机 104 换成了局域网内的一台真实存在的主机 253 —— 即把试验改为:重定向访问主机 253 81 端口的流量到主机 103 80 端口上。然后,再用 pcap 嗅探 ARP 流量。

果然, OUTBOUND_NETWORK 层拦截到了发往主机 253 的数据包;pcap 也能看出:主机 202 先询问了主机 253 的 MAC 地址,再询问了主机 103 的 MAC 地址。

TCP Driver -> construct IP packet and send
         |  (202:xx -> 253:81 / TCP)
     IP Driver -> search ARP table for host 253, and found
         |  (202:xx -> 253:81 / TCP)
  OUTBOUND_NETWORK -> we update 253:81 to 103:80 here!