简介:面向系统管理与网络安全方向开发者的VC++实战资源,讲解如何调用Windows设备管理与网络接口API,实现禁用USB接口和禁止上网两大管控功能。程序涉及SetupDiGetClassDevs、CM_Disable_DevNode、GetAdaptersInfo、SetInterfaceState等关键函数,覆盖设备枚举、状态设置、权限处理等完整流程,适合有一定C/C++基础、希望掌握系统级硬件与网络控制的读者。资源包共49个文件,主要为21个头文件和18个CPP源文件,配套工程配置(.dsp/.dsw)、界面与图标资源(.rc/.rc2/.ico/.bmp)以及编译好的可执行示例,大小仅294KB。源码将USB设备禁用、网络接口禁用、界面展示等功能模块化封装,结构清晰,便于直接阅读和二次开发。已有1718人学习,对理解Windows底层API调用和系统功能定制很有参考价值。
1. 用VC++做禁用USB接口和禁止上网功能的出发点
用VC++写一个工具,同时控制禁用USB接口和禁止上网功能,是局域网终端管理里很常见的诉求。制造车间的工控机不能随便插入U盘拷贝程序,又要断开外网防止代码外泄;企业财务机临时要断网,但远程桌面的端口还想保留。修改组策略需要域环境,在非域或Windows 10家庭版上经常不生效。更直接的办法是自己写一个带管理员权限的VC++程序,通过注册表、WMI、Windows防火墙API把系统和网络状态切过去。这篇文章不是讲内核驱动或WFP过滤,而是讲一线运维能落地、能回滚、能排错的Win32层方案,完整覆盖“怎么做、参数怎么设、坑在哪”。
2. 禁用USB接口:VC++里绕不开的注册表和服务控制
2.1 三个控制层:服务Start值、设备安装策略、设备实例
要禁用USB接口,不是在User32里发一条广播就能完成的。Windows枚举USB设备时,主要看三处:USB主控制器驱动、Hub驱动、以及大容量存储类驱动USBSTOR。常见的管理工具大多是三层组合:先把USBSTOR驱动的启动类型改成禁用,再用DeviceInstall设备安装策略拒绝新设备,最后清理已经枚举出来的设备残留。
第一层是服务控制。注册表路径在HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR,里面有个Start值控制驱动是否随系统启动。改成4表示禁用,0是boot,1是system,2是auto,3是demand。把Start改成4,新插入的U盘不会加载驱动,也不分配盘符。但已经识别过的设备在USBSTOR\Enum子键里还会有残留,直接插上去可能被系统从缓存中重新枚举,所以还需要第二层。
第二层是设备安装策略。路径在HKLM\SOFTWARE\Policies\Microsoft\Windows\DeviceInstall\Restrictions,设置DenyDeviceIDs=1并写入要拦截的硬件ID,再配合DenyDeviceIDsRetroactive=1让已装设备也被踢出。这层相当于在驱动加载前就被PnP管理器拦截,比只改USBSTOR更彻底。
第三层是设备实例级。用SetupAPI枚举USB设备,调用SetupDiCallClassInstaller的DIF_PROPERTYCHANGE把设备disable。这种做法的粒度最细,可以只禁用机箱前面板的两个端口,但代码量明显大很多。我一般不会直接用VC++写这一层,除非客户明确要求“禁指定端口”。大多数落地场景,前两层已经够了。
下面这张表是我做这类需求时的选型依据。
| 方法 | 作用位置 | 是否立即生效 | 恢复难度 | 适用场景 |
|---|---|---|---|---|
| USBSTOR Start=4 | 驱动服务 | 新设备立即,已插需重载 | 低 | 只禁U盘、移动硬盘 |
| DeviceInstall Restrictions | 设备安装策略 | 重启或重启PlugPlay服务 | 中 | 禁某类硬件ID |
| SetupAPI设备实例禁用 | 设备树 | 立即 | 中 | 精细控制某个USB端口 |
2.2 用VC++修改USBSTOR的Start值并清理Enum残留
下面是一个最小可用的函数,开关USB大容量存储驱动。
// UsbControl.cpp #include <windows.h> #include <tchar.h> #include <stdio.h> #include <shlwapi.h> #pragma comment(lib, "shlwapi.lib") INT_PTR SetUsbStorageState(BOOL bDisable) { HKEY hKey = NULL; DWORD dwStart = bDisable ? 4 : 3; // 4=禁用 3=手动 LPCTSTR lpPath = _T("SYSTEM\\CurrentControlSet\\Services\\USBSTOR"); LONG lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, lpPath, 0, KEY_SET_VALUE | KEY_WOW64_64KEY, &hKey); if (lRet != ERROR_SUCCESS) { _tprintf(_T("RegOpenKeyEx failed: %ld\n"), lRet); return -1; } lRet = RegSetValueEx(hKey, _T("Start"), 0, REG_DWORD, (const BYTE*)&dwStart, sizeof(dwStart)); RegCloseKey(hKey); if (lRet != ERROR_SUCCESS) return -2; return 0; } DWORD ClearUsbStorageEnum() { LPCTSTR lpPath = _T("SYSTEM\\CurrentControlSet\\Services\\USBSTOR"); HKEY hKey = NULL; LONG lRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, lpPath, 0, KEY_WRITE | KEY_WOW64_64KEY, &hKey); if (lRet != ERROR_SUCCESS) return lRet; DWORD dwIndex = 0; TCHAR szSubKey[128]; DWORD cchName = 128; while (RegEnumKeyEx(hKey, dwIndex, szSubKey, &cchName, NULL, NULL, NULL, NULL) == ERROR_SUCCESS) { if (lstrcmpi(szSubKey, _T("Enum")) == 0) { SHDeleteKey(hKey, _T("Enum")); break; } cchName = 128; dwIndex++; } RegCloseKey(hKey); return ERROR_SUCCESS; }逻辑说明:第一步打开USBSTOR服务注册表项,写入Start=4。第二步枚举该服务子键,找到Enum后删除。删除Enum是为了让系统在下一次插入设备时重新走一遍PnP安装流程,而不是沿用旧的设备缓存。如果只改Start,已经插着的U盘即使图标消失,在设备暗黑列表里依然可能被识别。
参数说明:注册表访问必须带上KEY_WOW64_64KEY,因为VC++工程如果编译成32位,在64位Windows上会被RegOpenKeyEx重定向到WOW6432Node,改不到真实的USBSTOR服务。这个坑很隐蔽,我见过不少工具写进去之后,regedit看值没变,就是因为忘了加这个标志。
2.3 用DeviceInstall限制策略兜底,并写一批要拒绝的硬件ID
设备安装策略适合按硬件ID做黑白名单,比如只拒绝USB大容量存储,不影响USB键鼠。
int SetDeviceInstallRestrictions(BOOL bDeny) { HKEY hRoot; DWORD dwDisposition; LPCTSTR lpPath = _T("SOFTWARE\\Policies\\Microsoft\\Windows\\DeviceInstall\\Restrictions"); LONG lRet = RegCreateKeyEx(HKEY_LOCAL_MACHINE, lpPath, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE | KEY_WOW64_64KEY, NULL, &hRoot, &dwDisposition); if (lRet != ERROR_SUCCESS) return -1; DWORD dwDeny = bDeny ? 1 : 0; RegSetValueEx(hRoot, _T("DenyDeviceIDs"), 0, REG_DWORD, (BYTE*)&dwDeny, sizeof(dwDeny)); RegSetValueEx(hRoot, _T("DenyDeviceIDsRetroactive"), 0, REG_DWORD, (BYTE*)&dwDeny, sizeof(dwDeny)); // 写入拦截列表,'*' 代表所有USB大容量存储设备 if (bDeny) { HKEY hList; RegCreateKeyEx(hRoot, _T("DenyDeviceIDsList"), 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE | KEY_WOW64_64KEY, NULL, &hList, &dwDisposition); RegSetValueEx(hList, _T("1"), 0, REG_SZ, (const BYTE*)_T("*USBSTOR*"), sizeof(TCHAR) * 11); RegCloseKey(hList); } RegCloseKey(hRoot); return 0; }这段代码把追加到DenyDeviceIDsList里的硬件ID设为*USBSTOR*,然后用DenyDeviceIDsRetroactive=1让已安装的设备也被拒绝。恢复时把DenyDeviceIDs设回0即可,但列表里的值不会自动删,建议恢复时把Restrictions键整体删除,避免留着半条规则。
提示:做USB接口禁用后,用USBPcap配合Wireshark抓USB总线,仍能看到设备枚举的SETUP包。这并不代表禁用失败,只能说明物理层还在工作,驱动层已经被挡住。要真正断开引脚电平,只能靠硬件继电器或者BIOS里的USB控制器开关。
3. 禁止上网功能的VC++实现:从禁网卡到防火墙
3.1 选型:WMI禁网卡、IP Helper路由、Windows防火墙
禁止上网比禁用USB更敏感,因为网卡一旦禁用,远程管理通道会立刻断掉。所以在设计时就要想清楚,是“彻底断网”还是“只断Internet”。如果是怕员工访问外网,但内网办公系统还要用,那就不能一刀切禁用网卡。
我常用的组合是两个层次:第一层用WMI禁用物理网卡作为硬切断;第二层用Windows防火墙的出站规则作为白名单边界。如果客户要求保留远程桌面,则多写一条allow规则放行3389端口。
三个方案的对比:
| 方案 | API/接口 | 影响范围 | 回滚手段 |
|---|---|---|---|
| 禁用网络适配器 | WMI Win32_NetworkAdapter | 断掉该适配器所有连接 | Enable方法恢复 |
| 删除默认路由 | GetIpForwardTable/DeleteIpForwardEntry | 断开到Internet的路由 | CreateIpForwardEntry恢复 |
| 防火墙出站阻止 | INetFwPolicy2或netsh | 按端口/程序/接口过滤 | 删除规则 |
从实际运维角度,防火墙规则是最灵活的,但也是忘记回滚概率最高的;禁用网卡是最容易验证的,拔网线就断网,但误操作代价也大。通常我让客户选,一般都选防火墙,因为远程桌面还能留着。
3.2 用VC++调用WMI禁用指定网络适配器
WMI是跨系统版本最稳的方案,比直接操作SetupAPI要简单得多。下面这段代码演示如何按连接名称禁用“以太网”适配器。
#include <windows.h> #include <comdef.h> #include <Wbemidl.h> #include <string> #pragma comment(lib, "wbemuuid.lib") HRESULT SetAdapterOnline(const wchar_t* wsName, BOOL bEnable) { HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hr)) return hr; hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); if (FAILED(hr) && hr != RPC_E_TOO_LATE) { CoUninitialize(); return hr; } IWbemLocator *pLoc = NULL; IWbemServices *pSvc = NULL; hr = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc); if (FAILED(hr)) { CoUninitialize(); return hr; } hr = pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), NULL, NULL, 0, NULL, 0, 0, &pSvc); if (FAILED(hr)) { pLoc->Release(); CoUninitialize(); return hr; } std::wstring wql = L"SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionID='"; wql += wsName; wql += L"'"; IEnumWbemClassObject *pEnum = NULL; hr = pSvc->ExecQuery(_bstr_t(L"WQL"), _bstr_t(wql.c_str()), WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, NULL, &pEnum); if (SUCCEEDED(hr) && pEnum) { IWbemClassObject *pObj = NULL; ULONG uRet = 0; while (pEnum->Next(WBEM_INFINITE, 1, &pObj, &uRet) == S_OK && uRet == 1) { VARIANT vt; VariantInit(&vt); pObj->Get(_bstr_t(L"__PATH"), 0, &vt, NULL, NULL); std::wstring wsPath = vt.bstrVal; VariantClear(&vt); IWbemClassObject *pInClass = NULL, *pIn = NULL; if (SUCCEEDED(pObj->GetMethod(bEnable ? L"Enable" : L"Disable", 0, NULL, &pInClass))) { pInClass->SpawnInstance(0, &pIn); pSvc->ExecMethod(_bstr_t(wsPath.c_str()), _bstr_t(bEnable ? L"Enable" : L"Disable"), 0, NULL, pIn, NULL, NULL); } if (pIn) pIn->Release(); if (pInClass) pInClass->Release(); pObj->Release(); } pEnum->Release(); } pSvc->Release(); pLoc->Release(); CoUninitialize(); return hr; }逻辑说明:通过WQL查询指定名称的网卡实例,然后取到WMI对象路径__PATH,最后调用Disable/Enable方法。方法无入参,所以SpawnInstance出来的对象直接传给ExecMethod即可。
参数说明:NetConnectionID是控制面板里看到的“以太网”,不一定等于适配器名。中文系统可能叫“以太网 2”或者“本地连接”。更稳的做法是先执行SELECT * FROM Win32_NetworkAdapter WHERE PhysicalAdapter=True枚举一遍,打印出所有NetConnectionID,再让用户选。
提示:调用Disable之后网络会话会立刻断掉,如果程序自身依赖网络下载,则必须在调用前把后续逻辑缓存起来。我习惯用CreateProcess调用一个延迟5秒的批处理来完成禁用,留出脚本刷新的间隔。
3.3 用IP Helper API删除默认路由
只禁网卡还有一种漏网情况:机器上有多个网卡,比如有线加4G模块,禁掉以太网后4G Dongle会立刻接管。所以还需要删除默认路由。用命令行最简单:
route delete 0.0.0.0 mask 0.0.0.0在VC++里可以直接调用iphlpapi函数,避免拉起进程。
#include <iphlpapi.h> #pragma comment(lib, "iphlpapi.lib") BOOL ClearDefaultRoute() { DWORD dwSize = 0; if (GetIpForwardTable(NULL, &dwSize, FALSE) != ERROR_INSUFFICIENT_BUFFER) return FALSE; PMIB_IPFORWARDTABLE pTable = (PMIB_IPFORWARDTABLE)new BYTE[dwSize]; if (GetIpForwardTable(pTable, &dwSize, FALSE) != NO_ERROR) { delete[] pTable; return FALSE; } for (DWORD i = 0; i < pTable->dwNumEntries; i++) { if (pTable->table[i].dwForwardDest == 0 && pTable->table[i].dwForwardMask == 0) { DeleteIpForwardEntry(&pTable->table[i]); } } delete[] pTable; return TRUE; }说明:这段代码遍历IPv4路由表,把所有目标地址和掩码都是0的默认路由删掉。它只影响需要走外网的流量,本地局域网通信不受影响。恢复时需要把默认网关加回来,最好提前用GetIpForwardTable把原条目保存到INI或注册表里,否则恢复时只能靠自动DHCP重新下发。
3.4 用netsh命令封装Windows防火墙规则
为了保留远程管理,推荐先加一条允许远程桌面的规则,再加一条阻止所有出站的规则。VC++里用CreateProcess执行netsh,不直接碰COM代码,可读性和可维护性都好很多。
void ExecNetsh(LPCTSTR szArgs) { TCHAR szCmd[512] = {0}; wsprintf(szCmd, _T("netsh %s"), szArgs); STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; if (CreateProcess(NULL, szCmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } } void ApplyFirewallBlock() { ExecNetsh(_T("advfirewall firewall delete rule name=\"BlockAllOut\"")); ExecNetsh(_T("advfirewall firewall add rule name=\"BlockAllOut\" dir=out action=block enable=yes")); } void AllowRemoteDesktop() { ExecNetsh(_T("advfirewall firewall add rule name=\"Allow_RDP\" dir=out action=allow protocol=TCP localport=3389 remoteip=any enable=yes")); }逻辑说明:dir=out action=block会阻止所有匹配该规则的外出连接,规则优先级低于允许规则,所以先建一条allow规则再建block规则,允许规则优先放行3389。enable=yes确保规则在运行时生效。
参数说明:如果只想阻止员工访问外网但允许内网,可以在add rule里加remoteip=local subnet,把外网IP段排除在外。规则名称用固定值,方便后续delete rule name=精确删除,不会误删用户自建规则。
4. 把禁用USB接口和禁止上网功能整合成一个VC++管理工具
4.1 控制台程序加命令行参数,比MFC弹窗更适合运维
这个工具的使用者是管理员自己,不是普通用户,用MFC对话框反而累赘。我建议做成控制台程序,定义两个动作:restrict执行限制,restore恢复。这样能直接拼进CMDB脚本或计划任务。代码入口如下。
int wmain(int argc, wchar_t* argv[]) { if (argc < 2) { wprintf(L"Usage: UsbNetGuard.exe [restrict|restore]\n"); return 1; } if (lstrcmpi(argv[1], L"restrict") == 0) return ApplyRestriction(TRUE); else if (lstrcmpi(argv[1], L"restore") == 0) return ApplyRestriction(FALSE); else return 1; }逻辑说明:程序根据第一个参数决定限制还是恢复。调用ApplyRestriction这个函数统一执行前面写的USB和网络操作。相比MFC按钮,命令行方式更容易被脚本和组策略调用,也方便做自动化回归测试。
4.2 管理员权限和单实例互斥
修改注册表和WMI都需要管理员权限,所以进程启动时必须提权。使用IsUserAnAdmin判断,配合runas重新拉起自身。
#include <shellapi.h> BOOL IsRunAsAdmin() { return IsUserAnAdmin(); } void ReLaunchAsAdmin(const wchar_t* szCmdLine) { SHELLEXECUTEINFO sei = {0}; sei.cbSize = sizeof(sei); sei.lpVerb = L"runas"; sei.lpFile = L"UsbNetGuard.exe"; sei.lpParameters = szCmdLine; sei.nShow = SW_HIDE; ShellExecuteEx(&sei); }再创建一个全局互斥体,防止多个实例同时写注册表产生竞态。
int EnsureSingleInstance() { HANDLE hMutex = CreateMutex(NULL, TRUE, L"Global\\UsbNetGuard"); if (!hMutex || GetLastError() == ERROR_ALREADY_EXISTS) return 0; return 1; }4.3 统一执行顺序:先USB后网络
执行顺序有讲究。一次常规限制操作,我会按这个顺序来:
- 设置DeviceInstall设备安装策略,拒绝新的USB设备。
- 设置USBSTOR服务为禁用。
- 删除USBSTOR的Enum残留。
- 添加防火墙出站阻止规则。
- 删除默认路由。
这里先禁USB再断网,是为了防止在网络已经断开的情况下,有人把U盘插入后驱动没被及时拦下。驱动安装策略必须最早生效。恢复顺序则反过来,先恢复网络,再恢复USB。
int ApplyRestriction(BOOL bRestrict) { if (bRestrict) { SetDeviceInstallRestrictions(TRUE); SetUsbStorageState(TRUE); ClearUsbStorageEnum(); AllowRemoteDesktop(); ApplyFirewallBlock(); ClearDefaultRoute(); } else { SetDeviceInstallRestrictions(FALSE); SetUsbStorageState(FALSE); ExecNetsh(_T("advfirewall firewall delete rule name=\"BlockAllOut\"")); SetAdapterOnline(L"以太网", TRUE); } return 0; }参数说明:AllowRemoteDesktop()要先于ApplyFirewallBlock()执行,否则阻塞规则生效后,后续创建进程都不再访问网络,但本机加规则不受影响,只是顺序清晰更易于阅读。恢复时没有调用ClearUsbStorageEnum,因为USBSTOR驱动重新启用后系统会重新生成枚举信息。
4.4 原始状态备份:避免恢复时把本已禁用的设备又打开
恢复操作最怕的就是把设备原来就是禁用的状态,误改成启用。所以在限制前要把USBSTOR的Start值和网络适配器状态记录到注册表或本地文件。这里给一个最简单的记录方式。
void SaveOriginalState() { HKEY hKey; RegCreateKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\UsbNetGuard", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL); DWORD dwUsbStart = 3; DWORD dwSize = sizeof(dwUsbStart); RegGetValue(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services\\USBSTOR", L"Start", RRF_RT_REG_DWORD, NULL, &dwUsbStart, &dwSize); RegSetValueEx(hKey, L"UsbStart", 0, REG_DWORD, (BYTE*)&dwUsbStart, sizeof(dwUsbStart)); // TODO: 在这里把当前网卡列表Name写入 RegCloseKey(hKey); }这样恢复时读UsbStart变量的值再写回USBSTOR,避免把原本禁用状态的U盘服务打开。网卡的状态备份可以用WMI枚举所有PhysicalAdapter,记录其NetConnectionID和NetEnabled属性到文件中,恢复时只启用备份时标记为启用过的网卡。
5. 验证禁用USB接口和禁止上网功能时容易踩的三个错误
5.1 验证USB禁用的三个层次
第一层看设备管理器:执行限制后插入U盘,如果看到“大容量存储设备”带黄色感叹号,说明PnP层已经拒绝;如果设备正常出现但盘符出不来,说明是USBSTOR驱动层被禁。两者都算正确。
第二层看是否存在残留盘符:把USBSTOR\Enum删除后,原盘符不会立刻消失,需要重启或手动重启设备管理器。验证时最好记录插入U盘前的盘符列表,执行后再对比。
第三层看总线抓包:用USBPcap抓USB总线,如果能看到设备的GET_DESCRIPTOR请求,但后续没有BOT/SCSI命令,表示物理连接正常,是驱动层拒绝。这是正常现象,不是失败。
5.2 验证禁止上网是否误伤内网
最简单的方法:在限制前后分别执行ping 内网网关和ping 114.114.114.114。限制后内网网关通、外网超时,说明默认路由或防火墙规则生效。如果再执行route print看到目标0.0.0.0的路由消失,那是路由删除生效;如果路由还在但外网不通,则是防火墙规则生效。
还有一个常见错误是只删除默认路由,但DHCP客户端在几分钟后又会重新申请到默认网关。所以在方案里必须配合防火墙规则出站阻止,或者把DHCP客户端的注册表租约缓存清掉。我一般会两套都做,单靠路由不可靠。
5.3 错误码和恢复口令
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 0x5 拒绝访问 | 没有提权或注册表权限不足 | 用runas提权,或检查是否SYSTEM账户运行 |
| 0x540 网络适配器不存在 | WQL名称与系统NetConnectionID不匹配 | 枚举一次所有网卡名称,动态匹配 |
| 0x57 参数错误 | WMI方法入参或对象路径错误 | 检查__PATH字符串是否完整 |
| USB残留盘符 | Enum未删除或DenyDeviceIDsRetroactive=0 | 删除Enum子键并重启设备管理器 |
| 防火墙规则不生效 | 未指定profile或组策略覆盖 | 显式加profiles=any,再检查组策略 |
| 恢复后上网仍然失败 | 默认路由没恢复 | 执行ipconfig /renew或手动加网关 |
建议在工具里加一个隐藏参数status,输出当前关键注册表值和网卡状态,方便分发给远程管理员排查。用退避等待策略,禁用USB接口后隔2秒再清Enum,避免设备枚举正在进行时删除注册表导致蓝屏。
本文还有配套的精品资源,点击获取