Reputation: 634
I need to implement functionality that impersonates a domain user. The impersonated thread needs to be able to read from/write to HKCU registry hive for the impersonated user. I am able to impersonate the user, but when I attempt to load any registry keys, I receive a Win32 "Access is denied" exception.
NOTE: The intention here is to provide a pseudo impersonated command-line to perform a specific set of actions as a service account. The service account may not have interactive logon rights, so I am required to use the BATCH logon type. As a test, I did also try the INTERACTIVE logon type, but the result was the same.
I followed this CodeProject article as a general guide. Here is what I have:
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;
}
}
}
I also read The MSDN documentation for LoadUserProfileA (I didn't find an article for LoadUserProfile() so I have to assume this is the ultimate COM function being called). It indicates: The token must have TOKEN_QUERY, TOKEN_IMPERSONATE, and TOKEN_DUPLICATE access.. I'm wondering if the logon token or duplicated token needs to be created differently in order to include these rights? I wasn't able to find any documentation on how to manipulate the token rights, though...
Upvotes: 1
Views: 787
Reputation: 634
I was able to resolve this problem. Here's what I did:
First, there are several Win32 methods which need to be exposed:
[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();
You'll also need to define a struct in support of calling 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;
}
We're going to store the impersonation account password in a SecureString object, but we also want to be able to easily access it as plaintext.
I used the following method to populate the SecureString password, masked, at a console prompt:
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}");
I also recommend defining the following extension method in order to conveniently convert a SecureString to a normal string since one of the Win32 methods we need to use will only accept a normal string:
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);
}
}
Before doing anything else having to do with impersonation, we need to call the Win32 method RegDisablePredefinedCache(). In terms of our purpose, this method tells Windows to dynamically determine where to look for the HKEY_CURRENT_USER registry hive, rather than using the cached location from when the process was initially invoked (Failing to call this method explains the "Access is denied" exception I was receiving previously. The impersonated user was attempting to load the HKCU hive for the invoker's account, which is obviously not allowed)
RegDisablePredefinedCache();
Next, we need to load that account's profile before entering the impersonated thread. This ensures that the impersonated account's registry hive is available in memory. We call the LogonUser() and LoadUserProfile() COM methods in order to accomplish that:
// 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);
}
Finally, thanks to one of the comments left on this question, I discovered a nifty nuget package called SimpleImpersonation. This obfuscates away most of the complexity involved with account impersonation:
//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
}
Upvotes: 2