如何通过C#获取USB设备插拔时间?解决注册表权限不足问题
解决USB设备最后插拔时间的权限问题(C#实现)
你遇到的问题是管理员权限无法访问注册表中USB设备的Properties子项,需要SYSTEM权限才能读取。以下是几种可行的解决方案:
一、优先方案:用SetupAPI获取插拔时间(无需SYSTEM权限)
不用纠结注册表权限问题,直接通过Windows的SetupAPI获取设备的最后安装/插拔时间,管理员权限即可访问,代码更简洁可靠。
代码示例
using System; using System.Runtime.InteropServices; using System.Text; public static class USBDeviceHelper { [DllImport("setupapi.dll", SetLastError = true)] private static extern IntPtr SetupDiGetClassDevs(ref Guid classGuid, string enumerator, IntPtr hwndParent, uint flags); [DllImport("setupapi.dll", SetLastError = true)] private static extern bool SetupDiEnumDeviceInfo(IntPtr deviceInfoSet, uint memberIndex, ref SP_DEVINFO_DATA deviceInfoData); [DllImport("setupapi.dll", SetLastError = true)] private static extern bool SetupDiGetDeviceRegistryProperty(IntPtr deviceInfoSet, ref SP_DEVINFO_DATA deviceInfoData, uint property, out uint propertyRegDataType, StringBuilder propertyBuffer, uint propertyBufferSize, out uint requiredSize); [DllImport("setupapi.dll", SetLastError = true)] private static extern bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet); [StructLayout(LayoutKind.Sequential)] private struct SP_DEVINFO_DATA { public int cbSize; public Guid classGuid; public uint devInst; public IntPtr reserved; } // USB设备类GUID private static readonly Guid USB_CLASS_GUID = new Guid("{36FC9E60-C465-11CF-8056-444553540000}"); // 最后安装/插拔时间属性ID private const uint SPDRP_LAST_INSTALL_DATE = 0x19; // 枚举标志:所有类+已连接设备 private const uint DIGCF_FLAGS = 0x4 | 0x2; public static void FetchUSBDeviceLastPlugTime() { var deviceInfoSet = SetupDiGetClassDevs(ref USB_CLASS_GUID, null, IntPtr.Zero, DIGCF_FLAGS); if (deviceInfoSet == IntPtr.Zero) { Console.WriteLine($"获取设备列表失败: {Marshal.GetLastWin32Error()}"); return; } var devInfoData = new SP_DEVINFO_DATA { cbSize = Marshal.SizeOf<SP_DEVINFO_DATA>() }; uint index = 0; while (SetupDiEnumDeviceInfo(deviceInfoSet, index, ref devInfoData)) { var buffer = new StringBuilder(256); uint regType = 0; uint reqSize = 0; if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, ref devInfoData, SPDRP_LAST_INSTALL_DATE, out regType, buffer, (uint)buffer.Capacity, out reqSize)) { // 可添加读取SPDRP_FRIENDLYNAME属性获取设备名称的逻辑 Console.WriteLine($"最后插拔时间: {buffer}"); } else { Console.WriteLine($"读取时间失败: {Marshal.GetLastWin32Error()}"); } index++; } SetupDiDestroyDeviceInfoList(deviceInfoSet); } }
说明
SPDRP_LAST_INSTALL_DATE属性返回的是设备最后被安装(包括插拔)的时间字符串,格式为yyyyMMddHHmmss.ffffff+UUU。- 该方法仅需要管理员权限,无需SYSTEM身份,兼容性更好。
二、通过SYSTEM身份访问注册表
如果必须读取注册表的Properties子项,需要让代码以SYSTEM身份运行,常见的实现方式有两种:
方式1:将程序注册为Windows服务
Windows服务默认以SYSTEM账户运行,直接在服务中编写注册表读取逻辑即可:
- 创建Windows服务项目,在
OnStart方法中添加注册表读取代码。 - 安装服务后启动,即可直接访问受限的注册表项。
方式2:临时创建SYSTEM权限进程(适合桌面应用)
通过API获取SYSTEM令牌,创建子进程执行注册表读取任务,主进程通过临时文件、命名管道等方式获取结果。
代码示例(创建SYSTEM进程)
using System; using System.Diagnostics; using System.Runtime.InteropServices; public static class SystemPrivilegeRunner { [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken(IntPtr hProcess, uint dwDesiredAccess, out IntPtr hToken); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, int impersonationLevel, int tokenType, out IntPtr hNewToken); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [StructLayout(LayoutKind.Sequential)] private struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } private const uint TOKEN_QUERY = 0x8; private const uint TOKEN_DUPLICATE = 0x2; private const uint TOKEN_ASSIGN_PRIMARY = 0x1; private const int SECURITY_IMPERSONATION_LEVEL = 2; private const int TOKEN_TYPE = 1; private const uint CREATE_NO_WINDOW = 0x8000000; public static bool RunAsSystem(string exePath, string args) { // 获取winlogon进程的SYSTEM令牌(winlogon默认以SYSTEM运行) var winlogon = Process.GetProcessesByName("winlogon")[0]; if (!OpenProcessToken(winlogon.Handle, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, out var systemToken)) { Console.WriteLine($"获取SYSTEM令牌失败: {Marshal.GetLastWin32Error()}"); return false; } if (!DuplicateTokenEx(systemToken, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL, TOKEN_TYPE, out var primaryToken)) { Console.WriteLine($"复制令牌失败: {Marshal.GetLastWin32Error()}"); Marshal.CloseHandle(systemToken); return false; } var startupInfo = new STARTUPINFO { cb = Marshal.SizeOf<STARTUPINFO>(), lpDesktop = "WinSta0\\Default" }; if (!CreateProcessAsUser(primaryToken, exePath, $"{exePath} {args}", IntPtr.Zero, IntPtr.Zero, false, CREATE_NO_WINDOW, IntPtr.Zero, null, ref startupInfo, out var procInfo)) { Console.WriteLine($"创建SYSTEM进程失败: {Marshal.GetLastWin32Error()}"); Marshal.CloseHandle(primaryToken); Marshal.CloseHandle(systemToken); return false; } // 释放资源 Marshal.CloseHandle(primaryToken); Marshal.CloseHandle(systemToken); Marshal.CloseHandle(procInfo.hProcess); Marshal.CloseHandle(procInfo.hThread); return true; } }
说明
- 主程序需要以管理员权限运行,才能获取winlogon进程的令牌。
- 子进程(如单独的控制台程序)负责读取注册表
Properties子项,将结果写入临时文件,主进程读取该文件获取数据。
三、权限相关说明
- 管理员权限并非Windows最高权限,SYSTEM账户拥有系统核心资源的访问权限,主要用于系统服务和核心进程。
- 不要随意让桌面程序以SYSTEM身份运行,会带来安全风险,优先选择SetupAPI的方案。
内容的提问来源于stack exchange,提问作者Antonio Argentieri
相关产品推荐
相关产品推荐

