【技术踩坑记】WFP 拦截可疑 IP 为何失效?——从原理到 TCP 强制关闭的完整实践

141 阅读3分钟

一、问题背景

最近在对 银狐木马 的防御场景中,我们做了一类通用的防御措施:

  • 一旦检测到进程对外连接可疑 IP,就立刻下发 WFP (Windows Filtering Platform) 规则,把这个 IP 封堵掉。
  • 理论上,这样做就能阻止恶意程序对外通信,达到“隔绝 C2 通道”的效果。

但是上线运行后,发现了一个奇怪的现象:

  • WFP 规则下发成功,拦截日志也有记录;
  • 但是检查系统 TCP 连接时,依旧能看到和可疑 IP 的 ESTABLISHED 连接存在
  • 因此,最初的判断逻辑(“连接是否还在 = 是否拦截成功”)完全失效。

换句话说:规则确实生效,但 TCP 连接没有立刻消失,导致检测机制错判。 这就是本次文章的起点。


二、底层原理分析

为什么会出现这种情况?

1. WFP 的工作方式

WFP 是 Windows 提供的内核过滤框架,可以在不同层级(ALE、Transport、Network)对数据流进行 过滤/修改/丢弃。 当我们下发规则屏蔽某个 IP 时:

  • 新的数据包会被 WFP 丢弃
  • 但是 已有的 TCP 控制块 (TCB) 依然存在。

2. TCP 的协议栈行为

TCP 是有状态的协议,维护着四元组(本地 IP、端口,对端 IP、端口)的会话控制块:

  • 即便你把数据包丢弃,TCP 栈里的连接状态依旧停留在 ESTABLISHED
  • 应用层 socket 仍然“以为”连接着,send() 还能成功(数据进了缓冲区,但发不出去);
  • 直到 TCP 经过多次重传超时,或者收到 RST,连接才会被动关闭。

3. 为什么要主动关闭?

如果仅靠 WFP 封堵:

  • 连接长期挂在系统里,形成“僵尸连接”;
  • 应用层感知滞后,可能阻塞、假性成功;
  • 系统资源被占用,高并发下可能堆积大量无效 socket。

如果 在下发 WFP 规则后,主动关闭对应 TCP 连接

  • 内核会立即删除 TCP 控制块;
  • 应用层立刻收到 WSAECONNRESET 等错误;
  • 资源立刻释放,不留隐患。

一句话总结: 👉 WFP 负责拦截流量,TCP 强制关闭负责回收会话,两者要配合使用,才能真正做到“彻底封堵”。


三、代码实践:关闭指定 TCP 连接

Windows 本身没有直接提供“关闭别人进程 socket”的 API,但可以通过 SetTcpEntry 来实现:

  • 先用 GetTcpTable2 枚举系统中的 TCP 连接;
  • 匹配到目标四元组(本地 IP/端口、远程 IP/端口);
  • 调用 SetTcpEntry,把状态改成 MIB_TCP_STATE_DELETE_TCB,强制删除。

下面给出核心代码示例(需管理员权限运行):

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

int KillTcpConnection(const char* srcIp, int srcPort,
                      const char* dstIp, int dstPort) {
    PMIB_TCPTABLE2 pTcpTable;
    DWORD dwSize = 0;

    // 第一次调用获取所需缓冲区大小
    GetTcpTable2(NULL, &dwSize, TRUE);
    pTcpTable = (MIB_TCPTABLE2*)malloc(dwSize);

    if (GetTcpTable2(pTcpTable, &dwSize, TRUE) == NO_ERROR) {
        for (DWORD i = 0; i < pTcpTable->dwNumEntries; i++) {
            MIB_TCPROW2 row = pTcpTable->table[i];

            char localAddr[INET_ADDRSTRLEN], remoteAddr[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &row.dwLocalAddr, localAddr, sizeof(localAddr));
            inet_ntop(AF_INET, &row.dwRemoteAddr, remoteAddr, sizeof(remoteAddr));

            int localPort = ntohs((u_short)row.dwLocalPort);
            int remotePort = ntohs((u_short)row.dwRemotePort);

            if (strcmp(localAddr, srcIp) == 0 && localPort == srcPort &&
                strcmp(remoteAddr, dstIp) == 0 && remotePort == dstPort) {
                
                MIB_TCPROW killRow;
                killRow.dwState = MIB_TCP_STATE_DELETE_TCB;
                killRow.dwLocalAddr = row.dwLocalAddr;
                killRow.dwLocalPort = row.dwLocalPort;
                killRow.dwRemoteAddr = row.dwRemoteAddr;
                killRow.dwRemotePort = row.dwRemotePort;

                if (SetTcpEntry(&killRow) == NO_ERROR) {
                    std::cout << "✅ Connection killed!\n";
                    free(pTcpTable);
                    return 0;
                } else {
                    std::cerr << "❌ SetTcpEntry failed\n";
                }
            }
        }
    }
    free(pTcpTable);
    return -1;
}

int main() {
    // 示例:关闭 192.168.1.100:50000 -> 192.168.1.200:80 的连接
    KillTcpConnection("192.168.1.100", 50000, "192.168.1.200", 80);
    return 0;
}

⚠️ 注意:

  • 需要 管理员权限 才能成功调用 SetTcpEntry
  • 仅适用于 IPv4(IPv6 需用 GetTcp6Table2);
  • 关闭连接相当于发出 RST,应用层会立即感知断开。

四、总结

这次的“踩坑”让我彻底理解了:

  • WFP 拦截的是流量,不是连接;
  • TCP 会话需要主动终结,否则就是挂在系统里的僵尸连接。

所以在做防御策略时,WFP + 强制关闭 TCP 才能构成完整的闭环:

  • WFP:拦截后续恶意流量;
  • KillTCP:快速断开已有连接,释放资源。

这样,才能既阻止恶意外联,又保证检测与判断逻辑的准确。

👉 希望这篇文章能帮到大家,少踩这个“WFP 封堵却不杀连接”的坑。