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();

 

posted @ 2023-08-17 16:41  log9527  阅读(293)  评论(0编辑  收藏  举报