上一篇: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();