一、问题背景
最近在对 银狐木马 的防御场景中,我们做了一类通用的防御措施:
- 一旦检测到进程对外连接可疑 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 封堵却不杀连接”的坑。