使用来自 WTSQueryUserToken 的令牌将注册表配置单元加载到 HKEY_USERS

Load registry hive into HKEY_USERS using token from WTSQueryUserToken

我正在尝试调用 CreateProcessAsUser 以 运行 服务用户的桌面应用程序。 (参见 Launching a process in user’s session from a service and CreateProcessAsUser from service and user security issues

基本可以,但有些应用程序无法正常运行。最终,经过一些调试后,我发现一个 C# 应用程序失败,因为 Environment.GetFolderPath returns null 用于特殊文件夹。我认为这可能是因为调用的用户注册表或其中的某些路径未正确加载用户配置文件。

REMARKS section of CreateProcessAsUserW 中有一些提示,说明了为用户创建流程时需要考虑的事项。我已经在使用 CreateEnvironmentBlock 创建一个环境块(并且环境变量似乎有效),但它也说:

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. [...]

我已经查看了 LoadUserProfile 的文档,其中有类似的内容:

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.

但是我没有找到任何示例或更多详细信息如何将用户的注册表配置单元加载到 HKEY_USERS 注册表项和我查看的注册表 API(RegLoadKey 看起来最有希望,但是我不认为这是正确的或如何将它与我的目标进程一起使用 before CreateProcessAsUser)不要执行 LoadUserProfile 上描述的操作。此外,我查看的所有 Whosebug 问题都没有提到这个,或者只是在他们正在使用这个函数的那一边提到。

我想也许传递配置文件可能是 STARTUPINFOEX 的一部分,但我也没有在 the documentation 中找到任何关于“配置文件”或“注册表”的信息。

我真的很努力地试图找出如何处理 LoadUserProfile 结果(结构 PROFILEINFO)以及如何将它与 CreateProcessAsUserW 函数一起使用。如果你能给我一些提示,我会很高兴我实际上想用那个函数做什么,以及我如何将它与 CreateProcessAsUserW 一起使用,因为我已经通过 WTSQueryUserToken.

获得了用户令牌

这是我当前的 C# 代码,我是如何生成进程的(它还包含一些管道代码,为了便于阅读,我省略了这些代码):

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('[=11=]');
        }

        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] == '[=11=]' && envPtr[i - 1] == '[=11=]'))
        {
            int start = i;
            while (envPtr[i] != '[=11=]' && envPtr[i] != '=')
            {
                i++;
            }
            // malformed check, needs =
            if (envPtr[i] == '[=11=]')
                continue;

            var name = Marshal.PtrToStringUni((IntPtr)(envPtr + start), i - start);
            i++;
            start = i;

            while (envPtr[i] != '[=11=]')
            {
                i++;
            }

            var value = Marshal.PtrToStringUni((IntPtr)(envPtr + start), i - start);
            i++;

            if (!envOverride.Keys.Contains(name))
            {
                environment.Append(name)
                    .Append('=')
                    .Append(value)
                    .Append('[=11=]');
            }
        }

        if (!MyWin32.DestroyEnvironmentBlock(lpEnvironment))
        {
            PrintWarning(
                "failed to free environment block, leaking memory (DestroyEnvironmentBlock)",
                win32: true);
        }

        environment.Append('[=11=]');
        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);
    }
}

然后,当我尝试打印出该应用程序中的一堆已知文件夹时(使用 SHGetSpecialFolderPathW),我在服务中得到以下信息:

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

在服务之外我得到:

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

我不知道为什么 SHGetKnownFolderPath 会这样失败。 (在 C# 中它只是 returns null)

我发现另一个问题似乎有完全相同的问题,但我已经在调用 LoadUserProfile,所以解决方案对我不起作用:

好的,问题是我向进程传递了错误的环境变量。我正确地使用了 CreateEnvironmentBlock,但提供了一种通过我的 API.

覆盖环境变量的方法

我的 API 使用 ProcessStartInfo,它将其环境字典默认为 运行 进程环境(系统服务环境),然后覆盖为用户生成的环境变量。

这导致已知文件夹 API SHGetKnownFolderPath 失败并出现错误 2(系统找不到指定的文件)或错误 5(访问被拒绝)

因此,不使用服务用户环境覆盖环境变量解决了问题,现在程序 运行 正确。