OpenProcessToken fails when querying process running as NETWORK SERVICE - c#

I have code which needs to query the process token (specifically the SID) of several processes, at least one of which is running as the built-in NETWORK SERVICE account. I'm using .NET to get the process ID, and then p/invoke to advapi32 methods to open the process and the token. All of this code runs as an administrator on the machine.
The issue is that I get ERROR_ACCESS_DENIED (return code 5) from OpenProcessToken every time.
What am I doing wrong? This must be possible (for example, Process Explorer shows the SID and other token information of the target process), so I figure I'm missing something. One thought is that perhaps I need SeDebugPrivilege. (EDIT: Doesn't work -- doing Process.EnterDebugMode to set SeDebugPrivilege on the current thread doesn't change behavior).
My code looks like this:
Process[] procs = Process.GetProcessesByName("processname");
Process process = procs[0];
IntPtr processHandle = NativeMethods.OpenProcess(
ProcessAccessFlags.QueryInformation,
false,
process.Id);
if (
!NativeMethods.OpenProcessToken(
processHandle,
(uint)(TokenAccessLevels.Read),
out tokenHandle))
{
int err = Marshal.GetLastWin32Error();
// fails with err = 5
throw new Win32Exception(err, "Failed to get process token. Error: " + err);
}
Note: I have tried a few other combinations of the access flags on both native calls, including using the PROCESS_ALL_ACCESS.
EDIT: One additional point, querying the same process running as a local limited-access user works fine.

Related

How to run windows automation application in AWS window server

If I run window automation application in AWS window server, then SendInput or SendKeys method are not working. The message of below is recieved error message, when I call SendInput and Sendkeys method in WPF application on AWS window.
System.Exception: Some simulated input commands were not sent successfully. The most common reason for this happening are the security features of Windows including User Interface Privacy Isolation (UIPI). Your application can only send commands to applications of the same or lower elevation. Similarly certain commands are restricted to Accessibility/UIAutomation applications. Refer to the project home page and the code samples for more information.
location: WindowsInput.WindowsInputMessageDispatcher.DispatchInput(INPUT[] inputs)
location: WindowsInput.MouseSimulator.MoveMouseTo(Double absoluteX, Double absoluteY)
When the my window automation application is running without RDP Session. It crashes.
I think, It is related RDP session. If I'm connecting RDP Session, my application is working very well.
You should provide more information than "RDP session".
You are using native function, SendInput.
[DllImport("user32.dll", SetLastError = true)]
public static extern UInt32 SendInput(UInt32 numberOfInputs, INPUT[] inputs, Int32 sizeOfInputStructure);
According to Microsoft document,
The function returns the number of events that it successfully inserted into the keyboard or mouse input stream. If the function returns zero, the input was already blocked by another thread. To get extended error information, call GetLastError. This function fails when it is blocked by UIPI. Note that neither GetLastError nor the return value will indicate the failure was caused by UIPI blocking.
Please provide GetLastError result.
You should call the GetLastError function immediately when a function's return value indicates that such a call will return useful data.
public void DispatchInput(INPUT[] inputs)
{
if (inputs == null) throw new ArgumentNullException("inputs");
if (inputs.Length == 0) throw new ArgumentException("The input array was empty", "inputs");
try
{
var successful = NativeMethods.SendInput((UInt32)inputs.Length, inputs, Marshal.SizeOf(typeof (INPUT)));
}
catch
{
// if you execute any function that communicate with OS before here, LastError might be lost.
error = Marshal.GetLastWin32Error();
Console.WriteLine("The last Win32 Error was: " + error);
}
if (successful != inputs.Length)
throw new Exception("Some simulated input commands were not sent successfully. The most common reason for this happening are the security features of Windows including User Interface Privacy Isolation (UIPI). Your application can only send commands to applications of the same or lower elevation. Similarly certain commands are restricted to Accessibility/UIAutomation applications. Refer to the project home page and the code samples for more information.");
}

GetGuiResources returns 0 (error 87) or nonsense value

I'm currently coding a windows service (installed as LocalSystem) that monitors several things on a pc/server including processes. For the processes, I'm watching the memory usage and also "try" to get the number of GDI Objects per process (as can be seen in task manager).
Sadly, C# Process objects don't have the gdi count built-in so I'm using the GetGuiResources method from 'user32.dll' as shown in this example:
https://www.pinvoke.net/default.aspx/user32.getguiresources.
Basically I have a list of executable names, for each of them I use GetProcessesByName to retrieve all process instances, and then for each unique process I take the handle and send it to the function to get the Gdi objects count back.
When I try this on my local machine as a simple console app (feeding a name through Console.ReadLine), it works no problem as long as the console app is launched as administrator; i get the same numbers as task manager.
However, when the monitoring service calls this function, I get either 0s (returning error code 87) or worse: processes tied to services (no gui) return me some random numbers (12, 7, 4, etc.) when the task manager actually shows 0 (and last error = 0).
So in summary, every process that shows some GID objects in Task Manager returns 0 (error 87), and each process who has 0 returns me a number (no error, or error 183 for the monitoring service itself).
I've tried this on Windows 10, Windows Server 2012, Windows Server 2008, Windows Server 2003, Windows Server 2016. On windows 10 (my machine) I get 0s everywhere, on other OS I get the mentionned results.
Here's a shortened version of the code I use:
// Monitoring processes exeName example: ssms, sqlbrowser
List<Process> result = Process.GetProcessesByName(exeName).ToList();
if (processes != null)
{
for (int i = 0; i < processes.Count; i++)
{
int gdiCount = processes[i].GetGDIObjectsCount(); // extension method
// logging and doing stuff with gdi count here (but i get 0s or random numbers as I told)
}
}
// Process extension method
public static class CProcessExtensions
{
[DllImport("User32", SetLastError = true)]
extern private static int GetGuiResources(IntPtr hProcess, int uiFlags);
private static int GetGDICount(IntPtr processHandle)
{
if (processHandle == IntPtr.Zero)
{
return -1;
}
int count = GetGuiResources(processHandle, 0);
// Logging Marshal.GetLastWin32Error() here
return count;
}
public static int GetGDIObjectsCount(this Process process)
{
IntPtr handle;
process.Refresh();
try
{
handle = process.Handle;
}
catch (Exception ex)
{
handle = IntPtr.Zero;
}
return GetGDICount(handle);
}
}
I've also tried getting the process handles with the OpenProcess dll method but had the same results.
Anyone faced this kind of problem before?
So, thanks to Jeremy Thompson's comment leading me to info about the session 0, and with further research, I was able to solve my problem.
References:
Application Loader to launch process in another session
Wait for process exit (ProcessWaitHandle)
Get Exit code
What I did is modify the sample code from the first reference to provide a process ID (the one I want the GDI objects count of) and launch my little console app (which takes the same process ID also, and returns the GDI count as exit code) in the same session by duplicating the token of the provided process and call CreateProcessAsUser.
By launching the console app in the same session I was able to retrieve the correct info on GDI objects on every OS I previously tested except Win Server 2003, which I can totally live without.

get process mdoules c# with kernel.dll

I'm getting some trouble while running this:
public MODULEENTRY32 getModule(String ModuleName)
{
MODULEENTRY32 module32;
module32.dwSize = (uint) Marshal.SizeOf(typeof(MODULEENTRY32));
IntPtr hSnap = CreateToolhelp32Snapshot(SnapshotFlags.TH32CS_SNAPMODULE | SnapshotFlags.TH32CS_SNAPMODULE32, (uint) process.Id);
Module32First(hSnap, out module32);
if (hSnap == IntPtr.Zero)
{
return new MODULEENTRY32();
}
do
{
if (module32.szModule.Equals(ModuleName))
{
CloseHandle(hSnap);
return module32;
}
} while (Module32Next(hSnap, out module32));
return new MODULEENTRY32();
}
I was trying to get modules from a process but it always return 0,
I'm sure that the module name is corrent and the process id too
I don't think you have provided enough information to determine what the problem is.
If you read the CreateToolHelp32Snapshot documentation you should check if hSnap returned is INVALID_HANDLE_VALUE (-1). If it is, you need to call GetLastError to determine the reason for the failure.
Possible reasons for failures are documented:
If the specified process is the Idle process or one of the CSRSS
processes, this function fails and the last error code is
ERROR_ACCESS_DENIED because their access restrictions prevent
user-level code from opening them.
If the specified process is a 64-bit process and the caller is a
32-bit process, this function fails and the last error code is
ERROR_PARTIAL_COPY (299).
and:
When taking snapshots that include heaps and modules for a process
other than the current process, the CreateToolhelp32Snapshot function
can fail or return incorrect information for a variety of reasons. For
example, if the loader data table in the target process is corrupted
or not initialized, or if the module list changes during the function
call as a result of DLLs being loaded or unloaded, the function might
fail with ERROR_BAD_LENGTH or other error code. Ensure that the target
process was not started in a suspended state, and try calling the
function again. If the function fails with ERROR_BAD_LENGTH when
called with TH32CS_SNAPMODULE or TH32CS_SNAPMODULE32, call the
function again until it succeeds.

Find out whether a Process is a System Process

I am trying to find out what programs a user is running while my program is running and output them to a file. Now I'm facing the situation that when retrieving all Processes using Process.GetProcesses() I'm greeted with a list of about 269 processes which amounts to about all the Task Manager is showing, including Windows Processes like 77 svchost processes.
Now I want to filter out some system processes (At least those displayed as "Windows-Processes" in the Task Manager). Is there any way to do this or will I have to maintain a list of process names (or file directories) of all Windows Processes?
Short answer:
The solution within the taskmanager is hard coded based on the following list (taken from the Windows 10 version):
%windir%\explorer.exe
%windir%\system32\ntoskrnl.exe
%windir%\system32\WerFault.exe
%windir%\system32\backgroundTaskHost.exe
%windir%\system32\backgroundTransferHost.exe
%windir%\system32\winlogon.exe
%windir%\system32\wininit.exe
%windir%\system32\csrss.exe
%windir%\system32\lsass.exe
%windir%\system32\smss.exe
%windir%\system32\services.exe
%windir%\system32\taskeng.exe
%windir%\system32\taskhost.exe
%windir%\system32\dwm.exe
%windir%\system32\conhost.exe
%windir%\system32\svchost.exe
%windir%\system32\sihost.exe
Long answer:
It took some time to get to that list - below is the path to enlightenment ;-)
Original answer:
To answer your question Find out whether a Process is a System Process is not as easy as it seems. In order to get this information you have to get the owner of the process which on windows systems is typically realted to Security identifiers.
A security identifier (SID) is a unique value of variable length used to identify a trustee. Each account has a unique SID issued by an authority, such as a Windows domain controller, and stored in a security database. Each time a user logs on, the system retrieves the SID for that user from the database and places it in the access token for that user. The system uses the SID in the access token to identify the user in all subsequent interactions with Windows security. When a SID has been used as the unique identifier for a user or group, it cannot ever be used again to identify another user or group.
You will have seen one of those for sure, it is something like S-1-5-18 or S-1-5-21-2557247...-...-...-1001.
There is a complete list of WellKnown SIDs which also includes a bunch of SIDs you would probably all consider as System Process-related.
If I am right in my assumption, you want to get all processes that are running under the local system account which would be S-1-5-18.
Stop talking, let's code:
First of all we (which is you, I have already tested it ;-) ) need to import GetSecurityInfo from advapi32.dll like this:
[DllImport("advapi32.dll", SetLastError = true)]
private static extern uint GetSecurityInfo(IntPtr handle,
SE_OBJECT_TYPE objectType,
SECURITY_INFORMATION securityInfo,
out IntPtr sidOwner,
out IntPtr sidGroup,
out IntPtr dacl,
out IntPtr sacl,
out IntPtr securityDescriptor);
...which requires two enumerations for SE_OBJECT_TYPE and SECURITY_INFORMATION to be defined like this:
private enum SE_OBJECT_TYPE
{
SE_UNKNOWN_OBJECT_TYPE,
SE_FILE_OBJECT,
SE_SERVICE,
SE_PRINTER,
SE_REGISTRY_KEY,
SE_LMSHARE,
SE_KERNEL_OBJECT,
SE_WINDOW_OBJECT,
SE_DS_OBJECT,
SE_DS_OBJECT_ALL,
SE_PROVIDER_DEFINED_OBJECT,
SE_WMIGUID_OBJECT,
SE_REGISTRY_WOW64_32KEY
}
private enum SECURITY_INFORMATION
{
OWNER_SECURITY_INFORMATION = 1,
GROUP_SECURITY_INFORMATION = 2,
DACL_SECURITY_INFORMATION = 4,
SACL_SECURITY_INFORMATION = 8,
}
Now we are almost there. If you call GetSecurityInfo in the following manner...
uint returnValue = GetSecurityInfo(process.Handle,
SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION,
out IntPtr ownerSid,
out IntPtr groupSid,
out IntPtr dacl,
out IntPtr sacl,
out IntPtr securityDescriptor);
... and get ERROR_SUCESS as a result (which is 0), you can use an instance of the SecurityIdentifier class to check whether the retrieved SID is the local system account or not, like this:
SecurityIdentifier securityIdentifier = new SecurityIdentifier(ownerSid);
if (securityIdentifier.IsWellKnown(WellKnownSidType.LocalSystemSid))
{
// The process is running unter the local system account.
}
That's it.
To achieve the final result you will have to check for multiple SIDs like System, Local service, Network service and so on...
Here is a small example, that does this for all processes on the local machine.
You will need to run this with the right priviledges of course, otherwise you will get access denied errors.
private static void Main(string[] args)
{
const uint ERROR_SUCCESS = 0;
Process[] processes = Process.GetProcesses();
foreach (Process process in processes)
{
try
{
uint returnValue = GetSecurityInfo(process.Handle,
SE_OBJECT_TYPE.SE_KERNEL_OBJECT,
SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION,
out IntPtr ownerSid,
out IntPtr groupSid,
out IntPtr dacl,
out IntPtr sacl,
out IntPtr securityDescriptor);
if (returnValue != ERROR_SUCCESS)
{
// If the function succeeds, the return value is ERROR_SUCCESS.
// If the function fails, the return value is a nonzero error code defined in WinError.h.
continue;
}
SecurityIdentifier securityIdentifier = new SecurityIdentifier(ownerSid);
Console.WriteLine("Owner of process {0} is {1}", process.ProcessName, securityIdentifier);
if (securityIdentifier.IsWellKnown(WellKnownSidType.LocalSystemSid))
{
Console.WriteLine("Running under System Account");
}
}
catch (Exception e)
{
Console.WriteLine("Unable to retrieve owner for process {0}: {1}", process.ProcessName, e.Message);
}
}
Update:
If you compare the result (of the original answer) with the list of processes in the task manager, there is still a discrepancy. As I investigated this issue further, I came accross an article that states, that processes which are marked as critical, will be shown under windows processes.
If the process has a visible window, then Task Manager calls it an "App".
If the process is marked as critical, then Task Manager calls it a "Windows Process".
Otherwise, Task Manager calls it a "Background Process".
This can be evaluated by simply calling IsProcessCritical. Therefore an DllImport is needed...
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool IsProcessCritical(IntPtr hProcess, ref bool Critical);
...afterwards it can be called like this:
bool criticalProcess = false;
if (!IsProcessCritical(process.Handle, ref criticalProcess))
{
// Could not retrieve process information
}
if (criticalProcess)
{
// This is a critical process, it should be listed
// in the "Windows processes" section.
}
Although this sounds promising, it is not - it still leads to incorrect results.
So after installing API Monitor (which is an incredible piece of software by the way) and filtering and searching through more than 5 millions of (already pre-filtered) api calls, I noticed, that Taskmgr.exe calls ExpandEnvironmentString multiple times with arguments, that are seemingly not retrieved prior to the calls.
After further investigation (and logical conclusion) I noticed, that there is a hard coded list embedded within Taskmgr.exe. It can be simply found by using the Process explorer:
Starting the process explorer
Right-click on Taskmgr.exe
Navigating to the strings tab
Scrolling down
Being disappointed
There are the following entries:
%windir%\explorer.exe
%windir%\system32\ntoskrnl.exe
%windir%\system32\WerFault.exe
%windir%\system32\backgroundTaskHost.exe
%windir%\system32\backgroundTransferHost.exe
%windir%\system32\winlogon.exe
%windir%\system32\wininit.exe
%windir%\system32\csrss.exe
%windir%\system32\lsass.exe
%windir%\system32\smss.exe
%windir%\system32\services.exe
%windir%\system32\taskeng.exe
%windir%\system32\taskhost.exe
%windir%\system32\dwm.exe
%windir%\system32\conhost.exe
%windir%\system32\svchost.exe
%windir%\system32\sihost.exe
So my conclusion is:The solution within the taskmanager is hard coded based on the above list (taken from the Windows 10 version).
One way to do this is by filtering out all processes whose path starts with the path of the windows directory.
You can get the path of the windows directory by calling Environment.GetFolderPath
with Environment.SpecialFolder.Windows like so:
var windowsPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
And then you can filter out all processes whose image is located somewhere in that folder:
var processes = Process.GetProcesses();
foreach (var process in processes) {
if (!process.MainModule.FileName.StartsWith(windowsPath)) {
// Do something with process
}
}
Just filter the result:
Process.GetProcesses().Where(x => x.MainWindowHandle != IntPtr.Zero)
Checking the path, could be bypassed

How to start a new Windows logon session (RDP or console) programmatically

I've been banging my head against this for several hours, so I figured it's time to ask. I'll start with a high-level description of the situation. You can find the entire source code at https://github.com/Jay-Rad/InstaTech_Client. This question only pertains to the project in "/InstaTech_Service/".
Overview
The InstaTech client is a remote control app that uses websockets and makes an outbound connection to an ASP.NET server for relay with the viewer. I have different versions, but they all function roughly the same (the Electron version tries WebRTC first before using raw websockets). The viewer portion of the app is web-based, and a demo can be found here: https://instatech.org/Demo/Remote_Control
The WPF (C#) and Electron versions present a GUI with a random ID that they must provide to the person remoting into their computer (similar to TeamViewer). Once a session is started, they capture the screen in different ways. For C#, I'm using a pinvoke to BitBlt to copy the image to an in-memory graphic, which is then sent through the websocket. Subsequent screen captures are compared to the previous one to create a box that encompasses the changed pixels, then that cropped section is sent. Mouse and keyboard inputs are received by the client and executed via pinvoke to keybd_event and mouse_event. These are working great.
The service I created works in similar fashion, but here are the differences. The service itself runs in session 0 under System account. It connects to the server and listens on the websocket. When a connection is made and screen viewing is requested, it launches a separate interactive process in the user's session in WinSta0\Default. Once the new process's websocket is connected, the server begins relaying messages between it and the viewer instead of the service and the viewer.
Although the new process is launched interactively in the user's session, it's running under the System account. This is achieved by a pinvoke to CreateProcessAsUser and duplicating the winlogon.exe access token.
The Problem
This solution works fine if someone is already logged in, even if via RDP. However, if nobody is logged in or the computer gets locked, I can't interact with the logon screen. When doing the screen capture, I'm detecting if the capture fails, which would mean the WinSta0\Default desktop is no longer active. Since I'm using CreateProcessAsUser, I can switch desktops to the WinSta0\Winlogon just fine. I can still see it (even if no one is logged in), but it won't take any inputs. I understand that this is by design for security reasons. Well, strangely, some mouse movements "slip through" if I'm moving it around and cause the cursor to reposition, but the rest get sent to the Default desktop and execute once logged back in.
So the problem is that I can't get an account logged into the computer with this setup. If it matters, I don't care to interact with the Winlogon desktop if someone else is already logged in and locked the computer. I only want to be able to log in if no one else is using it, or it's my account that's logged in and at the lock screen.
Attempted Solutions
I'm assuming that there's no way to circumvent the inability to send simulated inputs to the Winlogon desktop. (Correction: That is, using mouse_event and keybd_event functions. I've seen other applications do it, like TeamViewer and Microsoft SCCM Remote Control. I'm not sure how they do it, though.) If it is somehow possible, I think that'd be the most direct route. But here are some things I've looked into that focus on getting a new logon session started.
Pinvoke to LsaLogonUser. I'm not sure if this would accomplish what I'm after, but I tried anyway. However, even though the call to LsaLogonUser reports success, the handle I'm getting from LsaRegisterLogonProcess (out to lsaHan) is 0. I'm not sure what I'm doing wrong. I'm not too familiar with Win32 calls and trying to pick it up as I go. Maybe the calling process doesn't have the necessary rights. I've tried calling this from the service in session 0 and from the process running in the interactive session. An example of what I'm doing is below.
Microsoft Terminal Services Active Client COM library. I haven't dug too deeply into this, but I wonder if it might be possible to use this to initiate an RDP logon session. Once an RDP logon session is made, spawn a new InstaTech process in that session and connect to it. I doubt this would work if the RDP connection is being attempted from the same computer, though.
Credential Provider. I came across credential providers while researching. I'm not sure if creating one would solve the problem, but it sounds like it'd be a terribly complicated undertaking.
Does anyone have any suggestions? Or am I missing something entirely?
If you'd like to recompile the service and test things, I created a temporary admin account on the server. Any computer with the service installed will show up there, and you can log in using this account. Please keep in mind that anyone reading this post will be able to access any computers running the service, so make sure it's in an isolated environment.
Username: admin
Password: plzh#lpm3purdyplz
The service is self-installing. Pass the -install switch to install, -uninstall to uninstall. The EXE is copied to %programdata%\InstaTech, and the service starts it from there.
Thank you!
Reference Code
public static void CreateNewSession()
{
var kli = new SECUR32.KERB_INTERACTIVE_LOGON()
{
MessageType = SECUR32.KERB_LOGON_SUBMIT_TYPE.KerbInteractiveLogon,
UserName = "myusername#someplace.com",
Password = "superencryptedstring"
};
IntPtr pluid;
IntPtr lsaHan;
ulong secMode;
uint authPackID;
IntPtr kerbLogInfo;
SECUR32.LSA_STRING logonProc = new SECUR32.LSA_STRING()
{
Buffer = Marshal.StringToHGlobalAuto("InstaLogon"),
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon")),
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon"))
};
SECUR32.LSA_STRING originName = new SECUR32.LSA_STRING()
{
Buffer = Marshal.StringToHGlobalAuto("InstaLogon"),
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon")),
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon"))
};
SECUR32.LSA_STRING authPackage = new SECUR32.LSA_STRING()
{
Buffer = Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A"),
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A")),
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A"))
};
IntPtr hLogonProc = Marshal.AllocHGlobal(Marshal.SizeOf(logonProc));
Marshal.StructureToPtr(logonProc, hLogonProc, false);
ADVAPI32.AllocateLocallyUniqueId(out pluid);
SECUR32.LsaRegisterLogonProcess(hLogonProc, out lsaHan, out secMode);
SECUR32.LsaLookupAuthenticationPackage(lsaHan, ref authPackage, out authPackID);
kerbLogInfo = Marshal.AllocHGlobal(Marshal.SizeOf(kli));
Marshal.StructureToPtr(kli, kerbLogInfo, false);
var ts = new SECUR32.TOKEN_SOURCE("Insta");
IntPtr profBuf;
uint profBufLen;
long logonID;
IntPtr logonToken;
SECUR32.QUOTA_LIMITS quotas;
SECUR32.WinStatusCodes subStatus;
SECUR32.LsaLogonUser(lsaHan, ref originName, SECUR32.SecurityLogonType.Interactive, authPackID, kerbLogInfo, (uint)Marshal.SizeOf(kerbLogInfo), IntPtr.Zero, ref ts, out profBuf, out profBufLen, out logonID, out logonToken, out quotas, out subStatus);
}
This is the method that the service in session 0 is using to launch another instance in the interactive session. I got most of this from this article: https://www.codeproject.com/kb/vista-security/subvertingvistauac.aspx. I only added the RDP session lookup.
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;
}
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), subsequent threads will need to call this as well.

Categories

Resources