I'm writing a remote control application in C# for a suite of tech support tools. Everything works fine except that I'm unable to use SendInput to the Winlogon desktop. I'm successfully detecting the change from Default to Winlogon, and I'm able to switch to it and capture screenshots. It's just not accepting SendInput functions. I know it's possible because TeamViewer does it, and they don't have uiAccess=true in their manifest. They seem to be using the same process I am.
Here's what I'm doing in a nutshell: Install service. Service listens for connection request. Service launches new process in user's session with CreateProcessAsUser and a duplicated access token from winlogon.exe. Viewer connects to new process.
Can anyone identify what's missing to give the new process access to SendInput to winlogon? Here's the code I'm using to launch the new process from the service. The next is code I'm using to detect the change to Winlogon desktop and switch to it.
public static bool OpenProcessAsSystem(string applicationName, out PROCESS_INFORMATION procInfo)
{
try
{
uint winlogonPid = 0;
IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;
procInfo = new PROCESS_INFORMATION();
// Obtain session ID for active session.
uint dwSessionId = Kernel32.WTSGetActiveConsoleSessionId();
// Check for RDP session. If active, use that session ID instead.
var rdpSessionID = GetRDPSession();
if (rdpSessionID > 0)
{
dwSessionId = rdpSessionID;
}
// Obtain the process ID of the winlogon process that is running within the currently active session.
Process[] processes = Process.GetProcessesByName("winlogon");
foreach (Process p in processes)
{
if ((uint)p.SessionId == dwSessionId)
{
winlogonPid = (uint)p.Id;
}
}
// Obtain a handle to the winlogon process.
hProcess = Kernel32.OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);
// Obtain a handle to the access token of the winlogon process.
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
{
Kernel32.CloseHandle(hProcess);
return false;
}
// Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser.
SECURITY_ATTRIBUTES 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.
if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref 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.
STARTUPINFO si = new STARTUPINFO();
si.cb = (int)Marshal.SizeOf(si);
si.lpDesktop = @"winsta0\default"; // interactive window station parameter; basically this indicates that the process created can display a GUI on the desktop
// flags that specify the priority and creation method of the process
uint dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
// create a new process in the current user's logon session
bool result = CreateProcessAsUser(hUserTokenDup, // client's access token
null, // file to execute
applicationName, // command line
ref sa, // pointer to process SECURITY_ATTRIBUTES
ref sa, // pointer to thread SECURITY_ATTRIBUTES
false, // handles are not inheritable
dwCreationFlags, // creation flags
IntPtr.Zero, // pointer to new environment block
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out procInfo // receives information about new process
);
// invalidate the handles
Kernel32.CloseHandle(hProcess);
Kernel32.CloseHandle(hPToken);
Kernel32.CloseHandle(hUserTokenDup);
return result;
}
catch
{
procInfo = new PROCESS_INFORMATION() { };
return false;
}
}
public static uint GetRDPSession()
{
IntPtr ppSessionInfo = IntPtr.Zero;
Int32 count = 0;
Int32 retval = WTSAPI32.WTSEnumerateSessions(WTSAPI32.WTS_CURRENT_SERVER_HANDLE, 0, 1, ref ppSessionInfo, ref count);
Int32 dataSize = Marshal.SizeOf(typeof(WTSAPI32.WTS_SESSION_INFO));
var sessList = new List<WTSAPI32.WTS_SESSION_INFO>();
Int64 current = (int)ppSessionInfo;
if (retval != 0)
{
for (int i = 0; i < count; i++)
{
WTSAPI32.WTS_SESSION_INFO sessInf = (WTSAPI32.WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WTSAPI32.WTS_SESSION_INFO));
current += dataSize;
sessList.Add(sessInf);
}
}
uint retVal = 0;
var rdpSession = sessList.Find(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0);
if (sessList.Exists(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0))
{
retVal = (uint)rdpSession.SessionID;
}
return retVal;
}
Here's what I'm using to capture the screen, detect the desktop change, and switch to it.
var hWnd = User32.GetDesktopWindow();
var hDC = User32.GetWindowDC(hWnd);
var graphDC = graphic.GetHdc();
var copyResult = GDI32.BitBlt(graphDC, 0, 0, totalWidth, totalHeight, hDC, 0, 0, GDI32.TernaryRasterOperations.SRCCOPY | GDI32.TernaryRasterOperations.CAPTUREBLT);
// Change to input desktop if copy fails.
if (!copyResult)
{
var inputDesktop = User32.OpenInputDesktop();
if (User32.SetThreadDesktop(inputDesktop) == false)
{
graphic.Clear(System.Drawing.Color.White);
var font = new Font(FontFamily.GenericSansSerif, 30, System.Drawing.FontStyle.Bold);
graphic.DrawString("Waiting for screen capture...", font, Brushes.Black, new PointF((totalWidth / 2), totalHeight / 2), new StringFormat() { Alignment = StringAlignment.Center });
var error = Marshal.GetLastWin32Error();
writeToErrorLog(new Exception("Failed to open input desktop. Error: " + error.ToString()));
}
var dw = User32.GetDesktopWindow();
User32.SetActiveWindow(dw);
User32.SetForegroundWindow(dw);
User32.CloseDesktop(inputDesktop);
}
graphic.ReleaseHdc(graphDC);
User32.ReleaseDC(hWnd, hDC);
I got SendInput to work on the logon desktop (and, as it turns out, the UAC secure desktop). SetThreadDesktop must not give you the same privileges as if you'd initially started the process in the target desktop.
So when I detected a desktop change, instead of calling SetThreadDesktop, I launched yet another process in the new desktop with CreateProcessAsUser. Then I signaled for the viewer to switch and closed the current process.
Edit (years later): I ended up being wrong about this. You just need ensure your current thread doesn't have any open windows or hooks in the current desktop. And since this only sets the desktop for the calling thread (not the process), other threads will need to call this as well.