C# Windows登录界面进行截图,控制鼠标键盘等操作实现(二)
上一篇:C# Windows登录界面进行截图,控制鼠标键盘等操作实现(一) - log9527 - 博客园 (cnblogs.com) 我们了解了要实现在Windows登录界面进行截图等操作必须满足的条件,这一篇我们主要通过代码实现这些条件。
首先先建一个项目A
下面一些windows自带非托管dll的调用类。
/// <summary> /// WtsApi32.dll导入帮助类 /// </summary> public static class WtsApi32 { [StructLayout(LayoutKind.Sequential)] public struct WtsSessionInfo { public int SessionID; [MarshalAs(UnmanagedType.LPStr)] public string pWinStationName; public WtsConnectStateClass State; } public enum WtsConnectStateClass { WtsActive, WtsConnected, WtsConnectQuery, WtsShadow, WtsDisconnected, WtsIdle, WtsListen, WtsReset, WtsDown, WtsInit } public static IntPtr WtsCurrentServerHandle = IntPtr.Zero; [DllImport("wtsapi32.dll", SetLastError = true)] public static extern int WTSEnumerateSessions( IntPtr hServer, int reserved, int version, ref IntPtr ppSessionInfo, ref int pCount); }
/// <summary> /// user32.dll导入帮助类 /// </summary> public static class User32 { #region Constants public const int DesktopCapabilityIndex = 118; #endregion #region Enums [Flags] public enum AccessMask : uint { Delete = 0x00010000, ReadControl = 0x00020000, WriteDac = 0x00040000, WriteOwner = 0x00080000, Synchronize = 0x00100000, StandardRightsRequired = 0x000F0000, StandardRightsRead = 0x00020000, StandardRightsWrite = 0x00020000, StandardRightsExecute = 0x00020000, StandardRightsAll = 0x001F0000, SpecificRightsAll = 0x0000FFFF, AccessSystemSecurity = 0x01000000, MaximumAllowed = 0x02000000, GenericRead = 0x80000000, GenericWrite = 0x40000000, GenericExecute = 0x20000000, GenericAll = 0x10000000, DesktopReadObjects = 0x00000001, DesktopCreateWindow = 0x00000002, DesktopCreateMenu = 0x00000004, DesktopHookcontrol = 0x00000008, DesktopJournalrecord = 0x00000010, DesktopJournalplayback = 0x00000020, DesktopEnumerate = 0x00000040, DesktopWriteobjects = 0x00000080, DesktopSwitchdesktop = 0x00000100, WinstaEnumdesktops = 0x00000001, WinstaReadattributes = 0x00000002, WinstaAccessclipboard = 0x00000004, WinstaCreatedesktop = 0x00000008, WinstaWriteattributes = 0x00000010, WinstaAccessglobalatoms = 0x00000020, WinstaExitwindows = 0x00000040, WinstaEnumerate = 0x00000100, WinstaReadscreen = 0x00000200, WinstaAllAccess = 0x0000037F } #endregion #region DLL Imports [DllImport("gdi32.dll")] public static extern int GetDeviceCaps( IntPtr hdc, // handle to DC int nIndex // index of capability ); [DllImport("user32.dll")] public static extern IntPtr GetDesktopWindow(); [DllImport("user32.dll", SetLastError = true)] public static extern bool SwitchDesktop(IntPtr hDesktop); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr OpenInputDesktop(uint dwFlags, bool fInherit, AccessMask dwDesiredAccess); [return: MarshalAs(UnmanagedType.Bool)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool CloseWindowStation(IntPtr hWinsta); [DllImport("User32.dll")] public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport("user32.dll", SetLastError = true)] public static extern bool SetThreadDesktop(IntPtr hDesktop); [DllImport("user32.dll", SetLastError = true)] public static extern bool CloseDesktop(IntPtr hDesktop); [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hWnd); #endregion }
/// <summary> /// 非托管dll引入帮助类 /// </summary> public static class Kernel32 { #region DLL Imports [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hSnapshot); [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); [DllImport("kernel32.dll")] public static extern bool ProcessIdToSessionId(uint dwProcessId, ref uint pSessionId); [DllImport("kernel32.dll")] public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); #endregion }
/// <summary> /// 非托管dll引入帮助类 /// </summary> public static class AdvApi32 { #region Structs [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public int Length; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct STARTUP_INFO { public Int32 cb; public string lpReserved; public string lpDesktop; public string lpTitle; public Int32 dwX; public Int32 dwY; public Int32 dwXSize; public Int32 dwYSize; public Int32 dwXCountChars; public Int32 dwYCountChars; public Int32 dwFillAttribute; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } #endregion #region Enums public enum TOKEN_TYPE : int { TokenPrimary = 1, TokenImpersonation = 2 } public enum SECURITY_IMPERSONATION_LEVEL : int { SecurityAnonymous = 0, SecurityIdentification = 1, SecurityImpersonation = 2, SecurityDelegation = 3, } #endregion #region Constants public const int TOKEN_DUPLICATE = 0x0002; public const uint MAXIMUM_ALLOWED = 0x2000000; public const int CREATE_NEW_CONSOLE = 0x00000010; public const int CREATE_NO_WINDOW = 0x08000000; public const int DETACHED_PROCESS = 0x00000008; public const int NORMAL_PRIORITY_CLASS = 0x20; public const int UOI_NAME = 2; #endregion #region DLL Imports [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUP_INFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool AllocateLocallyUniqueId(out IntPtr pLuid); [DllImport("advapi32", SetLastError = true), SuppressUnmanagedCodeSecurityAttribute] public static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, ref IntPtr TokenHandle); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public extern static bool DuplicateTokenEx( IntPtr hExistingToken, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out IntPtr phNewToken); [DllImport("user32.dll", SetLastError = true)] public static extern bool GetUserObjectInformationW(IntPtr hObj, int nIndex, [Out] byte[] pvInfo, uint nLength, out uint lpnLengthNeeded); #endregion }
外部调用入口类为Win32Interop,包含创建桌面会话进程、将当前进程切换到Desktop等主要功能
/// <summary> /// 切换会话状态帮助类 /// </summary> public class Win32Interop { [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); /// <summary> /// 获取当前桌面名称 /// </summary> /// <returns></returns> public static string GetCurrentDesktop() { // 打开接收用户输入的桌面。 var inputDesktop = OpenInputDesktop(); var deskBytes = new byte[256]; // 检索有关指定窗口站或桌面对象的信息。(这里检索名字) var success = GetUserObjectInformationW(inputDesktop, UOI_NAME, deskBytes, 256, out var lenNeeded); if (!success) { CloseDesktop(inputDesktop); return "Default"; } // 返回窗口站或桌面的名字 var desktopName = Encoding.Unicode.GetString(deskBytes.Take((int)lenNeeded).ToArray()).Replace("\0", ""); // 关闭桌面对象的打开句柄。 CloseDesktop(inputDesktop); return desktopName; } /// <summary> /// 获取会话列表 /// </summary> /// <returns></returns> public static IEnumerable<WtsApi32.WtsSessionInfo> EnumerateSessions() { var ppSessionInfo = IntPtr.Zero; var count = 0; // 检索远程桌面会话主机(RD 会话主机)服务器上的会话列表。 var retrieval = WtsApi32.WTSEnumerateSessions(WtsApi32.WtsCurrentServerHandle, 0, 1, ref ppSessionInfo, ref count); var dataSize = Marshal.SizeOf(typeof(WtsApi32.WtsSessionInfo)); var current = (long)ppSessionInfo; if (retrieval == 0) yield break; for (var i = 0; i < count; i++) { var sessionInf = (WtsApi32.WtsSessionInfo)Marshal.PtrToStructure((System.IntPtr)current, typeof(WtsApi32.WtsSessionInfo)); current += dataSize; yield return sessionInf; } } /// <summary> /// 获取Rdp会话 /// </summary> /// <returns></returns> public static uint GetRdpSession() { // 返回远程桌面会话主机(RD 会话主机)服务器上的会话列表信息 var sessionList = EnumerateSessions(); uint retVal = 0; // pWinStationName 指向包含此会话的 WinStation 名称的 null 终止字符串的指针。 WinStation 名称是 Windows 与会话关联的名称,例如“services”、“console”或“RDP-Tcp#0”。 // 「rdp-tcp#0」和「console」是两种不同的远程桌面协议(Remote Desktop Protocol)的连接方式。 「rdp-tcp#0」是使用TCP 协议的RDP 连接,它可以通过网络与远程主机进行连接。 「console」则代表通过本地控制台连接,它只能在本地机器上直接连接到远程主机。 var wtsSessionInfos = sessionList.ToList(); var rdpSession = wtsSessionInfos.FirstOrDefault(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0); if (wtsSessionInfos.Any(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0)) { retVal = (uint)rdpSession.SessionID; } return retVal; } /// <summary> /// 打开输入桌面 /// </summary> /// <returns></returns> public static IntPtr OpenInputDesktop() { return User32.OpenInputDesktop(0, false, AccessMask.GenericAll); } /// <summary> /// 创建桌面会话进程 /// </summary> /// <param name="applicationName"></param> /// <param name="desktopName"></param> /// <param name="hiddenWindow"></param> /// <param name="dwSessionId"></param> /// <param name="procInfo"></param> /// <returns></returns> public static bool OpenInteractiveProcess(string applicationName, string desktopName, bool hiddenWindow, uint dwSessionId, out PROCESS_INFORMATION procInfo) { uint winlogonPid = 0; var hPToken = IntPtr.Zero; procInfo = new PROCESS_INFORMATION(); // Obtain the process ID of the winlogon process that is running within the currently active session. var processes = Process.GetProcessesByName("winlogon"); foreach (var p in processes) { if ((uint)p.SessionId == dwSessionId) { winlogonPid = (uint)p.Id; } } // Obtain a handle to the winlogon process. // 打开现有的本地进程对象。 var hProcess = Kernel32.OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid); // Obtain a handle to the access token of the winlogon process. // OpenProcessToken函数打开与进程关联的访问令牌。 if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken)) { Kernel32.CloseHandle(hProcess); return false; } // Security attribute structure used in DuplicateTokenEx and CreateProcessAsUser. var sa = new SECURITY_ATTRIBUTES(); sa.Length = Marshal.SizeOf(sa); // Copy the access token of the winlogon process; the newly created token will be a primary token. // DuplicateTokenEx函数创建一个新的访问令牌来复制现有令牌。此函数可以创建主令牌或模拟令牌。 if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, TOKEN_TYPE.TokenPrimary, out var hUserTokenDup)) { Kernel32.CloseHandle(hProcess); Kernel32.CloseHandle(hPToken); return false; } // By default, CreateProcessAsUser creates a process on a non-interactive window station, meaning // the window station has a desktop that is invisible and the process is incapable of receiving // user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user // interaction with the new process. var si = new STARTUP_INFO(); si.cb = Marshal.SizeOf(si); si.lpDesktop = @"winsta0\" + desktopName; // Flags that specify the priority and creation method of the process. uint dwCreationFlags; if (hiddenWindow) { dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW | DETACHED_PROCESS; } else { dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE; } // Create a new process in the current user's logon session. // 创建一个新进程及其主线程。新进程在指定令牌表示的用户的安全上下文中运行。 // 通常,调用CreateProcessAsUser函数的进程 必须具有SE_INCREASE_QUOTA_NAME权限,并且如果令牌不可分配,则可能需要SE_ASSIGNPRIMARYTOKEN_NAME权限。如果此函数失败并出现ERROR_PRIVILEGE_NOT_HELD(1314),请改用CreateProcessWithLogonW函数。CreateProcessWithLogonW不需要特殊权限,但必须允许指定的用户帐户以交互方式登录。通常,最好使用CreateProcessWithLogonW创建具有备用凭据的进程。 var result = CreateProcessAsUser(hUserTokenDup, null, applicationName, ref sa, ref sa, false, dwCreationFlags, IntPtr.Zero, null, ref si, out procInfo); // Invalidate the handles. Kernel32.CloseHandle(hProcess); Kernel32.CloseHandle(hPToken); Kernel32.CloseHandle(hUserTokenDup); return result; } /// <summary> /// 将当前进程切换到Desktop /// </summary> public static void SwitchToInputDesktop() { var inputDesktop = OpenInputDesktop(); SwitchDesktop(inputDesktop); SetThreadDesktop(inputDesktop); CloseDesktop(inputDesktop); } }
然后再建一个新项目B,用于启动一个新进程进行截图。进程名就叫WinLogonScreenShot。
项目A中继续写以下代码用于启动项目B中的进程WinLogonScreenShot:
/// <summary> /// 创建新进程 /// </summary> /// <returns></returns> internal bool Create() { try { // 返回值为uint类型,不要写int类型 uint dwSessionId = Win32Interop.WTSGetActiveConsoleSessionId(); Log.Info($"检索控制台会话的会话标识符句柄:{dwSessionId}"); var desktopName = Win32Interop.GetCurrentDesktop(); Log.Info($"返回窗口站或桌面的名字:{desktopName}"); // 返回rdpSession会话id var rdpSessionId = Win32Interop.GetRdpSession(); Log.Info($"返回rdpSession会话id:{rdpSessionId}"); if (rdpSessionId > 0) { if (dwSessionId != rdpSessionId) desktopName = "winlogon"; dwSessionId = rdpSessionId; } Log.Info($"desktopName:{desktopName};dwSessionId:{dwSessionId}"); var res = CreateProcessAsUser(dwSessionId, desktopName, _winLogonType); Log.Info($"CreateProcessAsUser:{res}"); Log.Info($"Service started."); return res; } catch (Exception ex) { ServiceCenters.Log.Error(ex); return false; } } /// <summary> /// 守护会话ID>0的进程 /// </summary> /// <returns></returns> internal async Task StartMonitorAsync() { await Task.Run(MonitorProcess); } /// <summary> /// 守护会话ID>0的进程 /// </summary> private async Task MonitorProcess() { while (true) { try { var process = Process.GetProcessesByName("WinLogonScreenShot"); if (process.Length == 0) { Log.Info($"创建会话ID=1的新进程"); var activeSessionId = Kernel32.WTSGetActiveConsoleSessionId(); //获取活动会话 Log.Info($"检索控制台会话的会话标识符句柄:{activeSessionId}"); var desktop = Win32Interop.GetCurrentDesktop(); var isOk = CreateProcessAsUser(activeSessionId, desktop, _winLogonType); Log.Info($"新建进程结果:{isOk}"); } await Task.Delay(1000); } catch (Exception ex) { ServiceCenters.Log.Error(ex); } } } /// <summary> /// 创建用户进程 /// </summary> /// <param name="dwSessionId"></param> /// <param name="desktopName"></param> /// <param name="type"></param> /// <returns></returns> private bool CreateProcessAsUser(uint dwSessionId, string desktopName, WinLogonType type) {//用户进程启动 var result = Win32Interop.OpenInteractiveProcess("WinLogonScreenShot", desktopName, true, dwSessionId, out _); return result; }
需要把项目B生成到项目A运行目录下。上面就是创建一个满足在登录界面截图的进程WinLogonScreenShot,并且守护该进程的操作。
然后在项目A中截图/控制鼠标键盘等操作前,必须先调将当前进程切换到Desktop的方法
Win32Interop.SwitchToInputDesktop();