Launching a Process in User's Session from a Service

How to start a process from windows service into currently logged in user's session

A blog on MSDN describes a solution

It's a terrific helpful post about starting a new process in interactive session from windows service on Vista/7.

For non-LocalSystem services, the basic idea is:

  • Enumerate the process to get the handle of the Explorer.

  • OpenProcessToken should give you the access token.
    Note : The account under which your service is running must have appropriate privileges to call this API and get process token.

  • Once you have the token call CreateProcessAsUser with this token. This token already have the right session Id.

Launching a process in user’s session from a service

It is really possible. The main problem which you have is that Windows should be seen as a terminal server and a users session as a remote session. Your service should be able to start a process which run in the remote session belongs to the user.

By the way, if you write a service which run under Windows XP which is not added to a domain and the fast user switching is activated, you can have the same problems to start a process on running on the second (third and so on) logged users desktop.

I hope you have a user token, which you receive for example with respect of impersonation or you have a dwSessionId of session. If you don't have it you can try use some WTS-function (Remote Desktop Services API http://msdn.microsoft.com/en-us/library/aa383464.aspx, for example WTSEnumerateProcesses or WTSGetActiveConsoleSessionId) or LSA-API to find out the corresponding users session (LsaEnumerateLogonSessions see http://msdn.microsoft.com/en-us/library/aa378275.aspx and LsaGetLogonSessionData see http://msdn.microsoft.com/en-us/library/aa378290.aspx) or ProcessIdToSessionId (see http://msdn.microsoft.com/en-us/library/aa382990.aspx).

You can use GetTokenInformation function with the parameter TokenSessionId (see http://msdn.microsoft.com/en-us/library/aa446671.aspx) to receive the session id dwSessionId of the users session if you knows the users token hClient.

BOOL bSuccess;
HANDLE hProcessToken = NULL, hNewProcessToken = NULL;
DWORD dwSessionId, cbReturnLength;

bSuccess = GetTokenInformation (hClient, TokenSessionId, &dwSessionId,
sizeof(DWORD), &cbReturnLength);
bSuccess = OpenProcessToken (GetCurrentProcess(), MAXIMUM_ALLOWED, &hProcessToken);
bSuccess = DuplicateTokenEx (hProcessToken, MAXIMUM_ALLOWED, NULL,
SecurityImpersonation,
TokenPrimary, &hNewProcessToken);
EnablePrivilege (SE_TCB_NAME);
bSuccess = SetTokenInformation (hNewProcessToken, TokenSessionId, &dwSessionId,
sizeof(DWORD));
bSuccess = CreateProcessAsUser (hNewProcessToken, NULL, szCommandToExecute, ...);

This code only a schema. EnablePrivilege is a simple function used AdjustTokenPrivileges to enable SE_TCB_NAME privilege (see http://msdn.microsoft.com/en-us/library/aa446619.aspx as a template). It is important that the process from which you are start a process you have TCB privilege, but if your service run under the Local System you have enough permissions. By the way, following code fragment work with not only Local System account, but the account must have SE_TCB_NAME privilege to be able to switch current terminal server session.

One more remark. In the code above we start new process with the same account as the current process have (for example Local System). You change change a code to use another account for example the users token hClient. It is only important to have a primary token. If you have an impersonation token you can convert it to the primary token exactly like in the code above.

In the STARTUPINFO structure used in CreateProcessAsUser you should use lpDesktop = WinSta0\Default".

Depend on your requirements it could be also needed to use CreateEnvironmentBlock to create an new environment block that you will be passing to the new process.

I recommend you also to read How to ensure process window launched by Process.Start(ProcessStartInfo) has focus of all Forms? where I describe how to force that the process will be started in foreground on the users desktop.

create process in user session from service

I use code similar to yours in my own service and it works fine. There are some things that need to be taken into account that the code you showed is not doing:

  1. When calling WTSQueryUserToken(), you have to make sure your service process has the SE_TCB_NAME privilege enabled. Use AdjustTokenPrivileges() for that.

  2. The session ID returned by WTSGetActiveConsoleSessionId() may not be the correct session that you need to run your spawned process on! It returns the session ID that is attached to the physical console (screen/keyboard/mouse) of the local machine, if any. That session may be displaying the secure WinLogon desktop, meaning no user is actually logged in to the physical machine, so calling WTSQueryUserToken() on that session ID would fail with an ERROR_NO_TOKEN error. A user can log in over a Remote Desktop Connection, for instance, in which case that connection would be running in a different session than the console. If you want your spawned process to run in a session that has a user logged in, you need to use WTSEnumerateSessions() to find a session that is in the WTSActive state. And even then, WTSQueryUserToken() may not return a token depending on how the user is logged in, so you need to call WTSQueryUserToken() on each active session you find until you find one that successfully gives you a token.

  3. When calling DuplicateTokenEx(), use SecurityIdentification instead of SecurityDelegation.

  4. When calling CreateProcessAsUser(), you can call CreateEnvironmentBlock() first to create an environment that is appropriate for that specific user and then pass that pointer to CreateProcessAsUser(). Otherwise, the spawned process will use your service's environment instead. This step is optional, depending on the particular needs of the spawned app.

How can I have a service run a process under the current user's session?

If the service needs to run as the current user, then why make it a service? The purpose of a service is to run at all times, even when a user is not logged in. As such, it's designed to run as a specific user at all times.

Why not convert this to a program that runs when a user logs in?

Run a process from a windows service as the current user

One option is to have background application that automatically starts when user logs on and listens to commands from your service through WCF, or thrift, or by just monitoring some file and reading command from there.

Another option is to do what you originally asked for - launch using windows API. But the code is quite scary. Here is a sample, that you can use. It will execute any command line under currently active user session, with CreateProcessInConsoleSession method:

internal class ApplicationLauncher
{
public enum TOKEN_INFORMATION_CLASS
{
TokenUser = 1,
TokenGroups,
TokenPrivileges,
TokenOwner,
TokenPrimaryGroup,
TokenDefaultDacl,
TokenSource,
TokenType,
TokenImpersonationLevel,
TokenStatistics,
TokenRestrictedSids,
TokenSessionId,
TokenGroupsAndPrivileges,
TokenSessionReference,
TokenSandBoxInert,
TokenAuditPolicy,
TokenOrigin,
MaxTokenInfoClass // MaxTokenInfoClass should always be the last enum
}

public const int READ_CONTROL = 0x00020000;

public const int STANDARD_RIGHTS_REQUIRED = 0x000F0000;

public const int STANDARD_RIGHTS_READ = READ_CONTROL;
public const int STANDARD_RIGHTS_WRITE = READ_CONTROL;
public const int STANDARD_RIGHTS_EXECUTE = READ_CONTROL;

public const int STANDARD_RIGHTS_ALL = 0x001F0000;

public const int SPECIFIC_RIGHTS_ALL = 0x0000FFFF;

public const int TOKEN_ASSIGN_PRIMARY = 0x0001;
public const int TOKEN_DUPLICATE = 0x0002;
public const int TOKEN_IMPERSONATE = 0x0004;
public const int TOKEN_QUERY = 0x0008;
public const int TOKEN_QUERY_SOURCE = 0x0010;
public const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
public const int TOKEN_ADJUST_GROUPS = 0x0040;
public const int TOKEN_ADJUST_DEFAULT = 0x0080;
public const int TOKEN_ADJUST_SESSIONID = 0x0100;

public const int TOKEN_ALL_ACCESS_P = (STANDARD_RIGHTS_REQUIRED |
TOKEN_ASSIGN_PRIMARY |
TOKEN_DUPLICATE |
TOKEN_IMPERSONATE |
TOKEN_QUERY |
TOKEN_QUERY_SOURCE |
TOKEN_ADJUST_PRIVILEGES |
TOKEN_ADJUST_GROUPS |
TOKEN_ADJUST_DEFAULT);

public const int TOKEN_ALL_ACCESS = TOKEN_ALL_ACCESS_P | TOKEN_ADJUST_SESSIONID;

public const int TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY;

public const int TOKEN_WRITE = STANDARD_RIGHTS_WRITE |
TOKEN_ADJUST_PRIVILEGES |
TOKEN_ADJUST_GROUPS |
TOKEN_ADJUST_DEFAULT;

public const int TOKEN_EXECUTE = STANDARD_RIGHTS_EXECUTE;

public const uint MAXIMUM_ALLOWED = 0x2000000;

public const int CREATE_NEW_PROCESS_GROUP = 0x00000200;
public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;

public const int IDLE_PRIORITY_CLASS = 0x40;
public const int NORMAL_PRIORITY_CLASS = 0x20;
public const int HIGH_PRIORITY_CLASS = 0x80;
public const int REALTIME_PRIORITY_CLASS = 0x100;

public const int CREATE_NEW_CONSOLE = 0x00000010;

public const string SE_DEBUG_NAME = "SeDebugPrivilege";
public const string SE_RESTORE_NAME = "SeRestorePrivilege";
public const string SE_BACKUP_NAME = "SeBackupPrivilege";

public const int SE_PRIVILEGE_ENABLED = 0x0002;

public const int ERROR_NOT_ALL_ASSIGNED = 1300;

private const uint TH32CS_SNAPPROCESS = 0x00000002;

public static int INVALID_HANDLE_VALUE = -1;

[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LookupPrivilegeValue(IntPtr lpSystemName, string lpname,
[MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid);

[DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.StdCall)]
public static extern bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine,
ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool DuplicateToken(IntPtr ExistingTokenHandle,
int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

[DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
public static extern bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);

[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, IntPtr ReturnLength);

[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool SetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass,
ref uint TokenInformation, uint TokenInformationLength);

[DllImport("userenv.dll", SetLastError = true)]
public static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

public static bool CreateProcessInConsoleSession(String CommandLine, bool bElevate)
{

PROCESS_INFORMATION pi;

bool bResult = false;
uint dwSessionId, winlogonPid = 0;
IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;

Debug.Print("CreateProcessInConsoleSession");
// Log the client on to the local computer.
dwSessionId = WTSGetActiveConsoleSessionId();

// Find the winlogon process
var procEntry = new PROCESSENTRY32();

uint hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return false;
}

procEntry.dwSize = (uint) Marshal.SizeOf(procEntry); //sizeof(PROCESSENTRY32);

if (Process32First(hSnap, ref procEntry) == 0)
{
return false;
}

String strCmp = "explorer.exe";
do
{
if (strCmp.IndexOf(procEntry.szExeFile) == 0)
{
// We found a winlogon process...make sure it's running in the console session
uint winlogonSessId = 0;
if (ProcessIdToSessionId(procEntry.th32ProcessID, ref winlogonSessId) &&
winlogonSessId == dwSessionId)
{
winlogonPid = procEntry.th32ProcessID;
break;
}
}
}
while (Process32Next(hSnap, ref procEntry) != 0);

//Get the user token used by DuplicateTokenEx
WTSQueryUserToken(dwSessionId, ref hUserToken);

var si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
si.lpDesktop = "winsta0\\default";
var tp = new TOKEN_PRIVILEGES();
var luid = new LUID();
hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

if (
!OpenProcessToken(hProcess,
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY
| TOKEN_ADJUST_SESSIONID | TOKEN_READ | TOKEN_WRITE, ref hPToken))
{
Debug.Print(String.Format("CreateProcessInConsoleSession OpenProcessToken error: {0}",
Marshal.GetLastWin32Error()));
}

if (!LookupPrivilegeValue(IntPtr.Zero, SE_DEBUG_NAME, ref luid))
{
Debug.Print(String.Format("CreateProcessInConsoleSession LookupPrivilegeValue error: {0}",
Marshal.GetLastWin32Error()));
}

var sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa);

if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa,
(int) SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int) TOKEN_TYPE.TokenPrimary,
ref hUserTokenDup))
{
Debug.Print(
String.Format(
"CreateProcessInConsoleSession DuplicateTokenEx error: {0} Token does not have the privilege.",
Marshal.GetLastWin32Error()));
CloseHandle(hProcess);
CloseHandle(hUserToken);
CloseHandle(hPToken);
return false;
}

if (bElevate)
{
//tp.Privileges[0].Luid = luid;
//tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

tp.PrivilegeCount = 1;
tp.Privileges = new int[3];
tp.Privileges[2] = SE_PRIVILEGE_ENABLED;
tp.Privileges[1] = luid.HighPart;
tp.Privileges[0] = luid.LowPart;

//Adjust Token privilege
if (
!SetTokenInformation(hUserTokenDup, TOKEN_INFORMATION_CLASS.TokenSessionId, ref dwSessionId,
(uint) IntPtr.Size))
{
Debug.Print(
String.Format(
"CreateProcessInConsoleSession SetTokenInformation error: {0} Token does not have the privilege.",
Marshal.GetLastWin32Error()));
//CloseHandle(hProcess);
//CloseHandle(hUserToken);
//CloseHandle(hPToken);
//CloseHandle(hUserTokenDup);
//return false;
}
if (
!AdjustTokenPrivileges(hUserTokenDup, false, ref tp, Marshal.SizeOf(tp), /*(PTOKEN_PRIVILEGES)*/
IntPtr.Zero, IntPtr.Zero))
{
int nErr = Marshal.GetLastWin32Error();

if (nErr == ERROR_NOT_ALL_ASSIGNED)
{
Debug.Print(
String.Format(
"CreateProcessInConsoleSession AdjustTokenPrivileges error: {0} Token does not have the privilege.",
nErr));
}
else
{
Debug.Print(String.Format("CreateProcessInConsoleSession AdjustTokenPrivileges error: {0}", nErr));
}
}
}

uint dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
IntPtr pEnv = IntPtr.Zero;
if (CreateEnvironmentBlock(ref pEnv, hUserTokenDup, true))
{
dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
}
else
{
pEnv = IntPtr.Zero;
}
// Launch the process in the client's logon session.
bResult = CreateProcessAsUser(hUserTokenDup, // client's access token
null, // file to execute
CommandLine, // command line
ref sa, // pointer to process SECURITY_ATTRIBUTES
ref sa, // pointer to thread SECURITY_ATTRIBUTES
false, // handles are not inheritable
(int) dwCreationFlags, // creation flags
pEnv, // pointer to new environment block
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out pi // receives information about new process
);
// End impersonation of client.

//GetLastError should be 0
int iResultOfCreateProcessAsUser = Marshal.GetLastWin32Error();

//Close handles task
CloseHandle(hProcess);
CloseHandle(hUserToken);
CloseHandle(hUserTokenDup);
CloseHandle(hPToken);

return (iResultOfCreateProcessAsUser == 0) ? true : false;
}

[DllImport("kernel32.dll")]
private static extern int Process32First(uint hSnapshot, ref PROCESSENTRY32 lppe);

[DllImport("kernel32.dll")]
private static extern int Process32Next(uint hSnapshot, ref PROCESSENTRY32 lppe);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hSnapshot);

[DllImport("kernel32.dll")]
private static extern uint WTSGetActiveConsoleSessionId();

[DllImport("Wtsapi32.dll")]
private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);

[DllImport("kernel32.dll")]
private static extern bool ProcessIdToSessionId(uint dwProcessId, ref uint pSessionId);

[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

[DllImport("advapi32", SetLastError = true)]
[SuppressUnmanagedCodeSecurity]
private static extern bool OpenProcessToken(IntPtr ProcessHandle, // handle to process
int DesiredAccess, // desired access to process
ref IntPtr TokenHandle);

#region Nested type: LUID

[StructLayout(LayoutKind.Sequential)]
internal struct LUID
{
public int LowPart;
public int HighPart;
}

#endregion

//end struct

#region Nested type: LUID_AND_ATRIBUTES

[StructLayout(LayoutKind.Sequential)]
internal struct LUID_AND_ATRIBUTES
{
public LUID Luid;
public int Attributes;
}

#endregion

#region Nested type: PROCESSENTRY32

[StructLayout(LayoutKind.Sequential)]
private struct PROCESSENTRY32
{
public uint dwSize;
public readonly uint cntUsage;
public readonly uint th32ProcessID;
public readonly IntPtr th32DefaultHeapID;
public readonly uint th32ModuleID;
public readonly uint cntThreads;
public readonly uint th32ParentProcessID;
public readonly int pcPriClassBase;
public readonly uint dwFlags;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public readonly string szExeFile;
}

#endregion

#region Nested type: PROCESS_INFORMATION

[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}

#endregion

#region Nested type: SECURITY_ATTRIBUTES

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public int Length;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}

#endregion

#region Nested type: SECURITY_IMPERSONATION_LEVEL

private enum SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous = 0,
SecurityIdentification = 1,
SecurityImpersonation = 2,
SecurityDelegation = 3,
}

#endregion

#region Nested type: STARTUPINFO

[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
public int cb;
public String lpReserved;
public String lpDesktop;
public String lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}

#endregion

#region Nested type: TOKEN_PRIVILEGES

[StructLayout(LayoutKind.Sequential)]
internal struct TOKEN_PRIVILEGES
{
internal int PrivilegeCount;
//LUID_AND_ATRIBUTES
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
internal int[] Privileges;
}

#endregion

#region Nested type: TOKEN_TYPE

private enum TOKEN_TYPE
{
TokenPrimary = 1,
TokenImpersonation = 2
}

#endregion

// handle to open access token
}

Open a JobObject created in a service from a user session process

if you create Job in service - this object by default will be have WinSystemLabelSid label SID: S-1-16-16384 - System Mandatory Level. (i just check this) so you need not only set Dacl but Sacl too. for example:

    ULONG cb = MAX_SID_SIZE;
PSID LowLabelSid = (PSID)alloca(MAX_SID_SIZE);
if (CreateWellKnownSid(WinLowLabelSid, 0, LowLabelSid, &cb))
{
PACL Sacl = (PACL)alloca(cb += sizeof(ACL) + sizeof(ACE_HEADER) + sizeof(ACCESS_MASK));
InitializeAcl(Sacl, cb, ACL_REVISION);
if (AddMandatoryAce(Sacl, ACL_REVISION, 0, 0, LowLabelSid))
{
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);
SetSecurityDescriptorSacl(&sd, TRUE, Sacl, FALSE);

SECURITY_ATTRIBUTES sa= { sizeof(sa), &sd, FALSE };

if (HANDLE hJob = CreateJobObject(&sa, L"Global\\{58BFC6DB-BE93-4cdb-919C-4C713ACB5A32}"))
{
CloseHandle(hJob);
}
}
}


Related Topics



Leave a reply



Submit