在 C# 中访问模拟用户的 HKCU 注册表配置单元

Access the HKCU registry hive for an impersonated user in C#

我需要实现模拟域用户的功能。模拟线程需要能够将 from/write 读取到模拟用户的 HKCU 注册表配置单元。我能够模拟用户,但是当我尝试加载任何注册表项时,我收到 Win32 "Access is denied" 异常。

注意:这里的目的是提供一个伪模拟命令行来作为服务帐户执行一组特定的操作。服务帐户可能没有交互式登录权限,因此我需要使用 BATCH 登录类型。作为测试,我确实也尝试了INTERACTIVE登录类型,但结果是一样的。

我遵循 this CodeProject article 作为一般指南。这是我拥有的:

partial class Program
{
    [DllImport("advapi32.dll")]
    public static extern int LogonUser(String lpszUserName, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool RevertToSelf();

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public static extern bool CloseHandle(IntPtr handle);

    [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
    public static extern int RegOpenCurrentUser(int samDesired, out IntPtr phkResult);

    [DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern bool LoadUserProfile(IntPtr hToken, ref ProfileInfo lpProfileInfo);

    [DllImport("Userenv.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true, CharSet = CharSet.Auto)]
    public static extern bool UnloadUserProfile(IntPtr hToken, IntPtr lpProfileInfo);

    [StructLayout(LayoutKind.Sequential)]
    public struct ProfileInfo
    {
        public int dwSize;
        public int dwFlags;
        public string lpUserName;
        public string lpProfilePath;
        public string lpDefaultPath;
        public string lpServerName;
        public string lpPolicyPath;
        public IntPtr hProfile;
    }

    private static string ImpUser = string.Empty;
    private static string ImpDomain = string.Empty;
    private static string FullyQualifiedImpUser
    {
        get
        {
            return $"{ImpDomain}\{ImpUser}";
        }
    }
    private static SecureString ImpSecret = new SecureString();
    private static bool CurrentlyImpersonating = false;

    private static WindowsIdentity ImpersonatedIdentity = null;
    private static IntPtr Token = IntPtr.Zero;
    private static IntPtr TokenDuplicate = IntPtr.Zero;

    //*** THIS IS THE CORE METHOD ***
    private static void EnterModeImpersonated()
    {
        bool loadSuccess;
        int errCode;

        try
        {
            if (RevertToSelf())
            {

                if (LogonUser(ImpUser, ImpDomain,
                              ImpSecret.Plaintext(), Constants.LOGON32_LOGON_TYPE_BATCH,
                              Constants.LOGON32_PROVIDER_DEFAULT, ref Token) != 0)
                {
                    if (DuplicateToken(Token, Constants.SecurityImpersonation, ref TokenDuplicate) != 0)
                    {
                        ImpersonatedIdentity = new WindowsIdentity(TokenDuplicate);
                        using (WindowsImpersonationContext m_ImpersonationContext = ImpersonatedIdentity.Impersonate())
                        {
                            if (m_ImpersonationContext != null)
                            {

                                #region LoadUserProfile
                                // Load user profile
                                ProfileInfo profileInfo = new ProfileInfo();
                                profileInfo.dwSize = Marshal.SizeOf(profileInfo);
                                profileInfo.lpUserName = ImpUser;
                                profileInfo.dwFlags = 1;

                                //Here is where I die:
                                loadSuccess = LoadUserProfile(TokenDuplicate, ref profileInfo);

                                if (!loadSuccess)
                                {
                                    errCode = Marshal.GetLastWin32Error();
                                    Win32Exception ex = new Win32Exception(errCode);
                                    throw new Exception($"Failed to load profile for {FullyQualifiedImpUser}. Error code: {errCode}", ex);
                                }

                                if (profileInfo.hProfile == IntPtr.Zero)
                                {
                                    errCode = Marshal.GetLastWin32Error();
                                    Win32Exception ex = new Win32Exception(errCode);
                                    throw new Exception($"Failed accessing HKCU registry for {FullyQualifiedImpUser}. Error code: {errCode}", ex);
                                }
                                #endregion

                                CloseHandle(Token);
                                CloseHandle(TokenDuplicate);

                                RegistryAgent.GetRootKeys(profileInfo.hProfile);

                                EnterMode();

                                UnloadUserProfile(TokenDuplicate, profileInfo.hProfile);
                                m_ImpersonationContext.Undo();
                                RegistryAgent.GetRootKeys(Constants.RevertToInvoker);
                            }
                        }
                    }
                    else
                    {
                        Console.WriteLine("DuplicateToken() failed with error code: " +
                                          Marshal.GetLastWin32Error());
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
            }
        }
        catch (Win32Exception we)
        {
            throw we;
        }
        catch
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        finally
        {
            if (Token != IntPtr.Zero) CloseHandle(Token);
            if (TokenDuplicate != IntPtr.Zero) CloseHandle(TokenDuplicate);

            Console.WriteLine("After finished impersonation: " +
                              WindowsIdentity.GetCurrent().Name);
        }
    }

    //Toggles on impersonation mode
    //Here, we grab the username, domain and password.
    private static bool EnableImpersonation(string userInfo)
    {
        if (userInfo.Contains('\'))
        {
            string[] parts = Parameter.ImpUser.TextValue.Split('\');
            ImpUser = parts[1];
            ImpDomain = parts[0];
        }
        else
        {
            ImpUser = userInfo;
            ImpDomain = Environment.UserDomainName;
        }

        //Prompt for the invoker to enter the impersonated account password
        GetSecret();

        if (TryImpersonate())
        {
            CurrentlyImpersonating = true;
        }
        else
        {
            DisableImpersonation();
        }

        return CurrentlyImpersonating;
    }

    //Toggles off impersontation & cleans up
    private static void DisableImpersonation()
    {
        ImpSecret = null;
        ImpersonatedIdentity = null;
        Token = IntPtr.Zero;
        TokenDuplicate = IntPtr.Zero;
        ImpUser = string.Empty;
        ImpDomain = string.Empty;

        CurrentlyImpersonating = false;
    }

    //Implements a console prompt to grab the impersonated account password
    //as a SecureString object
    private static void GetSecret()
    {
        ImpSecret = new SecureString();
        ConsoleKeyInfo key;

        Console.Write($"\r\nEnter the password for {FullyQualifiedImpUser}: ");
        do
        {
            key = Console.ReadKey(true);
            if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter)
            {
                ImpSecret.AppendChar(key.KeyChar);
                Console.Write("*");
            }
            else
            {
                if (key.Key == ConsoleKey.Backspace && ImpSecret.Length != 0)
                {
                    ImpSecret.RemoveAt(ImpSecret.Length - 1);
                    Console.Write("\b \b");
                }
            }
        }
        while (key.Key != ConsoleKey.Enter);
        Console.WriteLine();
    }

    //This method is intended to ensure that the credentials entered
    //for the impersonated user are correct.
    private static bool TryImpersonate()
    {
        IntPtr testToken = IntPtr.Zero;
        int result;

        try
        {
            result = LogonUser(ImpUser, ImpDomain, ImpSecret.Plaintext(), Constants.LOGON32_LOGON_TYPE_BATCH, Constants.LOGON32_PROVIDER_DEFAULT, ref testToken);

            if (result == 0)
            {
                int errCode = Marshal.GetLastWin32Error();
                Win32Exception ex = new Win32Exception(errCode);
                throw new Exception($"Failed to impersonate {FullyQualifiedImpUser}. Error code: {errCode}", ex);
            }

            return true;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            return false;
        }
    }
}

我还阅读了 The MSDN documentation for LoadUserProfileA(我没有找到关于 LoadUserProfile() 的文章,所以我不得不假设这是被调用的最终 COM 函数)。它表示: 令牌必须具有 TOKEN_QUERY、TOKEN_IMPERSONATE 和 TOKEN_DUPLICATE 访问权限。。我想知道是否需要以不同方式创建登录令牌或重复令牌以包含这些权利?不过,我找不到任何关于如何操作令牌权限的文档...

我能够解决这个问题。这是我所做的:

首先,有几个Win32方法需要暴露:

[DllImport("advapi32.dll")]
public static extern int LogonUser(String lpszUserName, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);

[DllImport("userenv.dll")]
public static extern bool LoadUserProfile(IntPtr hToken, ref ProfileInfo lpProfileInfo);

[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
public static extern int RegDisablePredefinedCache();

您还需要定义一个结构来支持调用 LoadUserProfile()

[StructLayout(LayoutKind.Sequential)]
    public struct ProfileInfo
    {
        public int dwSize;
        public int dwFlags;
        public string lpUserName;
        public string lpProfilePath;
        public string lpDefaultPath;
        public string lpServerName;
        public string lpPolicyPath;
        public IntPtr hProfile;
    }

我们打算将模拟帐户密码存储在 SecureString 对象中,但我们也希望能够以明文形式轻松访问它。

我使用以下方法在控制台提示符下填充 SecureString 密码(屏蔽):

public static SecureString GetPasswordAsSecureString(string prompt)
{
    SecureString pwd = new SecureString();
    ConsoleKeyInfo key;

    Console.Write(prompt + @": ");

    do
    {
        key = Console.ReadKey(true);
        if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter)
        {
            pwd.AppendChar(key.KeyChar);
            Console.Write("*");
        }
        else
        {
            if (key.Key == ConsoleKey.Backspace && pwd.Length != 0)
            {
                pwd.RemoveAt(pwd.Length - 1);
                Console.Write("\b \b");
            }
        }
    }
    while (key.Key != ConsoleKey.Enter);
    Console.WriteLine();
    return pwd;
}

var impPassword = GetPasswordAsSecureString($"Enter the password for {impUser}");

我还建议定义以下扩展方法,以便方便地将 SecureString 转换为普通字符串,因为我们需要使用的 Win32 方法之一只接受普通字符串:

public static string ToUnSecureString(this SecureString securePassword)
{
    if (securePassword == null)
    {
        return string.Empty;
    }

    IntPtr unmanagedString = IntPtr.Zero;
    try
    {
        unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(securePassword);
        return Marshal.PtrToStringUni(unmanagedString);
    }
    finally
    {
        Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);
    }
}

在执行与模拟有关的任何其他操作之前,我们需要调用 Win32 方法 RegDisablePredefinedCache()。就我们的目的而言,此方法告诉 Windows 动态确定在哪里寻找 HKEY_CURRENT_USER 注册表配置单元,而不是使用最初调用进程时的缓存位置(失败调用此方法解释了我之前收到的 "Access is denied" 异常。模拟用户试图为调用者的帐户加载 HKCU 配置单元,这显然是不允许的)

RegDisablePredefinedCache();

接下来,我们需要在进入模拟线程之前加载该帐户的配置文件。这确保模拟帐户的注册表配置单元在内存中可用。我们调用 LogonUser()LoadUserProfile() COM 方法来实现:

// Get a token for the user
const int LOGON32_LOGON_BATCH = 4;
const int LOGON32_PROVIDER_DEFAULT = 0;

//We'll use our extension method to pass the password as a normal string
LogonUser(ImpUser, ImpDomain, ImpPassword.ToUnSecureString(), LOGON32_LOGON_BATCH, LOGON32_PROVIDER_DEFAULT, ref Token);

// Load user profile
ProfileInfo profileInfo = new ProfileInfo();
profileInfo.dwSize = Marshal.SizeOf(profileInfo);
profileInfo.lpUserName = ImpUser;
profileInfo.dwFlags = 1;
bool loadSuccess = LoadUserProfile(Token, ref profileInfo);

//Detect and handle failure gracefully
if (!loadSuccess)
{
    errCode = Marshal.GetLastWin32Error();
    Win32Exception ex = new Win32Exception(errCode);
    throw new Exception($"Failed to load profile for {ImpUser}. Error code: {errCode}", ex);
}

if (profileInfo.hProfile == IntPtr.Zero)
{
    errCode = Marshal.GetLastWin32Error();
    Win32Exception ex = new Win32Exception(errCode);
    throw new Exception($"Failed accessing HKCU registry for {ImpUser}. Error code: {errCode}", ex);
}

最后,感谢对这个问题留下的评论之一,我发现了一个名为 SimpleImpersonation 的漂亮的 nuget 包。这混淆了帐户模拟所涉及的大部分复杂性:

//Note that UserCredentials() constructor I chose requires the  
//password to be passed as a SecureString object.
var Credentials = new UserCredentials(impDomain, impUser, impPassword);
Impersonation.RunAsUser(Credentials, LogonType.Batch, () =>
{
    //Within this bock, you can call methods such as
    //Registry.CurrentUser.OpenSubKey()
    //and they use the impersonated account's registry hive
}