I'm trying to call CreateProcessAsUser to run a desktop application for a user from a service. (See Launching a process in user’s session from a service and CreateProcessAsUser from service and user security issues)
It basically works, but some applications fail to run properly. Ultimately after some debugging I found out that one C# application fails because Environment.GetFolderPath
returns null
for a special folder. I'm thinking that this could be because the invoked user's registry or some paths in it are not correctly loaded for the user profile.
In the REMARKS section of CreateProcessAsUserW there are a few hints what to consider when creating a process for a user. I'm already creating an environment block using CreateEnvironmentBlock (and the environment variables seem to work), but it also says:
CreateProcessAsUser does not load the specified user's profile into the HKEY_USERS registry key. Therefore, to access the information in the HKEY_CURRENT_USER registry key, you must load the user's profile information into HKEY_USERS with the LoadUserProfile function before calling CreateProcessAsUser. [...]
I have checked the documentation of LoadUserProfile where it says something similar:
Note that it is your responsibility to load the user's registry hive into the HKEY_USERS registry key with the LoadUserProfile function before you call CreateProcessAsUser. This is because CreateProcessAsUser does not load the specified user's profile into HKEY_USERS. This means that access to information in the HKEY_CURRENT_USER registry key may not produce results consistent with a normal interactive logon.
However I didn't find any example or further details how to load a user's registry hive into the HKEY_USERS registry key and the registry APIs I looked at (RegLoadKey looked most promising, but I don't think that's the correct one or how to use it with a my target process before CreateProcessAsUser) don't do what is described on LoadUserProfile. Furthermore all StackOverflow questions I looked at didn't mention this or only just mentioned on the side that they are using this function.
I thought maybe passing the profile could be part of STARTUPINFOEX but I also didn't find anything regarding "profile" or "registry" in the documentation there.
I'm really struggling trying to find out what to do with the LoadUserProfile result (struct PROFILEINFO) and how to use it with the CreateProcessAsUserW function. I would be glad if you could give me some hints what I actually want to do with that function and how I can use it with CreateProcessAsUserW given that I have already acquired a user token with WTSQueryUserToken.
This is my current C# code how I spawn the process (it also contains some piping code which I left out for readability):
public class Win32ConPTYTerminal
{
public bool HasExited { get; private set; }
private IntPtr hPC;
private IntPtr hPipeIn = MyWin32.INVALID_HANDLE_VALUE;
private IntPtr hPipeOut = MyWin32.INVALID_HANDLE_VALUE;
private readonly TaskCompletionSource<int> tcs;
private readonly SemaphoreSlim exitSemaphore = new SemaphoreSlim(1);
private IntPtr hProcess;
private int dwPid;
public Win32ConPTYTerminal()
{
CreatePseudoConsoleAndPipes(out hPC, out hPipeIn, out hPipeOut);
tcs = new TaskCompletionSource<int>();
}
private static unsafe int InitializeStatupInfo(
ref MyWin32.STARTUPINFOEXW pInfo, IntPtr hPC)
{
pInfo.StartupInfo.cb = (uint)sizeof(MyWin32.STARTUPINFOEXW);
pInfo.StartupInfo.dwFlags = MyWin32.StartFlags.STARTF_USESTDHANDLES;
pInfo.StartupInfo.hStdInput = IntPtr.Zero;
pInfo.StartupInfo.hStdOutput = IntPtr.Zero;
pInfo.StartupInfo.hStdError = IntPtr.Zero;
fixed (char* title = "Console Title")
pInfo.StartupInfo.lpTitle = title;
var attrListSize = IntPtr.Zero;
MyWin32.InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0,
ref attrListSize);
pInfo.lpAttributeList = Marshal.AllocHGlobal(attrListSize.ToInt32());
if (MyWin32.InitializeProcThreadAttributeList(
pInfo.lpAttributeList, 1, 0, ref attrListSize))
{
if (!MyWin32.UpdateProcThreadAttribute(
pInfo.lpAttributeList,
0,
MyWin32.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
hPC,
new IntPtr(sizeof(IntPtr)),
IntPtr.Zero,
IntPtr.Zero
))
{
Marshal.FreeHGlobal(pInfo.lpAttributeList);
return Marshal.GetLastWin32Error();
}
else
{
return 0;
}
}
else
{
Marshal.FreeHGlobal(pInfo.lpAttributeList);
return Marshal.GetLastWin32Error();
}
}
private unsafe IntPtr ObtainUserToken(out string activeWinsta)
{
activeWinsta = @"Winsta0\default";
var process = MyWin32.GetCurrentProcess();
if (!MyWin32.OpenProcessToken(process,
MyWin32.AccessMask.TOKEN_ADJUST_PRIVILEGES
| MyWin32.AccessMask.TOKEN_QUERY
| MyWin32.AccessMask.TOKEN_DUPLICATE
| MyWin32.AccessMask.TOKEN_ASSIGN_PRIMARY,
out IntPtr processToken))
{
if (MyWin32.OpenProcessToken(process,
MyWin32.AccessMask.TOKEN_QUERY
| MyWin32.AccessMask.TOKEN_DUPLICATE
| MyWin32.AccessMask.TOKEN_ASSIGN_PRIMARY,
out var fallbackToken))
{
PrintWarning("could not obtain high privilege token for UI",
win32: true);
return fallbackToken;
}
throw new Win32Exception(
Marshal.GetLastWin32Error(),
"Unexpected win32 error code opening process token");
}
int sessionId = -1;
if (MyWin32.WTSEnumerateSessionsW(MyWin32.WTS_CURRENT_SERVER_HANDLE, 0, 1, out IntPtr pSession, out int sessionCount))
{
var sessions = (MyWin32.WTS_SESSION_INFOW*)pSession;
for (int i = 0; i < sessionCount; i++)
{
var session = sessions[i];
if (session.State != MyWin32.WTS_CONNECTSTATE_CLASS.WTSActive)
continue;
var winsta = Marshal.PtrToStringUni((IntPtr)session.pWinStationName);
PrintTrace("Detected active session " + winsta);
// activeWinsta = winsta;
sessionId = session.SessionId;
}
}
else
{
PrintWarning("WTSEnumerateSessionsW failed", win32: true);
}
if (sessionId == -1)
sessionId = MyWin32.WTSGetActiveConsoleSessionId();
if (sessionId == -1)
{
PrintWarning("no desktop user logged in to use",
win32: true);
return processToken;
}
var tokenPrivs = new MyWin32.TOKEN_PRIVILEGES();
if (!MyWin32.LookupPrivilegeValueW(null, "SeTcbPrivilege",
out var luid))
{
PrintWarning(
"could not change to desktop user (LookupPrivilegeValue)",
win32: true);
return processToken;
}
tokenPrivs.PrivilegeCount = 1;
tokenPrivs.Privileges.Luid = luid;
tokenPrivs.Privileges.Attributes = MyWin32.SE_PRIVILEGE_ENABLED;
if (!MyWin32.AdjustTokenPrivileges(processToken, false,
ref tokenPrivs, 0, IntPtr.Zero, IntPtr.Zero))
{
PrintWarning(
"could not change to desktop user (AdjustTokenPrivileges)",
win32: true);
return processToken;
}
try
{
if (!MyWin32.WTSQueryUserToken(sessionId, out var currentToken))
{
PrintWarning(
"could not change to desktop user on session " + sessionId + " (WTSQueryUserToken)",
win32: true);
return processToken;
}
return currentToken;
}
finally
{
tokenPrivs.Privileges.Attributes =
MyWin32.SE_PRIVILEGE_DISABLED;
MyWin32.AdjustTokenPrivileges(processToken, false,
ref tokenPrivs, 0, IntPtr.Zero, IntPtr.Zero);
}
}
private unsafe string? TraceTokenInfo(IntPtr token)
{
if (!MyWin32.GetTokenInformation(token,
MyWin32.TOKEN_INFORMATION_CLASS.TokenUser,
IntPtr.Zero,
0,
out int returnLength))
{
var error = Marshal.GetLastWin32Error();
if (error != MyWin32.ERROR_INSUFFICIENT_BUFFER)
{
PrintWarning(
"could not determine token user (GetTokenInformation)",
win32: true);
return null;
}
}
string username;
var userInfoPtr = Marshal.AllocHGlobal(returnLength);
try
{
if (!MyWin32.GetTokenInformation(token,
MyWin32.TOKEN_INFORMATION_CLASS.TokenUser,
userInfoPtr,
returnLength,
out int returnLength2))
{
PrintWarning(
"could not determine token user (GetTokenInformation)",
win32: true);
return null;
}
var user = (MyWin32.TOKEN_USER*)userInfoPtr;
var userSid = (*user).User.Sid;
StringBuilder name = new StringBuilder(255);
int nameLength = name.Capacity;
StringBuilder domainName = new StringBuilder(255);
int domainNameLength = domainName.Capacity;
if (!MyWin32.LookupAccountSidW(
null,
userSid,
name, ref nameLength,
domainName, ref domainNameLength,
out var peUse))
{
PrintWarning(
"could not determine token user (LookupAccountSidW)",
win32: true);
return null;
}
username = name.ToString();
PrintTrace("Running process with user " + username);
}
finally
{
Marshal.FreeHGlobal(userInfoPtr);
}
return username;
}
public void Start(ProcessStartInfo startInfo, CancellationToken cancel)
{
HasExited = false;
var startupinfo = new MyWin32.STARTUPINFOEXW();
var result = InitializeStatupInfo(ref startupinfo, hPC);
if (result != 0)
throw new Exception("Unexpected win32 error code " + result);
var token = ObtainUserToken(out var winsta);
var username = TraceTokenInfo(token);
string environment = MakeEnvironment(token, startInfo.Environment);
var cmdLine = new StringBuilder();
cmdLine
.Append(LocalProcessExecutor.EscapeShellParam(startInfo.FileName))
.Append(' ')
.Append(startInfo.Arguments);
cancel.ThrowIfCancellationRequested();
MyWin32.PROCESS_INFORMATION piClient;
var withProfileInfo = new WithProfileInfo(token, username);
if (!withProfileInfo.LoadedProfileInfo)
PrintWarning("Failed to LoadUserProfile, registry will not work as expected", true);
unsafe
{
fixed (char* winstaPtr = winsta)
{
startupinfo.StartupInfo.lpDesktop = winstaPtr;
result = MyWin32.CreateProcessAsUserW(
token,
startInfo.FileName,
cmdLine,
IntPtr.Zero, // Process handle not inheritable
IntPtr.Zero, // Thread handle not inheritable
false, // Inherit handles
MyWin32.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT // use startupinfoex
| MyWin32.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT // environment is wstring
// | MyWin32.ProcessCreationFlags.CREATE_NEW_PROCESS_GROUP // make this not a child (for maximum compatibility)
,
environment,
startInfo.WorkingDirectory,
ref startupinfo.StartupInfo,
out piClient
) ? 0 : Marshal.GetLastWin32Error();
}
}
if (result != 0)
{
Marshal.FreeHGlobal(startupinfo.lpAttributeList);
withProfileInfo.Dispose();
throw result switch
{
2 => new FileNotFoundException(
"Could not find file to execute",
startInfo.FileName
),
3 => new FileNotFoundException(
"Could not find path to execute",
startInfo.FileName
),
_ => new Win32Exception(result),
};
}
dwPid = piClient.dwProcessId;
hProcess = piClient.hProcess;
cancel.Register(() =>
{
if (HasExited || hProcess == IntPtr.Zero)
return;
Kill();
});
Task.Run(async () =>
{
MyWin32.WaitForSingleObject(hProcess, -1);
MyWin32.GetExitCodeProcess(hProcess, out var exitCode);
HasExited = true;
// wait a little bit for final log lines
var exited = await exitSemaphore.WaitAsync(50)
.ConfigureAwait(false);
if (exited)
exitSemaphore.Release();
tcs.TrySetResult(exitCode);
withProfileInfo.Dispose();
MyWin32.CloseHandle(piClient.hThread);
MyWin32.CloseHandle(piClient.hProcess);
hProcess = IntPtr.Zero;
MyWin32.DeleteProcThreadAttributeList(
startupinfo.lpAttributeList);
Marshal.FreeHGlobal(startupinfo.lpAttributeList);
Dispose();
});
}
private unsafe string MakeEnvironment(IntPtr token, IDictionary<string, string> envOverride)
{
StringBuilder environment = new StringBuilder();
foreach (var kv in envOverride)
{
environment.Append(kv.Key)
.Append('=')
.Append(kv.Value)
.Append('\0');
}
if (!MyWin32.CreateEnvironmentBlock(out var lpEnvironment, token, bInherit: false))
{
PrintWarning(
"could not generate environment variables (CreateEnvironmentBlock)",
win32: true);
return environment.ToString();
}
var envPtr = (char*)lpEnvironment;
int i = 0;
while (i == 0 || !(envPtr[i] == '\0' && envPtr[i - 1] == '\0'))
{
int start = i;
while (envPtr[i] != '\0' && envPtr[i] != '=')
{
i++;
}
// malformed check, needs =
if (envPtr[i] == '\0')
continue;
var name = Marshal.PtrToStringUni((IntPtr)(envPtr + start), i - start);
i++;
start = i;
while (envPtr[i] != '\0')
{
i++;
}
var value = Marshal.PtrToStringUni((IntPtr)(envPtr + start), i - start);
i++;
if (!envOverride.Keys.Contains(name))
{
environment.Append(name)
.Append('=')
.Append(value)
.Append('\0');
}
}
if (!MyWin32.DestroyEnvironmentBlock(lpEnvironment))
{
PrintWarning(
"failed to free environment block, leaking memory (DestroyEnvironmentBlock)",
win32: true);
}
environment.Append('\0');
return environment.ToString();
}
public Task<int> WaitForExit()
{
return tcs.Task;
}
public void Kill(int statusCode = -1)
{
if (HasExited || hProcess == IntPtr.Zero)
return;
MyWin32.EnumWindows((hwnd, _) => {
MyWin32.GetWindowThreadProcessId(hwnd, out var pid);
if (pid == dwPid)
{
MyWin32.PostMessageW(hwnd, MyWin32.Message.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
}
return true;
}, IntPtr.Zero);
if (MyWin32.WaitForSingleObject(hProcess, 1000) != 0)
{
MyWin32.TerminateProcess(hProcess, statusCode);
MyWin32.WaitForSingleObject(hProcess, 500);
}
else
{
statusCode = 0;
}
// wait a little bit for final log lines
if (exitSemaphore.Wait(50))
exitSemaphore.Release();
// actual exit code is more accurate, give watching thread time to set it first
Thread.Sleep(50);
// if thread hung up, set manually
tcs.TrySetResult(statusCode);
}
private bool disposed;
private readonly object _lockObject = new object();
public void Dispose()
{
lock (_lockObject)
{
if (disposed)
return;
disposed = true;
}
if (hPC != IntPtr.Zero)
MyWin32.ClosePseudoConsole(hPC);
hPC = IntPtr.Zero;
DisposePipes();
exitSemaphore.Dispose();
}
private void DisposePipes()
{
if (hPipeOut != MyWin32.INVALID_HANDLE_VALUE)
MyWin32.CloseHandle(hPipeOut);
hPipeOut = MyWin32.INVALID_HANDLE_VALUE;
if (hPipeIn != MyWin32.INVALID_HANDLE_VALUE)
MyWin32.CloseHandle(hPipeIn);
hPipeIn = MyWin32.INVALID_HANDLE_VALUE;
}
}
public unsafe class WithProfileInfo : IDisposable
{
private MyWin32.PROFILEINFOW profileInfo;
private IntPtr token;
private char* username;
public bool LoadedProfileInfo { get; }
public WithProfileInfo(IntPtr token, string username)
{
this.token = token;
this.username = (char*)Marshal.StringToHGlobalUni(username);
profileInfo.dwSize = sizeof(MyWin32.PROFILEINFOW);
profileInfo.lpUserName = this.username;
if (!MyWin32.LoadUserProfileW(token, ref profileInfo))
{
LoadedProfileInfo = false;
}
else
{
LoadedProfileInfo = true;
}
}
public void Dispose()
{
if (LoadedProfileInfo)
MyWin32.UnloadUserProfile(token, profileInfo.hProfile);
Marshal.FreeHGlobal((IntPtr)username);
}
}
Then when I try to print out a bunch of known folders in that application (using SHGetSpecialFolderPathW) I get the following inside the service:
FOLDERID_LocalAppData:
(Win32 Error for above) SHGetKnownFolderPath: 5 - Access is denied.
FOLDERID_RoamingAppData:
(Win32 Error for above) SHGetKnownFolderPath: 5 - Access is denied.
FOLDERID_Desktop:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Documents:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Downloads:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Favorites:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Links: C:\Users\testuser\Links
FOLDERID_Music:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Pictures:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Programs:
(Win32 Error for above) SHGetKnownFolderPath: 5 - Access is denied.
FOLDERID_SavedGames: C:\Users\testuser\Saved Games
FOLDERID_Startup:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Templates:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Videos:
(Win32 Error for above) SHGetKnownFolderPath: 2 - The system cannot find the file specified.
FOLDERID_Fonts: C:\WINDOWS\Fonts
FOLDERID_ProgramData: C:\ProgramData
FOLDERID_CommonPrograms: C:\ProgramData\Microsoft\Windows\Start Menu\Programs
FOLDERID_CommonStartup: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
FOLDERID_CommonTemplates: C:\ProgramData\Microsoft\Windows\Templates
FOLDERID_PublicDesktop: C:\Users\Public\Desktop
FOLDERID_PublicDocuments: C:\Users\Public\Documents
FOLDERID_PublicDownloads: C:\Users\Public\Downloads
FOLDERID_PublicMusic: C:\Users\Public\Music
FOLDERID_PublicPictures: C:\Users\Public\Pictures
FOLDERID_PublicVideos: C:\Users\Public\Videos
while outside the service I get:
FOLDERID_LocalAppData: C:\Users\testuser\AppData\Local
FOLDERID_RoamingAppData: C:\Users\testuser\AppData\Roaming
FOLDERID_Desktop: C:\Users\testuser\Desktop
FOLDERID_Documents: C:\Users\testuser\Documents
FOLDERID_Downloads: C:\Users\testuser\Downloads
FOLDERID_Favorites: C:\Users\testuser\Favorites
FOLDERID_Links: C:\Users\testuser\Links
FOLDERID_Music: C:\Users\testuser\Music
FOLDERID_Pictures: C:\Users\testuser\Pictures
FOLDERID_Programs: C:\Users\testuser\AppData\Roaming\Microsoft\Windows\Start Menu\Programs
FOLDERID_SavedGames: C:\Users\testuser\Saved Games
FOLDERID_Startup: C:\Users\testuser\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
FOLDERID_Templates: C:\Users\testuser\AppData\Roaming\Microsoft\Windows\Templates
FOLDERID_Videos: C:\Users\testuser\Videos
FOLDERID_Fonts: C:\WINDOWS\Fonts
FOLDERID_ProgramData: C:\ProgramData
FOLDERID_CommonPrograms: C:\ProgramData\Microsoft\Windows\Start Menu\Programs
FOLDERID_CommonStartup: C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
FOLDERID_CommonTemplates: C:\ProgramData\Microsoft\Windows\Templates
FOLDERID_PublicDesktop: C:\Users\Public\Desktop
FOLDERID_PublicDocuments: C:\Users\Public\Documents
FOLDERID_PublicDownloads: C:\Users\Public\Downloads
FOLDERID_PublicMusic: C:\Users\Public\Music
FOLDERID_PublicPictures: C:\Users\Public\Pictures
FOLDERID_PublicVideos: C:\Users\Public\Videos
I don't know why SHGetKnownFolderPath fails like this. (in C# it just returns null)
I found another question that seems to be having exactly the same problem, however I'm already calling LoadUserProfile, so the solution doesn't work for me: SHGetKnownFolderPath fails with E_ACCESSDENIED